datajunction-ui 0.0.1-rc.8 → 0.0.2

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 (205) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/dj-logo.svg +10 -0
  5. package/package.json +53 -14
  6. package/public/favicon.ico +0 -0
  7. package/public/index.html +1 -1
  8. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  9. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  10. package/src/app/components/AddNodeDropdown.jsx +44 -0
  11. package/src/app/components/ListGroupItem.jsx +9 -1
  12. package/src/app/components/NamespaceHeader.jsx +4 -13
  13. package/src/app/components/NodeListActions.jsx +69 -0
  14. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  15. package/src/app/components/QueryInfo.jsx +172 -0
  16. package/src/app/components/Search.jsx +94 -0
  17. package/src/app/components/Tab.jsx +8 -1
  18. package/src/app/components/ToggleSwitch.jsx +20 -0
  19. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  20. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  21. package/src/app/components/__tests__/Search.test.jsx +63 -0
  22. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  23. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  24. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  25. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  26. package/src/app/components/djgraph/Collapse.jsx +47 -0
  27. package/src/app/components/djgraph/DJNode.jsx +61 -83
  28. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  29. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  30. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  31. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  32. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  33. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  34. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  35. package/src/app/components/forms/Action.jsx +8 -0
  36. package/src/app/components/forms/NodeNameField.jsx +64 -0
  37. package/src/app/components/search.css +17 -0
  38. package/src/app/constants.js +2 -0
  39. package/src/app/icons/AddItemIcon.jsx +16 -0
  40. package/src/app/icons/AlertIcon.jsx +33 -0
  41. package/src/app/icons/CollapsedIcon.jsx +15 -0
  42. package/src/app/icons/CommitIcon.jsx +45 -0
  43. package/src/app/icons/DJLogo.jsx +36 -0
  44. package/src/app/icons/DeleteIcon.jsx +21 -0
  45. package/src/app/icons/DiffIcon.jsx +63 -0
  46. package/src/app/icons/EditIcon.jsx +18 -0
  47. package/src/app/icons/ExpandedIcon.jsx +15 -0
  48. package/src/app/icons/EyeIcon.jsx +20 -0
  49. package/src/app/icons/FilterIcon.jsx +7 -0
  50. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  51. package/src/app/icons/InvalidIcon.jsx +16 -0
  52. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  53. package/src/app/icons/LoadingIcon.jsx +14 -0
  54. package/src/app/icons/NodeIcon.jsx +49 -0
  55. package/src/app/icons/PythonIcon.jsx +14 -0
  56. package/src/app/icons/TableIcon.jsx +14 -0
  57. package/src/app/icons/ValidIcon.jsx +16 -0
  58. package/src/app/index.tsx +118 -37
  59. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  60. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  61. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  62. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  63. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +51 -0
  64. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  65. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  66. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  67. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  68. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  69. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  70. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  71. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  72. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  73. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  74. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  75. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +109 -0
  76. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +287 -0
  77. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  78. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  79. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  80. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  81. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  82. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  83. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  84. package/src/app/pages/AddEditNodePage/index.jsx +506 -0
  85. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  86. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  87. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  88. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  89. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  90. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  91. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  92. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  93. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  94. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  95. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  96. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  97. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  98. package/src/app/pages/LoginPage/index.jsx +17 -0
  99. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  100. package/src/app/pages/NamespacePage/Explorer.jsx +61 -0
  101. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  102. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  103. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  104. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  105. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -0
  106. package/src/app/pages/NamespacePage/index.jsx +319 -42
  107. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  108. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  109. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  110. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  111. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  112. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  113. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  114. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  115. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  116. package/src/app/pages/NodePage/NodeColumnTab.jsx +256 -30
  117. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  118. package/src/app/pages/NodePage/NodeGraphTab.jsx +123 -150
  119. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  120. package/src/app/pages/NodePage/NodeInfoTab.jsx +325 -49
  121. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  122. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  123. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  124. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  125. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  126. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  127. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  128. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  129. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  130. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  131. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  132. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  133. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  134. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  135. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  136. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  137. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +161 -0
  138. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  139. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  140. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  141. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  142. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  143. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +872 -0
  144. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  145. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  146. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  147. package/src/app/pages/NodePage/index.jsx +190 -44
  148. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  149. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  150. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  151. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  152. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  153. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  154. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  155. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  156. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  157. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  158. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  159. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  160. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  161. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  162. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  163. package/src/app/pages/OverviewPage/index.jsx +22 -0
  164. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  165. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
  166. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  167. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  168. package/src/app/pages/Root/__tests__/index.test.jsx +79 -0
  169. package/src/app/pages/Root/index.tsx +84 -6
  170. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  171. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  172. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  173. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  174. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  175. package/src/app/pages/TagPage/index.jsx +79 -0
  176. package/src/app/services/DJService.js +1314 -21
  177. package/src/app/services/__tests__/DJService.test.jsx +1559 -0
  178. package/src/index.tsx +1 -0
  179. package/src/mocks/mockNodes.jsx +1474 -0
  180. package/src/setupTests.ts +31 -1
  181. package/src/styles/dag.css +117 -5
  182. package/src/styles/index.css +1027 -30
  183. package/src/styles/loading.css +34 -0
  184. package/src/styles/login.css +81 -0
  185. package/src/styles/node-creation.scss +276 -0
  186. package/src/styles/node-list.css +4 -0
  187. package/src/styles/overview.css +72 -0
  188. package/src/styles/sorted-table.css +15 -0
  189. package/src/styles/styles.scss +44 -0
  190. package/src/styles/styles.scss.d.ts +9 -0
  191. package/src/utils/form.jsx +23 -0
  192. package/webpack.config.js +16 -6
  193. package/.babelrc +0 -4
  194. package/.env.local +0 -4
  195. package/.env.production +0 -1
  196. package/.github/pull_request_template.md +0 -11
  197. package/.github/workflows/ci.yml +0 -33
  198. package/.vscode/extensions.json +0 -7
  199. package/.vscode/launch.json +0 -15
  200. package/.vscode/settings.json +0 -25
  201. package/Dockerfile +0 -7
  202. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  203. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  204. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  205. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,297 @@
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
3
+ import DJClientContext from '../../../providers/djclient';
4
+ import { NamespacePage } from '../index';
5
+ import React from 'react';
6
+ import userEvent from '@testing-library/user-event';
7
+
8
+ const mockDjClient = {
9
+ namespaces: jest.fn(),
10
+ namespace: jest.fn(),
11
+ listNodesForLanding: jest.fn(),
12
+ addNamespace: jest.fn(),
13
+ whoami: jest.fn(),
14
+ users: jest.fn(),
15
+ listTags: jest.fn(),
16
+ };
17
+
18
+ describe('NamespacePage', () => {
19
+ const original = window.location;
20
+
21
+ const reloadFn = () => {
22
+ window.location.reload();
23
+ };
24
+
25
+ beforeAll(() => {
26
+ Object.defineProperty(window, 'location', {
27
+ configurable: true,
28
+ value: { reload: jest.fn() },
29
+ });
30
+ });
31
+
32
+ afterAll(() => {
33
+ Object.defineProperty(window, 'location', {
34
+ configurable: true,
35
+ value: original,
36
+ });
37
+ });
38
+
39
+ beforeEach(() => {
40
+ fetch.resetMocks();
41
+ mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
42
+ mockDjClient.users.mockResolvedValue([
43
+ { username: 'dj' },
44
+ { username: 'user1' },
45
+ ]);
46
+ mockDjClient.listTags.mockResolvedValue([
47
+ { name: 'tag1' },
48
+ { name: 'tag2' },
49
+ ]);
50
+ mockDjClient.namespaces.mockResolvedValue([
51
+ {
52
+ namespace: 'common.one',
53
+ num_nodes: 3,
54
+ },
55
+ {
56
+ namespace: 'common.one.a',
57
+ num_nodes: 6,
58
+ },
59
+ {
60
+ namespace: 'common.one.b',
61
+ num_nodes: 17,
62
+ },
63
+ {
64
+ namespace: 'common.one.c',
65
+ num_nodes: 64,
66
+ },
67
+ {
68
+ namespace: 'default',
69
+ num_nodes: 41,
70
+ },
71
+ {
72
+ namespace: 'default.fruits',
73
+ num_nodes: 1,
74
+ },
75
+ {
76
+ namespace: 'default.fruits.citrus.lemons',
77
+ num_nodes: 1,
78
+ },
79
+ {
80
+ namespace: 'default.vegetables',
81
+ num_nodes: 2,
82
+ },
83
+ ]);
84
+ mockDjClient.namespace.mockResolvedValue([
85
+ {
86
+ name: 'testNode',
87
+ display_name: 'Test Node',
88
+ type: 'transform',
89
+ mode: 'active',
90
+ updated_at: new Date(),
91
+ tags: [{ name: 'tag1' }],
92
+ edited_by: ['dj'],
93
+ },
94
+ ]);
95
+ mockDjClient.listNodesForLanding.mockResolvedValue({
96
+ data: {
97
+ findNodesPaginated: {
98
+ pageInfo: {
99
+ hasNextPage: true,
100
+ endCursor:
101
+ 'eyJjcmVhdGVkX2F0IjogIjIwMjQtMDQtMTZUMjM6MjI6MjIuNDQxNjg2KzAwOjAwIiwgImlkIjogNjE0fQ==',
102
+ hasPrevPage: true,
103
+ startCursor:
104
+ 'eyJjcmVhdGVkX2F0IjogIjIwMjQtMTAtMTZUMTY6MDM6MTcuMDgzMjY3KzAwOjAwIiwgImlkIjogMjQwOX0=',
105
+ },
106
+ edges: [
107
+ {
108
+ node: {
109
+ name: 'default.test_node',
110
+ type: 'DIMENSION',
111
+ currentVersion: 'v4.0',
112
+ tags: [],
113
+ editedBy: ['dj'],
114
+ current: {
115
+ displayName: 'Test Node',
116
+ status: 'VALID',
117
+ updatedAt: '2024-10-18T15:15:33.532949+00:00',
118
+ },
119
+ createdBy: {
120
+ username: 'dj',
121
+ },
122
+ },
123
+ },
124
+ ],
125
+ },
126
+ },
127
+ });
128
+ });
129
+
130
+ afterEach(() => {
131
+ jest.clearAllMocks();
132
+ });
133
+
134
+ it('displays namespaces and renders nodes', async () => {
135
+ reloadFn();
136
+ const element = (
137
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
138
+ <NamespacePage />
139
+ </DJClientContext.Provider>
140
+ );
141
+ render(
142
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
143
+ <Routes>
144
+ <Route path="namespaces/:namespace" element={element} />
145
+ </Routes>
146
+ </MemoryRouter>,
147
+ );
148
+
149
+ // Wait for initial nodes to load
150
+ await waitFor(() => {
151
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalled();
152
+ expect(screen.getByText('Namespaces')).toBeInTheDocument();
153
+ });
154
+
155
+ // Check that it displays namespaces
156
+ expect(screen.getByText('common')).toBeInTheDocument();
157
+ expect(screen.getByText('one')).toBeInTheDocument();
158
+ expect(screen.getByText('fruits')).toBeInTheDocument();
159
+ expect(screen.getByText('vegetables')).toBeInTheDocument();
160
+
161
+ // Check that it renders nodes
162
+ expect(screen.getByText('Test Node')).toBeInTheDocument();
163
+
164
+ // --- Sorting ---
165
+
166
+ // sort by 'name'
167
+ fireEvent.click(screen.getByText('name'));
168
+ await waitFor(() => {
169
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(2);
170
+ });
171
+
172
+ // flip direction
173
+ fireEvent.click(screen.getByText('name'));
174
+ await waitFor(() => {
175
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(3);
176
+ });
177
+
178
+ // sort by 'displayName'
179
+ fireEvent.click(screen.getByText('display Name'));
180
+ await waitFor(() => {
181
+ expect(mockDjClient.listNodesForLanding).toHaveBeenCalledTimes(4);
182
+ });
183
+
184
+ // --- Filters ---
185
+
186
+ // Node type
187
+ const selectNodeType = screen.getAllByTestId('select-node-type')[0];
188
+ fireEvent.keyDown(selectNodeType.firstChild, { key: 'ArrowDown' });
189
+ fireEvent.click(screen.getByText('Source'));
190
+
191
+ // Tag filter
192
+ const selectTag = screen.getAllByTestId('select-tag')[0];
193
+ fireEvent.keyDown(selectTag.firstChild, { key: 'ArrowDown' });
194
+
195
+ // User filter
196
+ const selectUser = screen.getAllByTestId('select-user')[0];
197
+ fireEvent.keyDown(selectUser.firstChild, { key: 'ArrowDown' });
198
+
199
+ // --- Expand/Collapse Namespace ---
200
+ fireEvent.click(screen.getByText('common'));
201
+ fireEvent.click(screen.getByText('common'));
202
+ });
203
+
204
+ it('can add new namespace via add namespace popover', async () => {
205
+ mockDjClient.addNamespace.mockReturnValue({
206
+ status: 201,
207
+ json: {},
208
+ });
209
+ const element = (
210
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
211
+ <NamespacePage />
212
+ </DJClientContext.Provider>
213
+ );
214
+ render(
215
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
216
+ <Routes>
217
+ <Route path="namespaces/:namespace" element={element} />
218
+ </Routes>
219
+ </MemoryRouter>,
220
+ );
221
+
222
+ // Find the button to toggle the add namespace popover
223
+ const addNamespaceToggle = screen.getByRole('button', {
224
+ name: 'AddNamespaceTogglePopover',
225
+ });
226
+ expect(addNamespaceToggle).toBeInTheDocument();
227
+
228
+ // Click the toggle and verify that the popover displays
229
+ fireEvent.click(addNamespaceToggle);
230
+ const addNamespacePopover = screen.getByRole('dialog', {
231
+ name: 'AddNamespacePopover',
232
+ });
233
+ expect(addNamespacePopover).toBeInTheDocument();
234
+
235
+ // Type in the new namespace
236
+ await userEvent.type(
237
+ screen.getByLabelText('Namespace'),
238
+ 'some.random.namespace',
239
+ );
240
+
241
+ // Save
242
+ const saveNamespace = screen.getByRole('button', {
243
+ name: 'SaveNamespace',
244
+ });
245
+ await waitFor(() => {
246
+ fireEvent.click(saveNamespace);
247
+ });
248
+ expect(mockDjClient.addNamespace).toHaveBeenCalled();
249
+ expect(mockDjClient.addNamespace).toHaveBeenCalledWith(
250
+ 'test.namespace.some.random.namespace',
251
+ );
252
+ expect(screen.getByText('Saved')).toBeInTheDocument();
253
+ expect(window.location.reload).toHaveBeenCalled();
254
+ });
255
+
256
+ it('can fail to add namespace', async () => {
257
+ mockDjClient.addNamespace.mockReturnValue({
258
+ status: 500,
259
+ json: { message: 'you failed' },
260
+ });
261
+ const element = (
262
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
263
+ <NamespacePage />
264
+ </DJClientContext.Provider>
265
+ );
266
+ render(
267
+ <MemoryRouter initialEntries={['/namespaces/test.namespace']}>
268
+ <Routes>
269
+ <Route path="namespaces/:namespace" element={element} />
270
+ </Routes>
271
+ </MemoryRouter>,
272
+ );
273
+
274
+ // Open the add namespace popover
275
+ const addNamespaceToggle = screen.getByRole('button', {
276
+ name: 'AddNamespaceTogglePopover',
277
+ });
278
+ fireEvent.click(addNamespaceToggle);
279
+
280
+ // Type in the new namespace
281
+ await userEvent.type(
282
+ screen.getByLabelText('Namespace'),
283
+ 'some.random.namespace',
284
+ );
285
+
286
+ // Save
287
+ const saveNamespace = screen.getByRole('button', {
288
+ name: 'SaveNamespace',
289
+ });
290
+ await waitFor(() => {
291
+ fireEvent.click(saveNamespace);
292
+ });
293
+
294
+ // Should display failure alert
295
+ expect(screen.getByText('you failed')).toBeInTheDocument();
296
+ });
297
+ });
@@ -1,83 +1,360 @@
1
1
  import * as React from 'react';
2
2
  import { useParams } from 'react-router-dom';
3
3
  import { useContext, useEffect, useState } from 'react';
4
- import NamespaceHeader from '../../components/NamespaceHeader';
5
4
  import NodeStatus from '../NodePage/NodeStatus';
6
5
  import DJClientContext from '../../providers/djclient';
6
+ import Explorer from '../NamespacePage/Explorer';
7
+ import AddNodeDropdown from '../../components/AddNodeDropdown';
8
+ import NodeListActions from '../../components/NodeListActions';
9
+ import AddNamespacePopover from './AddNamespacePopover';
10
+ import FilterIcon from '../../icons/FilterIcon';
11
+ import LoadingIcon from '../../icons/LoadingIcon';
12
+ import UserSelect from './UserSelect';
13
+ import NodeTypeSelect from './NodeTypeSelect';
14
+ import TagSelect from './TagSelect';
15
+
16
+ import 'styles/node-list.css';
17
+ import 'styles/sorted-table.css';
7
18
 
8
19
  export function NamespacePage() {
20
+ const ASC = 'ascending';
21
+ const DESC = 'descending';
22
+
23
+ const fields = ['name', 'displayName', 'type', 'status', 'updatedAt'];
24
+
9
25
  const djClient = useContext(DJClientContext).DataJunctionAPI;
10
- const { namespace } = useParams();
26
+ var { namespace } = useParams();
11
27
 
12
28
  const [state, setState] = useState({
13
- namespace: namespace,
29
+ namespace: namespace ? namespace : '',
14
30
  nodes: [],
15
31
  });
32
+ const [retrieved, setRetrieved] = useState(false);
33
+ const [currentUser, setCurrentUser] = useState(null);
34
+
35
+ const [filters, setFilters] = useState({
36
+ tags: [],
37
+ node_type: '',
38
+ edited_by: '',
39
+ });
40
+
41
+ const [namespaceHierarchy, setNamespaceHierarchy] = useState([]);
42
+
43
+ const [sortConfig, setSortConfig] = useState({
44
+ key: 'updatedAt',
45
+ direction: DESC,
46
+ });
47
+
48
+ const [before, setBefore] = useState(null);
49
+ const [after, setAfter] = useState(null);
50
+ const [prevCursor, setPrevCursor] = useState(true);
51
+ const [nextCursor, setNextCursor] = useState(true);
52
+
53
+ const [hasNextPage, setHasNextPage] = useState(true);
54
+ const [hasPrevPage, setHasPrevPage] = useState(true);
55
+
56
+ const requestSort = key => {
57
+ let direction = ASC;
58
+ if (sortConfig.key === key && sortConfig.direction === ASC) {
59
+ direction = DESC;
60
+ }
61
+ if (sortConfig.key !== key || sortConfig.direction !== direction) {
62
+ setSortConfig({ key, direction });
63
+ }
64
+ };
65
+
66
+ const getClassNamesFor = name => {
67
+ if (sortConfig.key === name) {
68
+ return sortConfig.direction;
69
+ }
70
+ return undefined;
71
+ };
72
+
73
+ const createNamespaceHierarchy = namespaceList => {
74
+ const hierarchy = [];
75
+
76
+ for (const item of namespaceList) {
77
+ const namespaces = item.namespace.split('.');
78
+ let currentLevel = hierarchy;
79
+
80
+ let path = '';
81
+ for (const ns of namespaces) {
82
+ path += ns;
83
+
84
+ let existingNamespace = currentLevel.find(el => el.namespace === ns);
85
+ if (!existingNamespace) {
86
+ existingNamespace = {
87
+ namespace: ns,
88
+ children: [],
89
+ path: path,
90
+ };
91
+ currentLevel.push(existingNamespace);
92
+ }
93
+
94
+ currentLevel = existingNamespace.children;
95
+ path += '.';
96
+ }
97
+ }
98
+ return hierarchy;
99
+ };
16
100
 
17
101
  useEffect(() => {
18
102
  const fetchData = async () => {
19
- const djNodes = await djClient.namespace(namespace);
20
- const nodes = djNodes.map(node => {
21
- return djClient.node(node);
22
- });
23
- const foundNodes = await Promise.all(nodes);
103
+ const namespaces = await djClient.namespaces();
104
+ const hierarchy = createNamespaceHierarchy(namespaces);
105
+ setNamespaceHierarchy(hierarchy);
106
+ const currentUser = await djClient.whoami();
107
+ // setFilters({...filters, edited_by: currentUser?.username});
108
+ setCurrentUser(currentUser);
109
+ };
110
+ fetchData().catch(console.error);
111
+ }, [djClient, djClient.namespaces]);
112
+
113
+ useEffect(() => {
114
+ const fetchData = async () => {
115
+ setRetrieved(false);
116
+ const nodes = await djClient.listNodesForLanding(
117
+ namespace,
118
+ filters.node_type ? [filters.node_type.toUpperCase()] : [],
119
+ filters.tags,
120
+ filters.edited_by,
121
+ before,
122
+ after,
123
+ 50,
124
+ sortConfig,
125
+ );
126
+
24
127
  setState({
25
128
  namespace: namespace,
26
- nodes: foundNodes,
129
+ nodes: nodes.data
130
+ ? nodes.data.findNodesPaginated.edges.map(n => n.node)
131
+ : [],
27
132
  });
133
+ if (nodes.data) {
134
+ setPrevCursor(
135
+ nodes.data ? nodes.data.findNodesPaginated.pageInfo.startCursor : '',
136
+ );
137
+ setNextCursor(
138
+ nodes.data ? nodes.data.findNodesPaginated.pageInfo.endCursor : '',
139
+ );
140
+ setHasPrevPage(
141
+ nodes.data
142
+ ? nodes.data.findNodesPaginated.pageInfo.hasPrevPage
143
+ : false,
144
+ );
145
+ setHasNextPage(
146
+ nodes.data
147
+ ? nodes.data.findNodesPaginated.pageInfo.hasNextPage
148
+ : false,
149
+ );
150
+ }
151
+ setRetrieved(true);
28
152
  };
29
153
  fetchData().catch(console.error);
30
- }, [djClient, namespace]);
154
+ }, [djClient, filters, before, after, sortConfig.key, sortConfig.direction]);
31
155
 
32
- const nodesList = state.nodes.map(node => (
156
+ const loadNext = () => {
157
+ if (nextCursor) {
158
+ setAfter(nextCursor);
159
+ setBefore(null);
160
+ }
161
+ };
162
+ const loadPrev = () => {
163
+ if (prevCursor) {
164
+ setAfter(null);
165
+ setBefore(prevCursor);
166
+ }
167
+ };
168
+
169
+ const nodesList = retrieved ? (
170
+ state.nodes.length > 0 ? (
171
+ state.nodes.map(node => (
172
+ <tr key={node.name}>
173
+ <td>
174
+ <a href={'/nodes/' + node.name} className="link-table">
175
+ {node.name}
176
+ </a>
177
+ <span
178
+ className="rounded-pill badge bg-secondary-soft"
179
+ style={{ marginLeft: '0.5rem' }}
180
+ >
181
+ {node.currentVersion}
182
+ </span>
183
+ </td>
184
+ <td>
185
+ <a href={'/nodes/' + node.name} className="link-table">
186
+ {node.type !== 'source' ? node.current.displayName : ''}
187
+ </a>
188
+ </td>
189
+ <td>
190
+ <span
191
+ className={
192
+ 'node_type__' + node.type.toLowerCase() + ' badge node_type'
193
+ }
194
+ >
195
+ {node.type}
196
+ </span>
197
+ </td>
198
+ <td>
199
+ <NodeStatus node={node} revalidate={false} />
200
+ </td>
201
+ <td>
202
+ <span className="status">
203
+ {new Date(node.current.updatedAt).toLocaleString('en-us')}
204
+ </span>
205
+ </td>
206
+ <td>
207
+ <NodeListActions nodeName={node?.name} />
208
+ </td>
209
+ </tr>
210
+ ))
211
+ ) : (
212
+ <tr>
213
+ <td>
214
+ <span
215
+ style={{
216
+ display: 'block',
217
+ marginTop: '2rem',
218
+ marginLeft: '2rem',
219
+ fontSize: '16px',
220
+ }}
221
+ >
222
+ There are no nodes in{' '}
223
+ <a href={`/namespaces/${namespace}`}>{namespace}</a> with the above
224
+ filters!
225
+ </span>
226
+ </td>
227
+ </tr>
228
+ )
229
+ ) : (
33
230
  <tr>
34
231
  <td>
35
- <a href={'/namespaces/' + node.namespace}>{node.namespace}</a>
36
- </td>
37
- <td>
38
- <a href={'/nodes/' + node.name} className="link-table">
39
- {node.display_name}
40
- </a>
41
- <span
42
- className="rounded-pill badge bg-secondary-soft"
43
- style={{ marginLeft: '0.5rem' }}
44
- >
45
- {node.version}
232
+ <span style={{ display: 'block', marginTop: '2rem' }}>
233
+ <LoadingIcon />
46
234
  </span>
47
235
  </td>
48
- <td>
49
- <span className={'node_type__' + node.type + ' badge node_type'}>
50
- {node.type}
51
- </span>
52
- </td>
53
- <td>
54
- <NodeStatus node={node} />
55
- </td>
56
- <td>
57
- <span className="status">{node.mode}</span>
58
- </td>
59
236
  </tr>
60
- ));
237
+ );
61
238
 
62
- // @ts-ignore
63
239
  return (
64
240
  <div className="mid">
65
- <NamespaceHeader namespace={namespace} />
66
241
  <div className="card">
67
242
  <div className="card-header">
68
- <h2>Nodes</h2>
243
+ <h2>Explore</h2>
244
+ <div className="menu" style={{ margin: '0 0 20px 0' }}>
245
+ <div
246
+ className="menu-link"
247
+ style={{
248
+ marginTop: '0.7em',
249
+ color: '#777',
250
+ fontFamily: "'Jost'",
251
+ fontSize: '18px',
252
+ marginRight: '10px',
253
+ marginLeft: '15px',
254
+ }}
255
+ >
256
+ <FilterIcon />
257
+ </div>
258
+ <div
259
+ className="menu-link"
260
+ style={{
261
+ marginTop: '0.6em',
262
+ color: '#777',
263
+ fontFamily: "'Jost'",
264
+ fontSize: '18px',
265
+ marginRight: '10px',
266
+ }}
267
+ >
268
+ Filter By
269
+ </div>
270
+ <NodeTypeSelect
271
+ onChange={entry =>
272
+ setFilters({ ...filters, node_type: entry ? entry.value : '' })
273
+ }
274
+ />
275
+ <TagSelect
276
+ onChange={entry =>
277
+ setFilters({
278
+ ...filters,
279
+ tags: entry ? entry.map(tag => tag.value) : [],
280
+ })
281
+ }
282
+ />
283
+ <UserSelect
284
+ onChange={entry =>
285
+ setFilters({ ...filters, edited_by: entry ? entry.value : '' })
286
+ }
287
+ currentUser={currentUser?.username}
288
+ />
289
+ <AddNodeDropdown namespace={namespace} />
290
+ </div>
69
291
  <div className="table-responsive">
292
+ <div className={`sidebar`}>
293
+ <span
294
+ style={{
295
+ textTransform: 'uppercase',
296
+ fontSize: '0.8125rem',
297
+ fontWeight: '600',
298
+ color: '#95aac9',
299
+ padding: '1rem 1rem 1rem 0',
300
+ }}
301
+ >
302
+ Namespaces <AddNamespacePopover namespace={namespace} />
303
+ </span>
304
+ {namespaceHierarchy
305
+ ? namespaceHierarchy.map(child => (
306
+ <Explorer
307
+ item={child}
308
+ current={state.namespace}
309
+ defaultExpand={true}
310
+ key={child.namespace}
311
+ />
312
+ ))
313
+ : null}
314
+ </div>
70
315
  <table className="card-table table">
71
316
  <thead>
72
317
  <tr>
73
- <th>Namespace</th>
74
- <th>Name</th>
75
- <th>Type</th>
76
- <th>Status</th>
77
- <th>Mode</th>
318
+ {fields.map(field => {
319
+ return (
320
+ <th key={field}>
321
+ <button
322
+ type="button"
323
+ onClick={() => requestSort(field)}
324
+ className={'sortable ' + getClassNamesFor(field)}
325
+ >
326
+ {field.replace(/([a-z](?=[A-Z]))/g, '$1 ')}
327
+ </button>
328
+ </th>
329
+ );
330
+ })}
331
+ <th>Actions</th>
78
332
  </tr>
79
333
  </thead>
80
334
  <tbody>{nodesList}</tbody>
335
+ <tfoot>
336
+ <tr>
337
+ <td>
338
+ {retrieved && hasPrevPage ? (
339
+ <a
340
+ onClick={loadPrev}
341
+ className="previous round pagination"
342
+ >
343
+ ← Previous
344
+ </a>
345
+ ) : (
346
+ ''
347
+ )}
348
+ {retrieved && hasNextPage ? (
349
+ <a onClick={loadNext} className="next round pagination">
350
+ Next →
351
+ </a>
352
+ ) : (
353
+ ''
354
+ )}
355
+ </td>
356
+ </tr>
357
+ </tfoot>
81
358
  </table>
82
359
  </div>
83
360
  </div>