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,390 @@
1
+ import React from 'react';
2
+ import {
3
+ render,
4
+ fireEvent,
5
+ waitFor,
6
+ screen,
7
+ act,
8
+ } from '@testing-library/react';
9
+ import ManageDimensionLinksDialog from '../ManageDimensionLinksDialog';
10
+ import DJClientContext from '../../../providers/djclient';
11
+
12
+ // Mock window.location.reload
13
+ delete window.location;
14
+ window.location = { reload: jest.fn() };
15
+
16
+ // Mock window.confirm
17
+ window.confirm = jest.fn(() => true);
18
+
19
+ const mockDjClient = {
20
+ DataJunctionAPI: {
21
+ linkDimension: jest.fn(),
22
+ unlinkDimension: jest.fn(),
23
+ addReferenceDimensionLink: jest.fn(),
24
+ removeReferenceDimensionLink: jest.fn(),
25
+ },
26
+ node: jest.fn().mockResolvedValue({
27
+ name: 'default.test_dimension',
28
+ columns: [{ name: 'id', type: 'int' }],
29
+ }),
30
+ };
31
+
32
+ describe('<ManageDimensionLinksDialog />', () => {
33
+ beforeEach(() => {
34
+ jest.clearAllMocks();
35
+ window.alert = jest.fn();
36
+ window.confirm.mockReturnValue(true);
37
+ });
38
+
39
+ const defaultProps = {
40
+ column: { name: 'test_column', type: 'int' },
41
+ node: { name: 'default.node1' },
42
+ dimensions: [
43
+ { value: 'default.dim1', label: 'dim1 (5 links)' },
44
+ { value: 'default.dim2', label: 'dim2 (3 links)' },
45
+ ],
46
+ fkLinks: [],
47
+ onSubmit: jest.fn(),
48
+ };
49
+
50
+ it('renders the toggle button', () => {
51
+ const { getByLabelText } = render(
52
+ <DJClientContext.Provider value={mockDjClient}>
53
+ <ManageDimensionLinksDialog {...defaultProps} />
54
+ </DJClientContext.Provider>,
55
+ );
56
+
57
+ expect(getByLabelText('ManageDimensionLinksToggle')).toBeInTheDocument();
58
+ });
59
+
60
+ it('opens modal when toggle button is clicked', async () => {
61
+ const { getByLabelText, getByRole } = render(
62
+ <DJClientContext.Provider value={mockDjClient}>
63
+ <ManageDimensionLinksDialog {...defaultProps} />
64
+ </DJClientContext.Provider>,
65
+ );
66
+
67
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
68
+
69
+ await waitFor(() => {
70
+ expect(
71
+ getByRole('dialog', { name: 'ManageDimensionLinksDialog' }),
72
+ ).toBeInTheDocument();
73
+ });
74
+ });
75
+
76
+ it('displays column name and type in header', async () => {
77
+ const { getByLabelText, getByText } = render(
78
+ <DJClientContext.Provider value={mockDjClient}>
79
+ <ManageDimensionLinksDialog
80
+ column={{ name: 'test_column', type: 'varchar' }}
81
+ node={defaultProps.node}
82
+ dimensions={defaultProps.dimensions}
83
+ fkLinks={[]}
84
+ onSubmit={defaultProps.onSubmit}
85
+ />
86
+ </DJClientContext.Provider>,
87
+ );
88
+
89
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
90
+
91
+ await waitFor(() => {
92
+ expect(getByText('test_column')).toBeInTheDocument();
93
+ expect(getByText('varchar')).toBeInTheDocument();
94
+ });
95
+ });
96
+
97
+ it('shows two tabs: FK Links and Reference Links', async () => {
98
+ const { getByLabelText, getByText } = render(
99
+ <DJClientContext.Provider value={mockDjClient}>
100
+ <ManageDimensionLinksDialog {...defaultProps} />
101
+ </DJClientContext.Provider>,
102
+ );
103
+
104
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
105
+
106
+ await waitFor(() => {
107
+ expect(getByText('FK Links')).toBeInTheDocument();
108
+ expect(getByText('Reference Links')).toBeInTheDocument();
109
+ });
110
+ });
111
+
112
+ it('switches to reference links tab when clicked', async () => {
113
+ const { getByLabelText, getByText } = render(
114
+ <DJClientContext.Provider value={mockDjClient}>
115
+ <ManageDimensionLinksDialog {...defaultProps} />
116
+ </DJClientContext.Provider>,
117
+ );
118
+
119
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
120
+
121
+ await waitFor(() => {
122
+ expect(getByText('Reference Links')).toBeInTheDocument();
123
+ });
124
+
125
+ fireEvent.click(getByText('Reference Links'));
126
+
127
+ await waitFor(() => {
128
+ expect(getByText('Dimension Node *')).toBeInTheDocument();
129
+ expect(getByText('Dimension Column *')).toBeInTheDocument();
130
+ });
131
+ });
132
+
133
+ it('displays FK links when provided', async () => {
134
+ const propsWithFkLinks = {
135
+ ...defaultProps,
136
+ fkLinks: ['default.dim1', 'default.dim2'],
137
+ };
138
+
139
+ const { getByLabelText } = render(
140
+ <DJClientContext.Provider value={mockDjClient}>
141
+ <ManageDimensionLinksDialog {...propsWithFkLinks} />
142
+ </DJClientContext.Provider>,
143
+ );
144
+
145
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
146
+
147
+ await waitFor(() => {
148
+ // The FK Links tab should be displayed by default
149
+ expect(getByLabelText('ManageDimensionLinksToggle')).toBeInTheDocument();
150
+ });
151
+ });
152
+
153
+ it('shows FK Links form with select dimensions field', async () => {
154
+ const { getByLabelText, getByText } = render(
155
+ <DJClientContext.Provider value={mockDjClient}>
156
+ <ManageDimensionLinksDialog {...defaultProps} />
157
+ </DJClientContext.Provider>,
158
+ );
159
+
160
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
161
+
162
+ await waitFor(() => {
163
+ expect(getByText('Select Dimensions')).toBeInTheDocument();
164
+ expect(getByText('Save')).toBeInTheDocument();
165
+ });
166
+ });
167
+
168
+ it('displays reference link form fields', async () => {
169
+ const { getByLabelText, getByText, getByPlaceholderText } = render(
170
+ <DJClientContext.Provider value={mockDjClient}>
171
+ <ManageDimensionLinksDialog {...defaultProps} />
172
+ </DJClientContext.Provider>,
173
+ );
174
+
175
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
176
+
177
+ await waitFor(() => {
178
+ expect(getByText('Reference Links')).toBeInTheDocument();
179
+ });
180
+
181
+ // Switch to reference link tab
182
+ fireEvent.click(getByText('Reference Links'));
183
+
184
+ await waitFor(() => {
185
+ expect(getByText('Dimension Node *')).toBeInTheDocument();
186
+ expect(getByText('Dimension Column *')).toBeInTheDocument();
187
+ expect(
188
+ getByPlaceholderText('e.g., birth_date, registration_date'),
189
+ ).toBeInTheDocument();
190
+ expect(getByText('Add Link')).toBeInTheDocument();
191
+ });
192
+ });
193
+
194
+ it('handles removing reference link', async () => {
195
+ mockDjClient.DataJunctionAPI.removeReferenceDimensionLink.mockResolvedValue(
196
+ {
197
+ status: 200,
198
+ },
199
+ );
200
+
201
+ const propsWithReferenceLink = {
202
+ ...defaultProps,
203
+ referenceLink: {
204
+ dimension: 'default.dim1',
205
+ dimension_column: 'id',
206
+ },
207
+ };
208
+
209
+ const { getByLabelText, getByText } = render(
210
+ <DJClientContext.Provider value={mockDjClient}>
211
+ <ManageDimensionLinksDialog {...propsWithReferenceLink} />
212
+ </DJClientContext.Provider>,
213
+ );
214
+
215
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
216
+
217
+ await waitFor(() => {
218
+ expect(getByText('Reference Links')).toBeInTheDocument();
219
+ });
220
+
221
+ // Switch to reference link tab
222
+ fireEvent.click(getByText('Reference Links'));
223
+
224
+ await waitFor(() => {
225
+ expect(getByText('Remove Link')).toBeInTheDocument();
226
+ });
227
+
228
+ // Remove the link
229
+ const removeButton = getByText('Remove Link');
230
+ await act(async () => {
231
+ fireEvent.click(removeButton);
232
+ });
233
+
234
+ await waitFor(() => {
235
+ expect(window.confirm).toHaveBeenCalledWith(
236
+ 'Are you sure you want to remove this reference link?',
237
+ );
238
+ expect(
239
+ mockDjClient.DataJunctionAPI.removeReferenceDimensionLink,
240
+ ).toHaveBeenCalledWith('default.node1', 'test_column');
241
+ expect(window.location.reload).toHaveBeenCalled();
242
+ });
243
+ });
244
+
245
+ it('does not remove reference link when user cancels confirm dialog', async () => {
246
+ window.confirm.mockReturnValueOnce(false);
247
+
248
+ const propsWithReferenceLink = {
249
+ ...defaultProps,
250
+ referenceLink: {
251
+ dimension: 'default.dim1',
252
+ dimension_column: 'id',
253
+ },
254
+ };
255
+
256
+ const { getByLabelText, getByText } = render(
257
+ <DJClientContext.Provider value={mockDjClient}>
258
+ <ManageDimensionLinksDialog {...propsWithReferenceLink} />
259
+ </DJClientContext.Provider>,
260
+ );
261
+
262
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
263
+ fireEvent.click(getByText('Reference Links'));
264
+
265
+ await waitFor(() => {
266
+ expect(getByText('Remove Link')).toBeInTheDocument();
267
+ });
268
+
269
+ const removeButton = getByText('Remove Link');
270
+ await act(async () => {
271
+ fireEvent.click(removeButton);
272
+ });
273
+
274
+ expect(
275
+ mockDjClient.DataJunctionAPI.removeReferenceDimensionLink,
276
+ ).not.toHaveBeenCalled();
277
+ });
278
+
279
+ it('handles failed reference link removal', async () => {
280
+ mockDjClient.DataJunctionAPI.removeReferenceDimensionLink.mockResolvedValue(
281
+ {
282
+ status: 500,
283
+ json: { message: 'Server error' },
284
+ },
285
+ );
286
+
287
+ const propsWithReferenceLink = {
288
+ ...defaultProps,
289
+ referenceLink: {
290
+ dimension: 'default.dim1',
291
+ dimension_column: 'id',
292
+ },
293
+ };
294
+
295
+ const { getByLabelText, getByText } = render(
296
+ <DJClientContext.Provider value={mockDjClient}>
297
+ <ManageDimensionLinksDialog {...propsWithReferenceLink} />
298
+ </DJClientContext.Provider>,
299
+ );
300
+
301
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
302
+ fireEvent.click(getByText('Reference Links'));
303
+
304
+ await waitFor(() => {
305
+ expect(getByText('Remove Link')).toBeInTheDocument();
306
+ });
307
+
308
+ const removeButton = getByText('Remove Link');
309
+ await act(async () => {
310
+ fireEvent.click(removeButton);
311
+ });
312
+
313
+ await waitFor(() => {
314
+ expect(window.alert).toHaveBeenCalled();
315
+ expect(window.location.reload).not.toHaveBeenCalled();
316
+ });
317
+ });
318
+
319
+ it('shows Update Link button when reference link exists', async () => {
320
+ const propsWithReferenceLink = {
321
+ ...defaultProps,
322
+ referenceLink: {
323
+ dimension: 'default.dim1',
324
+ dimension_column: 'user_id',
325
+ },
326
+ };
327
+
328
+ const { getByLabelText, getByText } = render(
329
+ <DJClientContext.Provider value={mockDjClient}>
330
+ <ManageDimensionLinksDialog {...propsWithReferenceLink} />
331
+ </DJClientContext.Provider>,
332
+ );
333
+
334
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
335
+ fireEvent.click(getByText('Reference Links'));
336
+
337
+ await waitFor(() => {
338
+ expect(getByText('Update Link')).toBeInTheDocument();
339
+ expect(getByText('Remove Link')).toBeInTheDocument();
340
+ });
341
+ });
342
+
343
+ it('shows validation error when submitting reference link without required fields', async () => {
344
+ const { getByLabelText, getByText, getAllByText } = render(
345
+ <DJClientContext.Provider value={mockDjClient}>
346
+ <ManageDimensionLinksDialog {...defaultProps} />
347
+ </DJClientContext.Provider>,
348
+ );
349
+
350
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
351
+ fireEvent.click(getByText('Reference Links'));
352
+
353
+ await waitFor(() => {
354
+ expect(getByText('Add Link')).toBeInTheDocument();
355
+ });
356
+
357
+ // Try to submit without filling required fields
358
+ const saveButton = getByText('Add Link');
359
+ await act(async () => {
360
+ fireEvent.click(saveButton);
361
+ });
362
+
363
+ await waitFor(() => {
364
+ const requiredErrors = getAllByText('Required');
365
+ expect(requiredErrors.length).toBeGreaterThan(0);
366
+ });
367
+ });
368
+
369
+ it('closes modal when clicking backdrop', async () => {
370
+ const { getByLabelText, getByRole, queryByRole } = render(
371
+ <DJClientContext.Provider value={mockDjClient}>
372
+ <ManageDimensionLinksDialog {...defaultProps} />
373
+ </DJClientContext.Provider>,
374
+ );
375
+
376
+ fireEvent.click(getByLabelText('ManageDimensionLinksToggle'));
377
+
378
+ await waitFor(() => {
379
+ expect(getByRole('dialog')).toBeInTheDocument();
380
+ });
381
+
382
+ // Click the backdrop
383
+ const backdrop = getByRole('dialog').parentElement;
384
+ fireEvent.click(backdrop);
385
+
386
+ await waitFor(() => {
387
+ expect(queryByRole('dialog')).not.toBeInTheDocument();
388
+ });
389
+ });
390
+ });
@@ -50,6 +50,17 @@ describe('<NodePage />', () => {
50
50
  unsubscribeFromNotifications: jest
51
51
  .fn()
52
52
  .mockResolvedValue({ status: 200 }),
53
+ setAttributes: jest.fn().mockResolvedValue({ status: 200 }),
54
+ linkDimension: jest.fn().mockResolvedValue({ status: 200 }),
55
+ unlinkDimension: jest.fn().mockResolvedValue({ status: 200 }),
56
+ addReferenceDimensionLink: jest.fn().mockResolvedValue({ status: 200 }),
57
+ removeReferenceDimensionLink: jest
58
+ .fn()
59
+ .mockResolvedValue({ status: 200 }),
60
+ addComplexDimensionLink: jest.fn().mockResolvedValue({ status: 200 }),
61
+ removeComplexDimensionLink: jest
62
+ .fn()
63
+ .mockResolvedValue({ status: 200 }),
53
64
  },
54
65
  };
55
66
  };
@@ -408,15 +419,15 @@ describe('<NodePage />', () => {
408
419
 
409
420
  it('renders the NodeColumns tab correctly', async () => {
410
421
  const djClient = mockDJClient();
411
- djClient.DataJunctionAPI.node.mockReturnValue(mocks.mockMetricNode);
422
+ djClient.DataJunctionAPI.node.mockResolvedValue(mocks.mockMetricNode);
412
423
  djClient.DataJunctionAPI.getMetric.mockReturnValue(
413
424
  mocks.mockMetricNodeJson,
414
425
  );
415
- djClient.DataJunctionAPI.columns.mockReturnValue(mocks.metricNodeColumns);
416
- djClient.DataJunctionAPI.attributes.mockReturnValue(mocks.attributes);
417
- djClient.DataJunctionAPI.dimensions.mockReturnValue(mocks.dimensions);
426
+ djClient.DataJunctionAPI.columns.mockResolvedValue(mocks.metricNodeColumns);
427
+ djClient.DataJunctionAPI.attributes.mockResolvedValue(mocks.attributes);
428
+ djClient.DataJunctionAPI.dimensions.mockResolvedValue(mocks.dimensions);
418
429
  djClient.DataJunctionAPI.engines.mockReturnValue([]);
419
- djClient.DataJunctionAPI.setPartition.mockReturnValue({
430
+ djClient.DataJunctionAPI.setPartition.mockResolvedValue({
420
431
  status: 200,
421
432
  json: { message: '' },
422
433
  });
@@ -459,14 +470,14 @@ describe('<NodePage />', () => {
459
470
  screen.getByRole('button', { name: 'SaveEditColumn' }),
460
471
  ).toBeInTheDocument();
461
472
 
462
- // check that the link dimension popover can be clicked
463
- const linkDimensionPopover = screen.getByRole('button', {
464
- name: 'LinkDimension',
473
+ // check that the manage dimension links dialog can be opened
474
+ const manageDimensionLinksButton = screen.getByRole('button', {
475
+ name: 'ManageDimensionLinksToggle',
465
476
  });
466
- expect(linkDimensionPopover).toBeInTheDocument();
467
- fireEvent.click(linkDimensionPopover);
477
+ expect(manageDimensionLinksButton).toBeInTheDocument();
478
+ fireEvent.click(manageDimensionLinksButton);
468
479
  expect(
469
- screen.getByRole('button', { name: 'SaveLinkDimension' }),
480
+ screen.getByRole('dialog', { name: 'ManageDimensionLinksDialog' }),
470
481
  ).toBeInTheDocument();
471
482
 
472
483
  // check that the set column partition popover can be clicked
@@ -483,7 +494,6 @@ describe('<NodePage />', () => {
483
494
  expect(screen.getByText('Saved!'));
484
495
  });
485
496
  }, 60000);
486
- // check compiled SQL on nodeInfo page
487
497
 
488
498
  it('renders the NodeHistory tab correctly', async () => {
489
499
  const djClient = mockDJClient();
@@ -69,7 +69,8 @@ describe('<RegisterTablePage />', () => {
69
69
  const catalog = getByTestId('choose-catalog');
70
70
  await waitFor(async () => {
71
71
  fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
72
- fireEvent.click(screen.getByText('warehouse'));
72
+ const warehouseOptions = screen.getAllByText('warehouse');
73
+ fireEvent.click(warehouseOptions[warehouseOptions.length - 1]);
73
74
  });
74
75
 
75
76
  await userEvent.type(screen.getByLabelText('Schema'), 'schema');
@@ -99,7 +100,8 @@ describe('<RegisterTablePage />', () => {
99
100
  const catalog = getByTestId('choose-catalog');
100
101
  await waitFor(async () => {
101
102
  fireEvent.keyDown(catalog.firstChild, { key: 'ArrowDown' });
102
- fireEvent.click(screen.getByText('warehouse'));
103
+ const warehouseOptions = screen.getAllByText('warehouse');
104
+ fireEvent.click(warehouseOptions[warehouseOptions.length - 1]);
103
105
  });
104
106
 
105
107
  await userEvent.type(screen.getByLabelText('Schema'), 'schema');
@@ -1090,10 +1090,10 @@ export const DataJunctionAPI = {
1090
1090
  'Content-Type': 'application/json',
1091
1091
  },
1092
1092
  body: JSON.stringify({
1093
- dimensionNode: dimensionNode,
1094
- joinType: joinType,
1095
- joinOn: joinOn,
1096
- joinCardinality: joinCardinality,
1093
+ dimension_node: dimensionNode,
1094
+ join_type: joinType,
1095
+ join_on: joinOn,
1096
+ join_cardinality: joinCardinality,
1097
1097
  role: role,
1098
1098
  }),
1099
1099
  credentials: 'include',
@@ -1112,7 +1112,7 @@ export const DataJunctionAPI = {
1112
1112
  'Content-Type': 'application/json',
1113
1113
  },
1114
1114
  body: JSON.stringify({
1115
- dimensionNode: dimensionNode,
1115
+ dimension_node: dimensionNode,
1116
1116
  role: role,
1117
1117
  }),
1118
1118
  credentials: 'include',
@@ -1120,6 +1120,46 @@ export const DataJunctionAPI = {
1120
1120
  return { status: response.status, json: await response.json() };
1121
1121
  },
1122
1122
 
1123
+ addReferenceDimensionLink: async function (
1124
+ nodeName,
1125
+ nodeColumn,
1126
+ dimensionNode,
1127
+ dimensionColumn,
1128
+ role = null,
1129
+ ) {
1130
+ const url = new URL(
1131
+ `${DJ_URL}/nodes/${nodeName}/columns/${nodeColumn}/link`,
1132
+ );
1133
+ url.searchParams.append('dimension_node', dimensionNode);
1134
+ url.searchParams.append('dimension_column', dimensionColumn);
1135
+ if (role) {
1136
+ url.searchParams.append('role', role);
1137
+ }
1138
+
1139
+ const response = await fetch(url.toString(), {
1140
+ method: 'POST',
1141
+ headers: {
1142
+ 'Content-Type': 'application/json',
1143
+ },
1144
+ credentials: 'include',
1145
+ });
1146
+ return { status: response.status, json: await response.json() };
1147
+ },
1148
+
1149
+ removeReferenceDimensionLink: async function (nodeName, nodeColumn) {
1150
+ const response = await fetch(
1151
+ `${DJ_URL}/nodes/${nodeName}/columns/${nodeColumn}/link`,
1152
+ {
1153
+ method: 'DELETE',
1154
+ headers: {
1155
+ 'Content-Type': 'application/json',
1156
+ },
1157
+ credentials: 'include',
1158
+ },
1159
+ );
1160
+ return { status: response.status, json: await response.json() };
1161
+ },
1162
+
1123
1163
  deactivate: async function (nodeName) {
1124
1164
  const response = await fetch(`${DJ_URL}/nodes/${nodeName}`, {
1125
1165
  method: 'DELETE',
@@ -1409,7 +1449,7 @@ export const DataJunctionAPI = {
1409
1449
  url.searchParams.append('entity_type', entity_type);
1410
1450
  url.searchParams.append('entity_name', entity_name);
1411
1451
 
1412
- const response = await fetch(url, {
1452
+ const response = await fetch(url.toString(), {
1413
1453
  method: 'DELETE',
1414
1454
  credentials: 'include',
1415
1455
  });