datajunction-ui 0.0.1-rc.9 → 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 +119 -148
  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,373 @@
1
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
2
+ import DJClientContext from '../../../providers/djclient';
3
+ import { CubeBuilderPage } from '../index';
4
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
5
+ import React from 'react';
6
+
7
+ const mockDjClient = {
8
+ metrics: jest.fn(),
9
+ commonDimensions: jest.fn(),
10
+ createCube: jest.fn(),
11
+ namespaces: jest.fn(),
12
+ cube: jest.fn(),
13
+ getCubeForEditing: jest.fn(),
14
+ node: jest.fn(),
15
+ listTags: jest.fn(),
16
+ tagsNode: jest.fn(),
17
+ patchCube: jest.fn(),
18
+ users: jest.fn(),
19
+ whoami: jest.fn(),
20
+ };
21
+
22
+ const mockMetrics = [
23
+ 'default.num_repair_orders',
24
+ 'default.avg_repair_price',
25
+ 'default.total_repair_cost',
26
+ ];
27
+
28
+ const mockCube = {
29
+ name: 'default.repair_orders_cube',
30
+ type: 'CUBE',
31
+ owners: [
32
+ {
33
+ username: 'someone@example.com',
34
+ },
35
+ ],
36
+ current: {
37
+ displayName: 'Default: Repair Orders Cube',
38
+ description: 'Repairs cube',
39
+ mode: 'DRAFT',
40
+ cubeMetrics: [
41
+ {
42
+ name: 'default.total_repair_cost',
43
+ },
44
+ {
45
+ name: 'default.num_repair_orders',
46
+ },
47
+ ],
48
+ cubeDimensions: [
49
+ {
50
+ name: 'default.hard_hat.country',
51
+ attribute: 'country',
52
+ properties: ['dimension'],
53
+ },
54
+ {
55
+ name: 'default.hard_hat.state',
56
+ attribute: 'state',
57
+ properties: ['dimension'],
58
+ },
59
+ ],
60
+ },
61
+ tags: [
62
+ {
63
+ name: 'repairs',
64
+ displayName: 'Repairs Domain',
65
+ },
66
+ ],
67
+ };
68
+
69
+ const mockCommonDimensions = [
70
+ {
71
+ name: 'default.date_dim.dateint',
72
+ type: 'timestamp',
73
+ node_name: 'default.date_dim',
74
+ node_display_name: 'Date',
75
+ properties: [],
76
+ path: [
77
+ 'default.repair_order_details.repair_order_id',
78
+ 'default.repair_order.hard_hat_id',
79
+ 'default.hard_hat.birth_date',
80
+ ],
81
+ },
82
+ {
83
+ name: 'default.date_dim.dateint',
84
+ type: 'timestamp',
85
+ node_name: 'default.date_dim',
86
+ node_display_name: 'Date',
87
+ properties: [],
88
+ path: [
89
+ 'default.repair_order_details.repair_order_id',
90
+ 'default.repair_order.hard_hat_id',
91
+ 'default.hard_hat.hire_date',
92
+ ],
93
+ },
94
+ {
95
+ name: 'default.date_dim.day',
96
+ type: 'int',
97
+ node_name: 'default.date_dim',
98
+ node_display_name: 'Date',
99
+ properties: [],
100
+ path: [
101
+ 'default.repair_order_details.repair_order_id',
102
+ 'default.repair_order.hard_hat_id',
103
+ 'default.hard_hat.birth_date',
104
+ ],
105
+ },
106
+ {
107
+ name: 'default.date_dim.day',
108
+ type: 'int',
109
+ node_name: 'default.date_dim',
110
+ node_display_name: 'Date',
111
+ properties: [],
112
+ path: [
113
+ 'default.repair_order_details.repair_order_id',
114
+ 'default.repair_order.hard_hat_id',
115
+ 'default.hard_hat.hire_date',
116
+ ],
117
+ },
118
+ {
119
+ name: 'default.date_dim.month',
120
+ type: 'int',
121
+ node_name: 'default.date_dim',
122
+ node_display_name: 'Date',
123
+ properties: [],
124
+ path: [
125
+ 'default.repair_order_details.repair_order_id',
126
+ 'default.repair_order.hard_hat_id',
127
+ 'default.hard_hat.birth_date',
128
+ ],
129
+ },
130
+ {
131
+ name: 'default.date_dim.month',
132
+ type: 'int',
133
+ node_name: 'default.date_dim',
134
+ node_display_name: 'Date',
135
+ properties: [],
136
+ path: [
137
+ 'default.repair_order_details.repair_order_id',
138
+ 'default.repair_order.hard_hat_id',
139
+ 'default.hard_hat.hire_date',
140
+ ],
141
+ },
142
+ {
143
+ name: 'default.date_dim.year',
144
+ type: 'int',
145
+ node_name: 'default.date_dim',
146
+ node_display_name: 'Date',
147
+ properties: [],
148
+ path: [
149
+ 'default.repair_order_details.repair_order_id',
150
+ 'default.repair_order.hard_hat_id',
151
+ 'default.hard_hat.birth_date',
152
+ ],
153
+ },
154
+ {
155
+ name: 'default.date_dim.year',
156
+ type: 'int',
157
+ node_name: 'default.date_dim',
158
+ node_display_name: 'Date',
159
+ properties: [],
160
+ path: [
161
+ 'default.repair_order_details.repair_order_id',
162
+ 'default.repair_order.hard_hat_id',
163
+ 'default.hard_hat.hire_date',
164
+ ],
165
+ },
166
+ ];
167
+
168
+ describe('CubeBuilderPage', () => {
169
+ beforeEach(() => {
170
+ mockDjClient.metrics.mockResolvedValue(mockMetrics);
171
+ mockDjClient.commonDimensions.mockResolvedValue(mockCommonDimensions);
172
+ mockDjClient.createCube.mockResolvedValue({ status: 201, json: {} });
173
+ mockDjClient.namespaces.mockResolvedValue(['default']);
174
+ mockDjClient.getCubeForEditing.mockResolvedValue(mockCube);
175
+ mockDjClient.listTags.mockResolvedValue([]);
176
+ mockDjClient.tagsNode.mockResolvedValue([]);
177
+ mockDjClient.patchCube.mockResolvedValue({ status: 201, json: {} });
178
+ mockDjClient.users.mockResolvedValue([{ username: 'dj' }]);
179
+ mockDjClient.whoami.mockResolvedValue({ username: 'dj' });
180
+
181
+ window.scrollTo = jest.fn();
182
+ });
183
+
184
+ afterEach(() => {
185
+ jest.clearAllMocks();
186
+ });
187
+
188
+ it('renders without crashing', () => {
189
+ render(
190
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
191
+ <CubeBuilderPage />
192
+ </DJClientContext.Provider>,
193
+ );
194
+ expect(screen.getByText('Cube')).toBeInTheDocument();
195
+ });
196
+
197
+ it('renders the Metrics section', () => {
198
+ render(
199
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
200
+ <CubeBuilderPage />
201
+ </DJClientContext.Provider>,
202
+ );
203
+ expect(screen.getByText('Metrics *')).toBeInTheDocument();
204
+ });
205
+
206
+ it('renders the Dimensions section', () => {
207
+ render(
208
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
209
+ <CubeBuilderPage />
210
+ </DJClientContext.Provider>,
211
+ );
212
+ expect(screen.getByText('Dimensions *')).toBeInTheDocument();
213
+ });
214
+
215
+ it('creates a new cube', async () => {
216
+ render(
217
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
218
+ <CubeBuilderPage />
219
+ </DJClientContext.Provider>,
220
+ );
221
+
222
+ await waitFor(() => {
223
+ expect(mockDjClient.metrics).toHaveBeenCalled();
224
+ });
225
+
226
+ const selectMetrics = screen.getAllByTestId('select-metrics')[0];
227
+ expect(selectMetrics).toBeDefined();
228
+ expect(selectMetrics).not.toBeNull();
229
+ expect(screen.getAllByText('3 Available Metrics')[0]).toBeInTheDocument();
230
+
231
+ fireEvent.keyDown(selectMetrics.firstChild, { key: 'ArrowDown' });
232
+ for (const metric of mockMetrics) {
233
+ await waitFor(() => {
234
+ expect(screen.getByText(metric)).toBeInTheDocument();
235
+ fireEvent.click(screen.getByText(metric));
236
+ });
237
+ }
238
+ fireEvent.click(screen.getAllByText('Dimensions *')[0]);
239
+
240
+ expect(mockDjClient.commonDimensions).toHaveBeenCalled();
241
+
242
+ const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
243
+ expect(selectDimensions).toBeDefined();
244
+ expect(selectDimensions).not.toBeNull();
245
+ expect(
246
+ screen.getByText(
247
+ 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
248
+ ),
249
+ ).toBeInTheDocument();
250
+
251
+ const selectDimensionsDate = screen.getAllByTestId(
252
+ 'dimensions-default.date_dim',
253
+ )[0];
254
+
255
+ fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
256
+ fireEvent.click(screen.getByText('Day'));
257
+ fireEvent.click(screen.getByText('Month'));
258
+ fireEvent.click(screen.getByText('Year'));
259
+ fireEvent.click(screen.getByText('Dateint'));
260
+
261
+ // Save
262
+ const createCube = screen.getAllByRole('button', {
263
+ name: 'CreateCube',
264
+ })[0];
265
+ expect(createCube).toBeInTheDocument();
266
+
267
+ await waitFor(() => {
268
+ fireEvent.click(createCube);
269
+ });
270
+ await waitFor(() => {
271
+ expect(mockDjClient.createCube).toHaveBeenCalledWith(
272
+ '',
273
+ '',
274
+ '',
275
+ 'published',
276
+ [
277
+ 'default.num_repair_orders',
278
+ 'default.avg_repair_price',
279
+ 'default.total_repair_cost',
280
+ ],
281
+ [
282
+ 'default.date_dim.day',
283
+ 'default.date_dim.month',
284
+ 'default.date_dim.year',
285
+ 'default.date_dim.dateint',
286
+ ],
287
+ [],
288
+ );
289
+ });
290
+ });
291
+
292
+ const renderEditNode = element => {
293
+ return render(
294
+ <MemoryRouter
295
+ initialEntries={['/nodes/default.repair_orders_cube/edit-cube']}
296
+ >
297
+ <Routes>
298
+ <Route path="nodes/:name/edit-cube" element={element} />
299
+ </Routes>
300
+ </MemoryRouter>,
301
+ );
302
+ };
303
+
304
+ it('updates an existing cube', async () => {
305
+ renderEditNode(
306
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
307
+ <CubeBuilderPage />
308
+ </DJClientContext.Provider>,
309
+ );
310
+ expect(screen.getAllByText('Edit')[0]).toBeInTheDocument();
311
+ await waitFor(() => {
312
+ expect(mockDjClient.getCubeForEditing).toHaveBeenCalled();
313
+ });
314
+ await waitFor(() => {
315
+ expect(mockDjClient.metrics).toHaveBeenCalled();
316
+ });
317
+
318
+ const selectMetrics = screen.getAllByTestId('select-metrics')[0];
319
+ expect(selectMetrics).toBeDefined();
320
+ expect(selectMetrics).not.toBeNull();
321
+ expect(screen.getByText('default.num_repair_orders')).toBeInTheDocument();
322
+
323
+ fireEvent.click(screen.getAllByText('Dimensions *')[0]);
324
+
325
+ expect(mockDjClient.commonDimensions).toHaveBeenCalled();
326
+
327
+ const selectDimensions = screen.getAllByTestId('select-dimensions')[0];
328
+ expect(selectDimensions).toBeDefined();
329
+ expect(selectDimensions).not.toBeNull();
330
+ expect(
331
+ screen.getByText(
332
+ 'default.repair_order_details.repair_order_id → default.repair_order.hard_hat_id → default.hard_hat.birth_date',
333
+ ),
334
+ ).toBeInTheDocument();
335
+
336
+ const selectDimensionsDate = screen.getAllByTestId(
337
+ 'dimensions-default.date_dim',
338
+ )[0];
339
+
340
+ fireEvent.keyDown(selectDimensionsDate.firstChild, { key: 'ArrowDown' });
341
+ fireEvent.click(screen.getByText('Day'));
342
+ fireEvent.click(screen.getByText('Month'));
343
+ fireEvent.click(screen.getByText('Year'));
344
+ fireEvent.click(screen.getByText('Dateint'));
345
+
346
+ // Save
347
+ const createCube = screen.getAllByRole('button', {
348
+ name: 'CreateCube',
349
+ })[0];
350
+ expect(createCube).toBeInTheDocument();
351
+
352
+ await waitFor(() => {
353
+ fireEvent.click(createCube);
354
+ });
355
+ await waitFor(() => {
356
+ expect(mockDjClient.patchCube).toHaveBeenCalledWith(
357
+ 'default.repair_orders_cube',
358
+ 'Default: Repair Orders Cube',
359
+ 'Repairs cube',
360
+ 'draft',
361
+ ['default.total_repair_cost', 'default.num_repair_orders'],
362
+ [
363
+ 'default.date_dim.day',
364
+ 'default.date_dim.month',
365
+ 'default.date_dim.year',
366
+ 'default.date_dim.dateint',
367
+ ],
368
+ [],
369
+ [],
370
+ );
371
+ });
372
+ });
373
+ });
@@ -0,0 +1,291 @@
1
+ import React, { useContext, useEffect, useState } from 'react';
2
+ import NamespaceHeader from '../../components/NamespaceHeader';
3
+ import { DataJunctionAPI } from '../../services/DJService';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import 'react-querybuilder/dist/query-builder.scss';
6
+ import 'styles/styles.scss';
7
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
8
+ import { displayMessageAfterSubmit } from '../../../utils/form';
9
+ import { useParams } from 'react-router-dom';
10
+ import { Action } from '../../components/forms/Action';
11
+ import NodeNameField from '../../components/forms/NodeNameField';
12
+ import { MetricsSelect } from './MetricsSelect';
13
+ import { DimensionsSelect } from './DimensionsSelect';
14
+ import { TagsField } from '../AddEditNodePage/TagsField';
15
+ import { OwnersField } from '../AddEditNodePage/OwnersField';
16
+
17
+ export function CubeBuilderPage() {
18
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
19
+
20
+ let { nodeType, initialNamespace, name } = useParams();
21
+ const action = name !== undefined ? Action.Edit : Action.Add;
22
+ const validator = ruleType => !!ruleType.value;
23
+
24
+ const initialValues = {
25
+ name: action === Action.Edit ? name : '',
26
+ namespace: action === Action.Add ? initialNamespace : '',
27
+ display_name: '',
28
+ description: '',
29
+ mode: 'published',
30
+ metrics: [],
31
+ dimensions: [],
32
+ filters: [],
33
+ tags: [],
34
+ owners: [],
35
+ };
36
+
37
+ const handleSubmit = (values, { setSubmitting, setStatus }) => {
38
+ if (action === Action.Add) {
39
+ setTimeout(() => {
40
+ createNode(values, setStatus);
41
+ setSubmitting(false);
42
+ }, 400);
43
+ } else {
44
+ setTimeout(() => {
45
+ patchNode(values, setStatus);
46
+ setSubmitting(false);
47
+ }, 400);
48
+ }
49
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
50
+ };
51
+
52
+ const createNode = async (values, setStatus) => {
53
+ const { status, json } = await djClient.createCube(
54
+ values.name,
55
+ values.display_name,
56
+ values.description,
57
+ values.mode,
58
+ values.metrics,
59
+ values.dimensions,
60
+ values.filters || [],
61
+ );
62
+ if (status === 200 || status === 201) {
63
+ if (values.tags) {
64
+ await djClient.tagsNode(values.name, values.tags);
65
+ }
66
+ setStatus({
67
+ success: (
68
+ <>
69
+ Successfully created {json.type} node{' '}
70
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
71
+ </>
72
+ ),
73
+ });
74
+ } else {
75
+ setStatus({
76
+ failure: `${json.message}`,
77
+ });
78
+ }
79
+ };
80
+
81
+ const patchNode = async (values, setStatus) => {
82
+ const { status, json } = await djClient.patchCube(
83
+ values.name,
84
+ values.display_name,
85
+ values.description,
86
+ values.mode,
87
+ values.metrics,
88
+ values.dimensions,
89
+ values.filters || [],
90
+ values.owners,
91
+ );
92
+ const tagsResponse = await djClient.tagsNode(
93
+ values.name,
94
+ (values.tags || []).map(tag => tag),
95
+ );
96
+ if ((status === 200 || status === 201) && tagsResponse.status === 200) {
97
+ setStatus({
98
+ success: (
99
+ <>
100
+ Successfully updated {json.type} node{' '}
101
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
102
+ </>
103
+ ),
104
+ });
105
+ } else {
106
+ setStatus({
107
+ failure: `${json.message}`,
108
+ });
109
+ }
110
+ };
111
+
112
+ const updateFieldsWithNodeData = (
113
+ data,
114
+ setFieldValue,
115
+ setSelectTags,
116
+ setSelectOwners,
117
+ ) => {
118
+ setFieldValue('display_name', data.current.displayName || '', false);
119
+ setFieldValue('description', data.current.description || '', false);
120
+ setFieldValue('mode', data.current.mode.toLowerCase() || 'draft', false);
121
+ setFieldValue(
122
+ 'tags',
123
+ data.tags.map(tag => tag.name),
124
+ );
125
+ // For react-select fields, we have to explicitly set the entire
126
+ // field rather than just the values
127
+ setSelectTags(
128
+ <TagsField
129
+ defaultValue={data.tags.map(t => {
130
+ return { value: t.name, label: t.displayName };
131
+ })}
132
+ />,
133
+ );
134
+ if (data.owners) {
135
+ setSelectOwners(
136
+ <OwnersField
137
+ defaultValue={data.owners.map(owner => {
138
+ return { value: owner.username, label: owner.username };
139
+ })}
140
+ />,
141
+ );
142
+ }
143
+ };
144
+
145
+ const staticFieldsInEdit = () => (
146
+ <>
147
+ <div className="NodeNameInput NodeCreationInput">
148
+ <label htmlFor="name">Name</label> {name}
149
+ </div>
150
+ <div className="NodeNameInput NodeCreationInput">
151
+ <label htmlFor="name">Type</label> cube
152
+ </div>
153
+ <div className="DisplayNameInput NodeCreationInput">
154
+ <ErrorMessage name="display_name" component="span" />
155
+ <label htmlFor="displayName">Display Name</label>
156
+ <Field
157
+ type="text"
158
+ name="display_name"
159
+ id="displayName"
160
+ placeholder="Human readable display name"
161
+ />
162
+ </div>
163
+ </>
164
+ );
165
+
166
+ // @ts-ignore
167
+ return (
168
+ <>
169
+ <div className="mid">
170
+ <NamespaceHeader namespace="" />
171
+ <Formik
172
+ initialValues={initialValues}
173
+ validate={validator}
174
+ onSubmit={handleSubmit}
175
+ >
176
+ {function Render({ isSubmitting, status, setFieldValue, props }) {
177
+ const [node, setNode] = useState([]);
178
+ const [selectTags, setSelectTags] = useState(null);
179
+ const [selectOwners, setSelectOwners] = useState(null);
180
+
181
+ // Get cube
182
+ useEffect(() => {
183
+ const fetchData = async () => {
184
+ if (name) {
185
+ const cube = await djClient.getCubeForEditing(name);
186
+ setNode(cube);
187
+ updateFieldsWithNodeData(
188
+ cube,
189
+ setFieldValue,
190
+ setSelectTags,
191
+ setSelectOwners,
192
+ );
193
+ }
194
+ };
195
+ fetchData().catch(console.error);
196
+ }, [setFieldValue]);
197
+
198
+ return (
199
+ <Form>
200
+ <div className="card">
201
+ <div className="card-header">
202
+ <h2>
203
+ {action === Action.Edit ? 'Edit' : 'Create'}{' '}
204
+ <span
205
+ className={`node_type__cube node_type_creation_heading`}
206
+ >
207
+ Cube
208
+ </span>
209
+ </h2>
210
+ {displayMessageAfterSubmit(status)}
211
+ {action === Action.Add ? (
212
+ <NodeNameField />
213
+ ) : (
214
+ staticFieldsInEdit(node)
215
+ )}
216
+ <div className="DescriptionInput NodeCreationInput">
217
+ <ErrorMessage name="description" component="span" />
218
+ <label htmlFor="Description">Description</label>
219
+ <Field
220
+ type="textarea"
221
+ as="textarea"
222
+ name="description"
223
+ id="Description"
224
+ placeholder="Describe your node"
225
+ />
226
+ </div>
227
+ <div className="CubeCreationInput">
228
+ <label>Metrics *</label>
229
+ <p>Select metrics to include in the cube.</p>
230
+ <span
231
+ data-testid="select-metrics"
232
+ style={{ marginTop: '15px' }}
233
+ >
234
+ {action === Action.Edit ? (
235
+ <MetricsSelect cube={node} />
236
+ ) : (
237
+ <MetricsSelect />
238
+ )}
239
+ </span>
240
+ </div>
241
+ <br />
242
+ <br />
243
+ <div className="CubeCreationInput">
244
+ <label>Dimensions *</label>
245
+ <p>
246
+ Select dimensions to include in the cube. As metrics are
247
+ selected above, the list of available dimensions will be
248
+ filtered to those shared by the selected metrics. If the
249
+ dimensions list is empty, no shared dimensions were
250
+ discovered.
251
+ </p>
252
+ <span data-testid="select-dimensions">
253
+ {action === Action.Edit ? (
254
+ <DimensionsSelect cube={node} />
255
+ ) : (
256
+ <DimensionsSelect />
257
+ )}
258
+ </span>
259
+ </div>
260
+ <div className="NodeModeInput NodeCreationInput">
261
+ <ErrorMessage name="mode" component="span" />
262
+ <label htmlFor="Mode">Mode</label>
263
+ <Field as="select" name="mode" id="Mode">
264
+ <option value="draft">Draft</option>
265
+ <option value="published">Published</option>
266
+ </Field>
267
+ </div>
268
+ {action === Action.Edit ? selectTags : <TagsField />}
269
+ {action === Action.Edit ? selectOwners : <OwnersField />}
270
+ <button
271
+ type="submit"
272
+ disabled={isSubmitting}
273
+ aria-label="CreateCube"
274
+ >
275
+ {action === Action.Add ? 'Create Cube' : 'Save'}{' '}
276
+ {nodeType}
277
+ </button>
278
+ </div>
279
+ </div>
280
+ </Form>
281
+ );
282
+ }}
283
+ </Formik>
284
+ </div>
285
+ </>
286
+ );
287
+ }
288
+
289
+ CubeBuilderPage.defaultProps = {
290
+ djClient: DataJunctionAPI,
291
+ };