datajunction-ui 0.0.93 → 0.0.94

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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NodeComponents.jsx +4 -0
  3. package/src/app/components/Tab.jsx +11 -16
  4. package/src/app/components/__tests__/Tab.test.jsx +4 -2
  5. package/src/app/hooks/useWorkspaceData.js +226 -0
  6. package/src/app/index.tsx +17 -1
  7. package/src/app/pages/MyWorkspacePage/ActiveBranchesSection.jsx +38 -107
  8. package/src/app/pages/MyWorkspacePage/MyNodesSection.jsx +31 -6
  9. package/src/app/pages/MyWorkspacePage/MyWorkspacePage.css +5 -0
  10. package/src/app/pages/MyWorkspacePage/NeedsAttentionSection.jsx +86 -100
  11. package/src/app/pages/MyWorkspacePage/TypeGroupGrid.jsx +7 -11
  12. package/src/app/pages/MyWorkspacePage/__tests__/ActiveBranchesSection.test.jsx +79 -11
  13. package/src/app/pages/MyWorkspacePage/__tests__/CollectionsSection.test.jsx +22 -0
  14. package/src/app/pages/MyWorkspacePage/__tests__/MaterializationsSection.test.jsx +57 -0
  15. package/src/app/pages/MyWorkspacePage/__tests__/MyNodesSection.test.jsx +60 -18
  16. package/src/app/pages/MyWorkspacePage/__tests__/MyWorkspacePage.test.jsx +156 -162
  17. package/src/app/pages/MyWorkspacePage/__tests__/NeedsAttentionSection.test.jsx +17 -18
  18. package/src/app/pages/MyWorkspacePage/__tests__/NotificationsSection.test.jsx +179 -0
  19. package/src/app/pages/MyWorkspacePage/__tests__/TypeGroupGrid.test.jsx +169 -49
  20. package/src/app/pages/MyWorkspacePage/index.jsx +41 -73
  21. package/src/app/pages/NodePage/NodeDataFlowTab.jsx +464 -0
  22. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +1 -1
  23. package/src/app/pages/NodePage/NodeDimensionsTab.jsx +362 -0
  24. package/src/app/pages/NodePage/NodeLineageTab.jsx +1 -0
  25. package/src/app/pages/NodePage/NodesWithDimension.jsx +3 -3
  26. package/src/app/pages/NodePage/__tests__/NodeDataFlowTab.test.jsx +428 -0
  27. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +18 -1
  28. package/src/app/pages/NodePage/__tests__/NodeDimensionsTab.test.jsx +362 -0
  29. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +28 -3
  30. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +2 -2
  31. package/src/app/pages/NodePage/index.jsx +15 -8
  32. package/src/app/services/DJService.js +73 -6
  33. package/src/app/services/__tests__/DJService.test.jsx +591 -0
  34. package/src/styles/index.css +32 -0
@@ -0,0 +1,362 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { MemoryRouter } from 'react-router-dom';
4
+ import DJClientContext from '../../../providers/djclient';
5
+ import NodeDimensionsTab from '../NodeDimensionsTab';
6
+
7
+ // Mock reactflow — renders DimNode components directly to cover lines 42-136
8
+ jest.mock('reactflow', () => {
9
+ const { useState } = require('react');
10
+ const React = require('react');
11
+ return {
12
+ __esModule: true,
13
+ default: ({ nodes, nodeTypes }) => {
14
+ return (
15
+ <div data-testid="reactflow">
16
+ {(nodes || []).map(n => {
17
+ const NodeComp = nodeTypes?.[n.type];
18
+ return NodeComp ? (
19
+ <NodeComp key={n.id} data={n.data} />
20
+ ) : (
21
+ <div key={n.id}>{n.id}</div>
22
+ );
23
+ })}
24
+ </div>
25
+ );
26
+ },
27
+ useNodesState: initial => {
28
+ const [n, s] = useState(initial || []);
29
+ return [n, s, () => {}];
30
+ },
31
+ useEdgesState: initial => {
32
+ const [e, s] = useState(initial || []);
33
+ return [e, s, () => {}];
34
+ },
35
+ Handle: () => null,
36
+ MarkerType: { ArrowClosed: 'arrowclosed' },
37
+ Position: { Left: 'left', Right: 'right' },
38
+ addEdge: (params, eds) => [...(eds || []), params],
39
+ };
40
+ });
41
+
42
+ jest.mock('reactflow/dist/style.css', () => ({}));
43
+
44
+ jest.mock('dagre', () => {
45
+ function MockGraph() {
46
+ this.setDefaultEdgeLabel = function () {};
47
+ this.setGraph = function () {};
48
+ this.setNode = function () {};
49
+ this.setEdge = function () {};
50
+ this.node = function () {
51
+ return { x: 100, y: 100 };
52
+ };
53
+ }
54
+ return {
55
+ graphlib: { Graph: MockGraph },
56
+ layout: function () {},
57
+ };
58
+ });
59
+
60
+ describe('<NodeDimensionsTab />', () => {
61
+ const mockDjClient = {
62
+ dimensionDag: jest.fn(),
63
+ };
64
+
65
+ const renderWithContext = djNode =>
66
+ render(
67
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
68
+ <MemoryRouter>
69
+ <NodeDimensionsTab djNode={djNode} />
70
+ </MemoryRouter>
71
+ </DJClientContext.Provider>,
72
+ );
73
+
74
+ beforeEach(() => {
75
+ jest.clearAllMocks();
76
+ });
77
+
78
+ it('shows "No dimension links found" when dimensionDag returns empty', async () => {
79
+ const djNode = {
80
+ name: 'default.metric1',
81
+ type: 'metric',
82
+ parents: [],
83
+ dimension_links: [],
84
+ };
85
+ mockDjClient.dimensionDag.mockResolvedValue({
86
+ inbound: [],
87
+ inbound_edges: [],
88
+ outbound: [],
89
+ outbound_edges: [],
90
+ });
91
+
92
+ renderWithContext(djNode);
93
+
94
+ await waitFor(() => {
95
+ expect(
96
+ screen.getByText('No dimension links found for this node.'),
97
+ ).toBeInTheDocument();
98
+ });
99
+ });
100
+
101
+ it('does not fetch when djNode has no name (early return)', () => {
102
+ renderWithContext({ type: 'metric', parents: [], dimension_links: [] });
103
+ expect(mockDjClient.dimensionDag).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it('renders ReactFlow with DimNode components when outbound dimensions exist', async () => {
107
+ const djNode = {
108
+ name: 'default.metric1',
109
+ type: 'metric',
110
+ parents: [],
111
+ dimension_links: [],
112
+ };
113
+ const dimNode = {
114
+ name: 'default.dim1',
115
+ type: 'dimension',
116
+ display_name: 'Default: Dim 1',
117
+ };
118
+
119
+ mockDjClient.dimensionDag.mockResolvedValue({
120
+ inbound: [],
121
+ inbound_edges: [],
122
+ outbound: [dimNode],
123
+ outbound_edges: [{ source: 'default.metric1', target: 'default.dim1' }],
124
+ });
125
+
126
+ renderWithContext(djNode);
127
+
128
+ await waitFor(() => {
129
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
130
+ });
131
+ expect(screen.getByText('dimension')).toBeInTheDocument();
132
+ });
133
+
134
+ it('handles error from dimensionDag gracefully', async () => {
135
+ const consoleSpy = jest
136
+ .spyOn(console, 'error')
137
+ .mockImplementation(() => {});
138
+ const djNode = {
139
+ name: 'default.metric1',
140
+ type: 'metric',
141
+ parents: [],
142
+ dimension_links: [],
143
+ };
144
+ mockDjClient.dimensionDag.mockRejectedValue(new Error('fetch failed'));
145
+
146
+ renderWithContext(djNode);
147
+
148
+ await waitFor(() => {
149
+ expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error));
150
+ });
151
+ await waitFor(() => {
152
+ expect(
153
+ screen.getByText('No dimension links found for this node.'),
154
+ ).toBeInTheDocument();
155
+ });
156
+ consoleSpy.mockRestore();
157
+ });
158
+
159
+ it('covers DimNode isCurrent border style (current node renders correctly)', async () => {
160
+ const djNode = {
161
+ name: 'default.metric1',
162
+ type: 'metric',
163
+ parents: [],
164
+ dimension_links: [],
165
+ };
166
+
167
+ mockDjClient.dimensionDag.mockResolvedValue({
168
+ inbound: [],
169
+ inbound_edges: [],
170
+ outbound: [
171
+ { name: 'default.dim1', type: 'dimension', display_name: 'Dim 1' },
172
+ ],
173
+ outbound_edges: [{ source: 'default.metric1', target: 'default.dim1' }],
174
+ });
175
+
176
+ renderWithContext(djNode);
177
+
178
+ await waitFor(() => {
179
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
180
+ });
181
+ // metric1 is the current node (isCurrent=true)
182
+ expect(screen.getByText('metric')).toBeInTheDocument();
183
+ });
184
+
185
+ it('covers DimNode with no namespace (node name has no dots)', async () => {
186
+ const djNode = {
187
+ name: 'metric1',
188
+ type: 'metric',
189
+ parents: [],
190
+ dimension_links: [],
191
+ };
192
+
193
+ mockDjClient.dimensionDag.mockResolvedValue({
194
+ inbound: [],
195
+ inbound_edges: [],
196
+ outbound: [{ name: 'dim1', type: 'dimension', display_name: 'Dim 1' }],
197
+ outbound_edges: [{ source: 'metric1', target: 'dim1' }],
198
+ });
199
+
200
+ renderWithContext(djNode);
201
+
202
+ await waitFor(() => {
203
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
204
+ });
205
+ });
206
+
207
+ it('covers DimNode with unknown type (fallback color)', async () => {
208
+ const djNode = {
209
+ name: 'default.metric1',
210
+ type: 'metric',
211
+ parents: [],
212
+ dimension_links: [],
213
+ };
214
+
215
+ mockDjClient.dimensionDag.mockResolvedValue({
216
+ inbound: [],
217
+ inbound_edges: [],
218
+ outbound: [
219
+ {
220
+ name: 'default.custom_node',
221
+ type: 'unknown_type',
222
+ display_name: 'Custom Node',
223
+ },
224
+ ],
225
+ outbound_edges: [
226
+ { source: 'default.metric1', target: 'default.custom_node' },
227
+ ],
228
+ });
229
+
230
+ renderWithContext(djNode);
231
+
232
+ await waitFor(() => {
233
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
234
+ });
235
+ expect(screen.getByText('unknown_type')).toBeInTheDocument();
236
+ });
237
+
238
+ it('renders dimension DAG for a dimension node (inbound + outbound)', async () => {
239
+ const djNode = {
240
+ name: 'default.hard_hat',
241
+ type: 'dimension',
242
+ display_name: 'Hard Hat',
243
+ parents: [],
244
+ dimension_links: [],
245
+ };
246
+ const inboundNode = {
247
+ name: 'default.repair_orders',
248
+ display_name: 'Repair Orders',
249
+ type: 'source',
250
+ };
251
+ const outboundNode = {
252
+ name: 'default.us_state',
253
+ display_name: 'US State',
254
+ type: 'dimension',
255
+ };
256
+
257
+ mockDjClient.dimensionDag.mockResolvedValue({
258
+ inbound: [inboundNode],
259
+ inbound_edges: [{ source: inboundNode.name, target: djNode.name }],
260
+ outbound: [outboundNode],
261
+ outbound_edges: [{ source: djNode.name, target: outboundNode.name }],
262
+ });
263
+
264
+ renderWithContext(djNode);
265
+
266
+ await waitFor(() => {
267
+ expect(mockDjClient.dimensionDag).toHaveBeenCalledWith(
268
+ 'default.hard_hat',
269
+ );
270
+ });
271
+ await waitFor(() => {
272
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
273
+ });
274
+ });
275
+
276
+ it('shows "No dimension links found" when dimension DAG returns empty inbound + outbound', async () => {
277
+ const djNode = {
278
+ name: 'default.payment_type',
279
+ type: 'dimension',
280
+ display_name: 'Payment Type',
281
+ parents: [],
282
+ dimension_links: [],
283
+ };
284
+ mockDjClient.dimensionDag.mockResolvedValue({
285
+ inbound: [],
286
+ inbound_edges: [],
287
+ outbound: [],
288
+ outbound_edges: [],
289
+ });
290
+
291
+ renderWithContext(djNode);
292
+
293
+ await waitFor(() => {
294
+ expect(
295
+ screen.getByText('No dimension links found for this node.'),
296
+ ).toBeInTheDocument();
297
+ });
298
+ });
299
+
300
+ it('handles dimensionDag error gracefully', async () => {
301
+ const consoleSpy = jest
302
+ .spyOn(console, 'error')
303
+ .mockImplementation(() => {});
304
+ const djNode = {
305
+ name: 'default.hard_hat',
306
+ type: 'dimension',
307
+ parents: [],
308
+ dimension_links: [],
309
+ };
310
+ mockDjClient.dimensionDag.mockRejectedValue(new Error('network error'));
311
+
312
+ renderWithContext(djNode);
313
+
314
+ await waitFor(() => {
315
+ expect(consoleSpy).toHaveBeenCalledWith(expect.any(Error));
316
+ });
317
+ await waitFor(() => {
318
+ expect(
319
+ screen.getByText('No dimension links found for this node.'),
320
+ ).toBeInTheDocument();
321
+ });
322
+ consoleSpy.mockRestore();
323
+ });
324
+
325
+ it('renders multi-level inbound chain (non-flat graph)', async () => {
326
+ const djNode = {
327
+ name: 'default.us_state',
328
+ type: 'dimension',
329
+ display_name: 'US State',
330
+ parents: [],
331
+ dimension_links: [],
332
+ };
333
+
334
+ mockDjClient.dimensionDag.mockResolvedValue({
335
+ inbound: [
336
+ {
337
+ name: 'default.hard_hat',
338
+ type: 'dimension',
339
+ display_name: 'Hard Hat',
340
+ },
341
+ {
342
+ name: 'default.repair_order',
343
+ type: 'source',
344
+ display_name: 'Repair Order',
345
+ },
346
+ ],
347
+ inbound_edges: [
348
+ { source: 'default.hard_hat', target: 'default.us_state' },
349
+ { source: 'default.repair_order', target: 'default.hard_hat' },
350
+ ],
351
+ outbound: [],
352
+ outbound_edges: [],
353
+ });
354
+
355
+ renderWithContext(djNode);
356
+
357
+ await waitFor(() => {
358
+ expect(screen.getByTestId('reactflow')).toBeInTheDocument();
359
+ });
360
+ expect(screen.getAllByText('dimension').length).toBeGreaterThan(0);
361
+ });
362
+ });
@@ -14,6 +14,23 @@ jest.mock('cronstrue', () => ({
14
14
  // Mock CSS imports
15
15
  jest.mock('../../../../styles/preaggregations.css', () => ({}));
16
16
 
17
+ // Mock recharts for NodeDataFlowTab (Sankey doesn't work in jsdom)
18
+ jest.mock('recharts', () => ({
19
+ Sankey: () => <div data-testid="sankey-chart" />,
20
+ Tooltip: () => null,
21
+ }));
22
+
23
+ // ResizeObserver is not defined in jsdom
24
+ global.ResizeObserver = function (callback) {
25
+ return { observe: () => {}, disconnect: () => {}, unobserve: () => {} };
26
+ };
27
+
28
+ // jsdom doesn't implement canvas — stub getContext so text-measurement code doesn't crash
29
+ HTMLCanvasElement.prototype.getContext = () => ({
30
+ font: '',
31
+ measureText: () => ({ width: 0 }),
32
+ });
33
+
17
34
  describe('<NodePage />', () => {
18
35
  const domTestingLib = require('@testing-library/dom');
19
36
  const { queryHelpers } = domTestingLib;
@@ -71,6 +88,12 @@ describe('<NodePage />', () => {
71
88
  .mockResolvedValue({ status: 200 }),
72
89
  listPreaggs: jest.fn().mockResolvedValue({ items: [] }),
73
90
  deactivatePreaggWorkflow: jest.fn().mockResolvedValue({ status: 200 }),
91
+ namespaceSources: jest.fn().mockResolvedValue(null),
92
+ listDeployments: jest.fn().mockResolvedValue([]),
93
+ getNamespaceGitConfig: jest.fn().mockResolvedValue(null),
94
+ upstreamsGQL: jest.fn().mockResolvedValue([]),
95
+ downstreamsGQL: jest.fn().mockResolvedValue([]),
96
+ findCubesWithMetrics: jest.fn().mockResolvedValue([]),
74
97
  },
75
98
  };
76
99
  };
@@ -729,15 +752,17 @@ describe('<NodePage />', () => {
729
752
  </DJClientContext.Provider>
730
753
  );
731
754
  render(
732
- <MemoryRouter initialEntries={['/nodes/default.num_repair_orders/graph']}>
755
+ <MemoryRouter
756
+ initialEntries={['/nodes/default.num_repair_orders/data-flow']}
757
+ >
733
758
  <Routes>
734
759
  <Route path="nodes/:name/:tab" element={element} />
735
760
  </Routes>
736
761
  </MemoryRouter>,
737
762
  );
738
763
  await waitFor(() => {
739
- fireEvent.click(screen.getByRole('button', { name: 'Graph' }));
740
- expect(djClient.DataJunctionAPI.node_dag).toHaveBeenCalledWith(
764
+ fireEvent.click(screen.getByRole('button', { name: 'Data Flow' }));
765
+ expect(djClient.DataJunctionAPI.upstreamsGQL).toHaveBeenCalledWith(
741
766
  mocks.mockMetricNode.name,
742
767
  );
743
768
  });
@@ -164,10 +164,10 @@ describe('<NodesWithDimension />', () => {
164
164
  );
165
165
  for (const node of mockNodesWithDimension) {
166
166
  // renders nodes based on nodesWithDimension data
167
- expect(screen.getByText(node.display_name)).toBeInTheDocument();
167
+ expect(screen.getByText(node.name)).toBeInTheDocument();
168
168
 
169
169
  // renders links to the correct URLs
170
- const link = screen.getByText(node.display_name).closest('a');
170
+ const link = screen.getByText(node.name).closest('a');
171
171
  expect(link).toHaveAttribute('href', `/nodes/${node.name}`);
172
172
  }
173
173
  });
@@ -5,7 +5,8 @@ import Tab from '../../components/Tab';
5
5
  import NamespaceHeader from '../../components/NamespaceHeader';
6
6
  import NodeInfoTab from './NodeInfoTab';
7
7
  import NodeColumnTab from './NodeColumnTab';
8
- import NodeGraphTab from './NodeGraphTab';
8
+ import NodeDataFlowTab from './NodeDataFlowTab';
9
+ import NodeDimensionsTab from './NodeDimensionsTab';
9
10
  import NodeHistory from './NodeHistory';
10
11
  import NotebookDownload from './NotebookDownload';
11
12
  import DJClientContext from '../../providers/djclient';
@@ -83,10 +84,15 @@ export function NodePage() {
83
84
  display: node?.type !== 'metric',
84
85
  },
85
86
  {
86
- id: 'graph',
87
- name: 'Graph',
87
+ id: 'data-flow',
88
+ name: 'Data Flow',
88
89
  display: true,
89
90
  },
91
+ {
92
+ id: 'dimensions',
93
+ name: 'Dimensions',
94
+ display: node?.type !== 'source',
95
+ },
90
96
  {
91
97
  id: 'history',
92
98
  name: 'History',
@@ -128,8 +134,11 @@ export function NodePage() {
128
134
  case 'columns':
129
135
  tabToDisplay = <NodeColumnTab node={node} djClient={djClient} />;
130
136
  break;
131
- case 'graph':
132
- tabToDisplay = <NodeGraphTab djNode={node} djClient={djClient} />;
137
+ case 'data-flow':
138
+ tabToDisplay = <NodeDataFlowTab djNode={node} />;
139
+ break;
140
+ case 'dimensions':
141
+ tabToDisplay = <NodeDimensionsTab djNode={node} />;
133
142
  break;
134
143
  case 'history':
135
144
  tabToDisplay = <NodeHistory node={node} djClient={djClient} />;
@@ -261,9 +270,7 @@ export function NodePage() {
261
270
  {node?.version}
262
271
  </span>
263
272
  </div>
264
- <div className="align-items-center row">
265
- {tabsList(node).map(buildTabs)}
266
- </div>
273
+ <div className="dj-tabs-bar">{tabsList(node).map(buildTabs)}</div>
267
274
  {tabToDisplay}
268
275
  </div>
269
276
  ) : node?.message !== undefined ? (
@@ -865,6 +865,11 @@ export const DataJunctionAPI = {
865
865
  upstreamNodes(nodeNames: $nodeNames) {
866
866
  name
867
867
  type
868
+ current {
869
+ parents {
870
+ name
871
+ }
872
+ }
868
873
  }
869
874
  }
870
875
  `;
@@ -886,6 +891,11 @@ export const DataJunctionAPI = {
886
891
  downstreamNodes(nodeNames: $nodeNames) {
887
892
  name
888
893
  type
894
+ current {
895
+ parents {
896
+ name
897
+ }
898
+ }
889
899
  }
890
900
  }
891
901
  `;
@@ -900,6 +910,39 @@ export const DataJunctionAPI = {
900
910
  return results.data?.downstreamNodes || [];
901
911
  },
902
912
 
913
+ // Batch-fetch cubes by name, returning each cube's name and its metric node names.
914
+ // Used to build the Sankey data flow graph without N individual cube fetches.
915
+ findCubesWithMetrics: async function (cubeNames) {
916
+ if (!cubeNames || cubeNames.length === 0) return [];
917
+ const query = `
918
+ query FindCubesWithMetrics($names: [String!]) {
919
+ findNodes(names: $names, nodeTypes: [CUBE]) {
920
+ name
921
+ current {
922
+ displayName
923
+ cubeMetrics {
924
+ name
925
+ }
926
+ }
927
+ }
928
+ }
929
+ `;
930
+ const results = await (
931
+ await fetch(DJ_GQL, {
932
+ method: 'POST',
933
+ headers: { 'Content-Type': 'application/json' },
934
+ credentials: 'include',
935
+ body: JSON.stringify({ query, variables: { names: cubeNames } }),
936
+ })
937
+ ).json();
938
+ return (results.data?.findNodes || []).map(n => ({
939
+ name: n.name,
940
+ display_name: n.current?.displayName || n.name,
941
+ type: 'cube',
942
+ parents: (n.current?.cubeMetrics || []).map(m => ({ name: m.name })),
943
+ }));
944
+ },
945
+
903
946
  node_dag: async function (name) {
904
947
  return await (
905
948
  await fetch(`${DJ_URL}/nodes/${name}/dag/`, {
@@ -1097,9 +1140,10 @@ export const DataJunctionAPI = {
1097
1140
  ).json();
1098
1141
  },
1099
1142
 
1100
- nodesWithDimension: async function (name) {
1143
+ nodesWithDimension: async function (name, nodeType = null) {
1144
+ const params = nodeType ? `?node_type=${nodeType}` : '';
1101
1145
  return await (
1102
- await fetch(`${DJ_URL}/dimensions/${name}/nodes/`, {
1146
+ await fetch(`${DJ_URL}/dimensions/${name}/nodes/${params}`, {
1103
1147
  credentials: 'include',
1104
1148
  })
1105
1149
  ).json();
@@ -1444,6 +1488,13 @@ export const DataJunctionAPI = {
1444
1488
  })
1445
1489
  ).json();
1446
1490
  },
1491
+ dimensionDag: async function (nodeName) {
1492
+ return await (
1493
+ await fetch(`${DJ_URL}/nodes/${nodeName}/dimension-dag/`, {
1494
+ credentials: 'include',
1495
+ })
1496
+ ).json();
1497
+ },
1447
1498
  linkDimension: async function (nodeName, columnName, dimensionName) {
1448
1499
  const response = await fetch(
1449
1500
  `${DJ_URL}/nodes/${nodeName}/columns/${columnName}?dimension=${dimensionName}`,
@@ -1928,11 +1979,18 @@ export const DataJunctionAPI = {
1928
1979
 
1929
1980
  // ===== My Workspace GraphQL Queries =====
1930
1981
 
1931
- getWorkspaceRecentlyEdited: async function (username, limit = 10) {
1982
+ getWorkspaceRecentlyEdited: async function (
1983
+ username,
1984
+ limit = 10,
1985
+ nodeType = null,
1986
+ ) {
1932
1987
  // Nodes the user has edited, ordered by last updated (excluding source nodes)
1933
1988
  const query = `
1934
1989
  query RecentlyEdited($editedBy: String!, $limit: Int!, $nodeTypes: [NodeType!]) {
1935
1990
  findNodesPaginated(editedBy: $editedBy, limit: $limit, nodeTypes: $nodeTypes, orderBy: UPDATED_AT, ascending: false) {
1991
+ pageInfo {
1992
+ hasNextPage
1993
+ }
1936
1994
  edges {
1937
1995
  node {
1938
1996
  name
@@ -1957,6 +2015,7 @@ export const DataJunctionAPI = {
1957
2015
  }
1958
2016
  }
1959
2017
  `;
2018
+ const allTypes = ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'];
1960
2019
  return await (
1961
2020
  await fetch(DJ_GQL, {
1962
2021
  method: 'POST',
@@ -1967,18 +2026,25 @@ export const DataJunctionAPI = {
1967
2026
  variables: {
1968
2027
  editedBy: username,
1969
2028
  limit,
1970
- nodeTypes: ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'],
2029
+ nodeTypes: nodeType ? [nodeType] : allTypes,
1971
2030
  },
1972
2031
  }),
1973
2032
  })
1974
2033
  ).json();
1975
2034
  },
1976
2035
 
1977
- getWorkspaceOwnedNodes: async function (username, limit = 10) {
2036
+ getWorkspaceOwnedNodes: async function (
2037
+ username,
2038
+ limit = 10,
2039
+ nodeType = null,
2040
+ ) {
1978
2041
  // Owned nodes ordered by UPDATED_AT (excluding source nodes)
1979
2042
  const query = `
1980
2043
  query OwnedNodes($ownedBy: String!, $limit: Int!, $nodeTypes: [NodeType!]) {
1981
2044
  findNodesPaginated(ownedBy: $ownedBy, limit: $limit, nodeTypes: $nodeTypes, orderBy: UPDATED_AT, ascending: false) {
2045
+ pageInfo {
2046
+ hasNextPage
2047
+ }
1982
2048
  edges {
1983
2049
  node {
1984
2050
  name
@@ -2003,6 +2069,7 @@ export const DataJunctionAPI = {
2003
2069
  }
2004
2070
  }
2005
2071
  `;
2072
+ const allTypes = ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'];
2006
2073
  return await (
2007
2074
  await fetch(DJ_GQL, {
2008
2075
  method: 'POST',
@@ -2013,7 +2080,7 @@ export const DataJunctionAPI = {
2013
2080
  variables: {
2014
2081
  ownedBy: username,
2015
2082
  limit,
2016
- nodeTypes: ['TRANSFORM', 'METRIC', 'DIMENSION', 'CUBE'],
2083
+ nodeTypes: nodeType ? [nodeType] : allTypes,
2017
2084
  },
2018
2085
  }),
2019
2086
  })