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,506 @@
1
+ /**
2
+ * Node add + edit page for transforms, metrics, and dimensions. The creation and edit flow for these
3
+ * node types is largely the same, with minor differences handled server-side. For the `query`
4
+ * field, this page will render a CodeMirror SQL editor with autocompletion and syntax highlighting.
5
+ */
6
+ import { ErrorMessage, Form, Formik } from 'formik';
7
+ import NamespaceHeader from '../../components/NamespaceHeader';
8
+ import { useContext, useEffect, useState } from 'react';
9
+ import DJClientContext from '../../providers/djclient';
10
+ import 'styles/node-creation.scss';
11
+ import { useParams, useNavigate } from 'react-router-dom';
12
+ import { FullNameField } from './FullNameField';
13
+ import { MetricQueryField } from './MetricQueryField';
14
+ import { displayMessageAfterSubmit } from '../../../utils/form';
15
+ import { NodeQueryField } from './NodeQueryField';
16
+ import { MetricMetadataFields } from './MetricMetadataFields';
17
+ import { UpstreamNodeField } from './UpstreamNodeField';
18
+ import { OwnersField } from './OwnersField';
19
+ import { TagsField } from './TagsField';
20
+ import { NamespaceField } from './NamespaceField';
21
+ import { AlertMessage } from './AlertMessage';
22
+ import { DisplayNameField } from './DisplayNameField';
23
+ import { DescriptionField } from './DescriptionField';
24
+ import { NodeModeField } from './NodeModeField';
25
+ import { RequiredDimensionsSelect } from './RequiredDimensionsSelect';
26
+ import LoadingIcon from '../../icons/LoadingIcon';
27
+ import { ColumnsSelect } from './ColumnsSelect';
28
+
29
+ class Action {
30
+ static Add = new Action('add');
31
+ static Edit = new Action('edit');
32
+
33
+ constructor(name) {
34
+ this.name = name;
35
+ }
36
+ }
37
+
38
+ export function AddEditNodePage({ extensions = {} }) {
39
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
40
+ const navigate = useNavigate();
41
+
42
+ let { nodeType, initialNamespace, name } = useParams();
43
+ const action = name !== undefined ? Action.Edit : Action.Add;
44
+
45
+ const initialValues = {
46
+ name: action === Action.Edit ? name : '',
47
+ namespace: action === Action.Add ? initialNamespace : '',
48
+ display_name: '',
49
+ query: '',
50
+ type: nodeType,
51
+ description: '',
52
+ primary_key: '',
53
+ mode: 'published',
54
+ owners: [],
55
+ };
56
+
57
+ const validator = values => {
58
+ const errors = {};
59
+ if (!values.name) {
60
+ errors.name = 'Required';
61
+ }
62
+ if (values.type !== 'metric' && !values.query) {
63
+ errors.query = 'Required';
64
+ }
65
+ return errors;
66
+ };
67
+
68
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
69
+ if (action === Action.Add) {
70
+ await createNode(values, setStatus).then(_ => {
71
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
72
+ setSubmitting(false);
73
+ });
74
+ } else {
75
+ await patchNode(values, setStatus).then(_ => {
76
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
77
+ setSubmitting(false);
78
+ });
79
+ }
80
+ };
81
+ const submitHandlers = [handleSubmit];
82
+
83
+ const pageTitle =
84
+ action === Action.Add ? (
85
+ <h2>
86
+ Create{' '}
87
+ <span className={`node_type__${nodeType} node_type_creation_heading`}>
88
+ {nodeType}
89
+ </span>
90
+ </h2>
91
+ ) : (
92
+ <h2>Edit</h2>
93
+ );
94
+
95
+ const staticFieldsInEdit = node => (
96
+ <>
97
+ <div className="NodeNameInput NodeCreationInput">
98
+ <label>Name</label> {name}
99
+ </div>
100
+ <div className="NodeNameInput NodeCreationInput">
101
+ <label>Type</label> {node.type}
102
+ </div>
103
+ </>
104
+ );
105
+
106
+ const primaryKeyToList = primaryKey => {
107
+ return primaryKey.map(columnName => columnName.trim());
108
+ };
109
+
110
+ const createNode = async (values, setStatus) => {
111
+ const { status, json } = await djClient.createNode(
112
+ nodeType,
113
+ values.name,
114
+ values.display_name,
115
+ values.description,
116
+ values.type === 'metric'
117
+ ? `SELECT ${values.aggregate_expression} \n FROM ${values.upstream_node}`
118
+ : values.query,
119
+ values.mode,
120
+ values.namespace,
121
+ values.primary_key ? primaryKeyToList(values.primary_key) : null,
122
+ values.metric_direction,
123
+ values.metric_unit,
124
+ values.required_dimensions,
125
+ );
126
+ if (status === 200 || status === 201) {
127
+ if (values.tags) {
128
+ await djClient.tagsNode(values.name, values.tags);
129
+ }
130
+ setStatus({
131
+ success: (
132
+ <>
133
+ Successfully created {json.type} node{' '}
134
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
135
+ </>
136
+ ),
137
+ });
138
+ } else {
139
+ setStatus({
140
+ failure: `${json.message}`,
141
+ });
142
+ }
143
+ };
144
+
145
+ const patchNode = async (values, setStatus) => {
146
+ const { status, json } = await djClient.patchNode(
147
+ values.name,
148
+ values.display_name,
149
+ values.description,
150
+ values.type === 'metric'
151
+ ? `SELECT ${values.aggregate_expression} \n FROM ${values.upstream_node}`
152
+ : values.query,
153
+ values.mode,
154
+ values.primary_key ? primaryKeyToList(values.primary_key) : null,
155
+ values.metric_direction,
156
+ values.metric_unit,
157
+ values.significant_digits,
158
+ values.required_dimensions,
159
+ values.owners,
160
+ );
161
+ const tagsResponse = await djClient.tagsNode(
162
+ values.name,
163
+ values.tags.map(tag => tag),
164
+ );
165
+ if ((status === 200 || status === 201) && tagsResponse.status === 200) {
166
+ setStatus({
167
+ success: (
168
+ <>
169
+ Successfully updated {json.type} node{' '}
170
+ <a href={`/nodes/${json.name}`}>{json.name}</a>!
171
+ </>
172
+ ),
173
+ });
174
+ } else {
175
+ setStatus({
176
+ failure: `${json.message}`,
177
+ });
178
+ }
179
+ };
180
+
181
+ const fullNameInput = (
182
+ <div className="FullNameInput NodeCreationInput">
183
+ <ErrorMessage name="name" component="span" />
184
+ <label htmlFor="FullName">Full Name *</label>
185
+ <FullNameField type="text" name="name" />
186
+ </div>
187
+ );
188
+
189
+ const nodeCanBeEdited = nodeType => {
190
+ return new Set(['transform', 'metric', 'dimension']).has(nodeType);
191
+ };
192
+
193
+ const getExistingNodeData = async name => {
194
+ const node = await djClient.getNodeForEditing(name);
195
+ if (node === null) {
196
+ return { message: `Node ${name} does not exist` };
197
+ }
198
+ const baseData = {
199
+ name: node.name,
200
+ type: node.type.toLowerCase(),
201
+ display_name: node.current.displayName,
202
+ description: node.current.description,
203
+ primary_key: node.current.primaryKey,
204
+ query: node.current.query,
205
+ tags: node.tags,
206
+ mode: node.current.mode.toLowerCase(),
207
+ owners: node.owners,
208
+ };
209
+
210
+ if (node.type === 'METRIC') {
211
+ return {
212
+ ...baseData,
213
+ metric_direction: node.current.metricMetadata?.direction?.toLowerCase(),
214
+ metric_unit: node.current.metricMetadata?.unit?.name?.toLowerCase(),
215
+ significant_digits: node.current.metricMetadata?.significantDigits,
216
+ required_dimensions: node.current.requiredDimensions.map(
217
+ dim => dim.name,
218
+ ),
219
+ upstream_node: node.current.parents[0]?.name,
220
+ aggregate_expression: node.current.metricMetadata?.expression,
221
+ };
222
+ }
223
+ return baseData;
224
+ };
225
+
226
+ const runValidityChecks = (data, setNode, setMessage) => {
227
+ // Check if node exists
228
+ if (data.message !== undefined) {
229
+ setNode(null);
230
+ setMessage(`Node ${name} does not exist!`);
231
+ return;
232
+ }
233
+ // Check if node type can be edited
234
+ if (!nodeCanBeEdited(data.type)) {
235
+ setNode(null);
236
+ if (data.type === 'cube') {
237
+ navigate(`/nodes/${data.name}/edit-cube`);
238
+ }
239
+ setMessage(`Node ${name} is of type ${data.type} and cannot be edited`);
240
+ }
241
+ };
242
+
243
+ const updateFieldsWithNodeData = (
244
+ data,
245
+ setFieldValue,
246
+ setNode,
247
+ setSelectTags,
248
+ setSelectPrimaryKey,
249
+ setSelectUpstreamNode,
250
+ setSelectRequiredDims,
251
+ setSelectOwners,
252
+ ) => {
253
+ // Update fields with existing data to prepare for edit
254
+ const fields = [
255
+ 'display_name',
256
+ 'query',
257
+ 'type',
258
+ 'description',
259
+ 'primary_key',
260
+ 'mode',
261
+ 'tags',
262
+ 'aggregate_expression',
263
+ 'upstream_node',
264
+ 'metric_unit',
265
+ 'metric_direction',
266
+ 'significant_digits',
267
+ 'owners',
268
+ ];
269
+ fields.forEach(field => {
270
+ if (field === 'tags') {
271
+ setFieldValue(
272
+ field,
273
+ data[field].map(tag => tag.name),
274
+ );
275
+ } else if (field === 'owners') {
276
+ setFieldValue(
277
+ field,
278
+ data[field].map(owner => owner.username),
279
+ );
280
+ } else {
281
+ setFieldValue(field, data[field] || '', false);
282
+ }
283
+ });
284
+ setNode(data);
285
+
286
+ // For react-select fields, we have to explicitly set the entire
287
+ // field rather than just the values
288
+ setSelectTags(
289
+ <TagsField
290
+ defaultValue={data.tags.map(t => {
291
+ return { value: t.name, label: t.displayName };
292
+ })}
293
+ />,
294
+ );
295
+ setSelectPrimaryKey(
296
+ <ColumnsSelect
297
+ defaultValue={data.primary_key}
298
+ fieldName="primary_key"
299
+ label="Primary Key"
300
+ isMulti={true}
301
+ />,
302
+ );
303
+ if (data.required_dimensions) {
304
+ setSelectRequiredDims(
305
+ <RequiredDimensionsSelect
306
+ defaultValue={data.required_dimensions.map(dim => {
307
+ return { value: dim, label: dim };
308
+ })}
309
+ />,
310
+ );
311
+ }
312
+ setSelectUpstreamNode(
313
+ <UpstreamNodeField
314
+ defaultValue={{
315
+ value: data.upstream_node,
316
+ label: data.upstream_node,
317
+ }}
318
+ />,
319
+ );
320
+ if (data.owners) {
321
+ setSelectOwners(
322
+ <OwnersField
323
+ defaultValue={data.owners.map(owner => {
324
+ return { value: owner.username, label: owner.username };
325
+ })}
326
+ />,
327
+ );
328
+ }
329
+ };
330
+
331
+ return (
332
+ <div className="mid">
333
+ <NamespaceHeader
334
+ namespace={
335
+ initialNamespace
336
+ ? initialNamespace
337
+ : name
338
+ ? name.substring(0, name.lastIndexOf('.'))
339
+ : ''
340
+ }
341
+ />
342
+ <div className="card">
343
+ <div className="card-header">
344
+ {pageTitle}
345
+ <center>
346
+ <Formik
347
+ initialValues={initialValues}
348
+ validate={validator}
349
+ onSubmit={async (values, { setSubmitting, setStatus }) => {
350
+ try {
351
+ for (const handler of submitHandlers) {
352
+ await handler(values, { setSubmitting, setStatus });
353
+ }
354
+ } catch (error) {
355
+ console.error('Error in submission', error);
356
+ } finally {
357
+ setSubmitting(false);
358
+ }
359
+ }}
360
+ >
361
+ {function Render({ isSubmitting, status, setFieldValue }) {
362
+ const [node, setNode] = useState([]);
363
+ const [selectPrimaryKey, setSelectPrimaryKey] = useState(null);
364
+ const [selectRequiredDims, setSelectRequiredDims] =
365
+ useState(null);
366
+ const [selectUpstreamNode, setSelectUpstreamNode] =
367
+ useState(null);
368
+ const [selectOwners, setSelectOwners] = useState(null);
369
+ const [selectTags, setSelectTags] = useState(null);
370
+ const [message, setMessage] = useState('');
371
+
372
+ useEffect(() => {
373
+ const fetchData = async () => {
374
+ if (action === Action.Edit) {
375
+ const data = await getExistingNodeData(name);
376
+ runValidityChecks(data, setNode, setMessage);
377
+ updateFieldsWithNodeData(
378
+ data,
379
+ setFieldValue,
380
+ setNode,
381
+ setSelectTags,
382
+ setSelectPrimaryKey,
383
+ setSelectUpstreamNode,
384
+ setSelectRequiredDims,
385
+ setSelectOwners,
386
+ );
387
+ }
388
+ };
389
+ fetchData().catch(console.error);
390
+ }, [setFieldValue]);
391
+ return (
392
+ <Form>
393
+ {displayMessageAfterSubmit(status)}
394
+ {action === Action.Edit && message ? (
395
+ <AlertMessage message={message} />
396
+ ) : (
397
+ <>
398
+ {action === Action.Add ? (
399
+ <NamespaceField initialNamespace={initialNamespace} />
400
+ ) : (
401
+ staticFieldsInEdit(node)
402
+ )}
403
+ <DisplayNameField />
404
+ {action === Action.Add ? fullNameInput : ''}
405
+ <DescriptionField />
406
+ <br />
407
+ {nodeType === 'metric' || node?.type === 'metric' ? (
408
+ action === Action.Edit ? (
409
+ selectUpstreamNode
410
+ ) : (
411
+ <UpstreamNodeField />
412
+ )
413
+ ) : (
414
+ ''
415
+ )}
416
+ <br />
417
+ <br />
418
+ {action === Action.Edit ? (
419
+ selectOwners
420
+ ) : (
421
+ <OwnersField />
422
+ )}
423
+ <br />
424
+ <br />
425
+ {nodeType === 'metric' || node.type === 'metric' ? (
426
+ <MetricQueryField
427
+ djClient={djClient}
428
+ value={
429
+ node.aggregate_expression
430
+ ? node.aggregate_expression
431
+ : ''
432
+ }
433
+ />
434
+ ) : (
435
+ <NodeQueryField
436
+ djClient={djClient}
437
+ value={node.query ? node.query : ''}
438
+ />
439
+ )}
440
+ <br />
441
+ {nodeType === 'metric' || node.type === 'metric' ? (
442
+ <MetricMetadataFields />
443
+ ) : (
444
+ ''
445
+ )}
446
+ {nodeType !== 'metric' && node.type !== 'metric' ? (
447
+ action === Action.Edit ? (
448
+ selectPrimaryKey
449
+ ) : (
450
+ <ColumnsSelect
451
+ defaultValue={[]}
452
+ fieldName="primary_key"
453
+ label="Primary Key"
454
+ isMulti={true}
455
+ />
456
+ )
457
+ ) : action === Action.Edit ? (
458
+ selectRequiredDims
459
+ ) : (
460
+ <RequiredDimensionsSelect />
461
+ )}
462
+ {Object.entries(extensions).map(
463
+ ([key, ExtensionComponent]) => (
464
+ <div key={key} className="mt-4 border-t pt-4">
465
+ <ExtensionComponent
466
+ node={node}
467
+ action={action}
468
+ registerSubmitHandler={(
469
+ onSubmit,
470
+ { prepend } = {},
471
+ ) => {
472
+ if (!submitHandlers.includes(onSubmit)) {
473
+ if (prepend) {
474
+ submitHandlers.unshift(onSubmit);
475
+ } else {
476
+ submitHandlers.push(onSubmit);
477
+ }
478
+ }
479
+ }}
480
+ />
481
+ </div>
482
+ ),
483
+ )}
484
+ {action === Action.Edit ? selectTags : <TagsField />}
485
+ <NodeModeField />
486
+
487
+ <button type="submit" disabled={isSubmitting}>
488
+ {isSubmitting ? (
489
+ <LoadingIcon />
490
+ ) : (
491
+ (action === Action.Add ? 'Create ' : 'Save ') +
492
+ (nodeType ? nodeType : '')
493
+ )}
494
+ </button>
495
+ </>
496
+ )}
497
+ </Form>
498
+ );
499
+ }}
500
+ </Formik>
501
+ </center>
502
+ </div>
503
+ </div>
504
+ </div>
505
+ );
506
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Asynchronously loads the component for the Node page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const AddEditTagPage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.AddEditTagPage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,107 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { render } from '../../../../setupTests';
6
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
7
+ import DJClientContext from '../../../providers/djclient';
8
+ import { AddEditTagPage } from '../index';
9
+
10
+ describe('<AddEditTagPage />', () => {
11
+ const initializeMockDJClient = () => {
12
+ return {
13
+ DataJunctionAPI: {
14
+ addTag: jest.fn(),
15
+ },
16
+ };
17
+ };
18
+
19
+ const mockDjClient = initializeMockDJClient();
20
+
21
+ beforeEach(() => {
22
+ fetchMock.resetMocks();
23
+ jest.clearAllMocks();
24
+ window.scrollTo = jest.fn();
25
+ });
26
+
27
+ const renderAddEditTagPage = element => {
28
+ return render(
29
+ <MemoryRouter initialEntries={['/create/tag']}>
30
+ <Routes>
31
+ <Route path="create/tag" element={element} />
32
+ </Routes>
33
+ </MemoryRouter>,
34
+ );
35
+ };
36
+
37
+ const testElement = djClient => {
38
+ return (
39
+ <DJClientContext.Provider value={djClient}>
40
+ <AddEditTagPage />
41
+ </DJClientContext.Provider>
42
+ );
43
+ };
44
+
45
+ it('adds a tag correctly', async () => {
46
+ mockDjClient.DataJunctionAPI.addTag.mockReturnValue({
47
+ status: 200,
48
+ json: {
49
+ name: 'amanita_muscaria',
50
+ display_name: 'Amanita Muscaria',
51
+ tag_type: 'fungi',
52
+ description: 'Fly agaric, is poisonous',
53
+ },
54
+ });
55
+
56
+ const element = testElement(mockDjClient);
57
+ renderAddEditTagPage(element);
58
+
59
+ await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria');
60
+ await userEvent.type(
61
+ screen.getByLabelText('Display Name'),
62
+ 'Amanita Muscaria',
63
+ );
64
+ await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi');
65
+ await userEvent.click(screen.getByRole('button'));
66
+
67
+ await waitFor(() => {
68
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled();
69
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalledWith(
70
+ 'amanita_muscaria',
71
+ 'Amanita Muscaria',
72
+ 'fungi',
73
+ undefined,
74
+ );
75
+ expect(screen.getByTestId('success')).toHaveTextContent(
76
+ 'Successfully added tag Amanita Muscaria',
77
+ );
78
+ });
79
+ }, 60000);
80
+
81
+ it('fails to add a tag', async () => {
82
+ mockDjClient.DataJunctionAPI.addTag.mockReturnValue({
83
+ status: 500,
84
+ json: { message: 'Tag exists' },
85
+ });
86
+
87
+ const element = testElement(mockDjClient);
88
+ renderAddEditTagPage(element);
89
+
90
+ await userEvent.click(screen.getByRole('button'));
91
+
92
+ await userEvent.type(screen.getByLabelText('Name'), 'amanita_muscaria');
93
+ await userEvent.type(
94
+ screen.getByLabelText('Display Name'),
95
+ 'Amanita Muscaria',
96
+ );
97
+ await userEvent.type(screen.getByLabelText('Tag Type'), 'fungi');
98
+ await userEvent.click(screen.getByRole('button'));
99
+
100
+ await waitFor(() => {
101
+ expect(mockDjClient.DataJunctionAPI.addTag).toBeCalled();
102
+ expect(screen.getByTestId('failure')).toHaveTextContent(
103
+ 'alert_fillTag exists',
104
+ );
105
+ });
106
+ }, 60000);
107
+ });