datajunction-ui 0.0.1-rc.9 → 0.0.2-0.dev1

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 (244) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/cleanup-deps.sh +70 -0
  5. package/dj-logo.svg +10 -0
  6. package/package.json +53 -14
  7. package/public/favicon.ico +0 -0
  8. package/public/index.html +1 -1
  9. package/runit.sh +30 -0
  10. package/runit2.sh +30 -0
  11. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  12. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  13. package/src/app/components/AddNodeDropdown.jsx +44 -0
  14. package/src/app/components/ListGroupItem.jsx +9 -1
  15. package/src/app/components/NamespaceHeader.jsx +4 -13
  16. package/src/app/components/NodeListActions.jsx +69 -0
  17. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  18. package/src/app/components/NotificationBell.tsx +223 -0
  19. package/src/app/components/QueryInfo.jsx +172 -0
  20. package/src/app/components/Search.jsx +94 -0
  21. package/src/app/components/Tab.jsx +8 -1
  22. package/src/app/components/ToggleSwitch.jsx +20 -0
  23. package/src/app/components/UserMenu.tsx +100 -0
  24. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  25. package/src/app/components/__tests__/NodeMaterializationDelete.test.jsx +263 -0
  26. package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
  27. package/src/app/components/__tests__/QueryInfo.test.jsx +183 -0
  28. package/src/app/components/__tests__/Search.test.jsx +307 -0
  29. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  30. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  31. package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
  32. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  33. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  34. package/src/app/components/djgraph/Collapse.jsx +47 -0
  35. package/src/app/components/djgraph/DJNode.jsx +61 -83
  36. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  37. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  38. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  39. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  40. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  41. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  42. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  43. package/src/app/components/forms/Action.jsx +8 -0
  44. package/src/app/components/forms/NodeNameField.jsx +64 -0
  45. package/src/app/components/search.css +17 -0
  46. package/src/app/constants.js +2 -0
  47. package/src/app/icons/AddItemIcon.jsx +16 -0
  48. package/src/app/icons/AlertIcon.jsx +33 -0
  49. package/src/app/icons/CollapsedIcon.jsx +15 -0
  50. package/src/app/icons/CommitIcon.jsx +45 -0
  51. package/src/app/icons/DJLogo.jsx +36 -0
  52. package/src/app/icons/DeleteIcon.jsx +21 -0
  53. package/src/app/icons/DiffIcon.jsx +63 -0
  54. package/src/app/icons/EditIcon.jsx +18 -0
  55. package/src/app/icons/ExpandedIcon.jsx +15 -0
  56. package/src/app/icons/EyeIcon.jsx +20 -0
  57. package/src/app/icons/FilterIcon.jsx +7 -0
  58. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  59. package/src/app/icons/InvalidIcon.jsx +16 -0
  60. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  61. package/src/app/icons/LoadingIcon.jsx +14 -0
  62. package/src/app/icons/NodeIcon.jsx +49 -0
  63. package/src/app/icons/NotificationIcon.jsx +27 -0
  64. package/src/app/icons/PythonIcon.jsx +14 -0
  65. package/src/app/icons/SettingsIcon.jsx +28 -0
  66. package/src/app/icons/TableIcon.jsx +14 -0
  67. package/src/app/icons/ValidIcon.jsx +16 -0
  68. package/src/app/icons/WrenchIcon.jsx +36 -0
  69. package/src/app/index.tsx +130 -37
  70. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  71. package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
  72. package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
  73. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  74. package/src/app/pages/AddEditNodePage/CustomMetadataField.jsx +144 -0
  75. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  76. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  77. package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
  78. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +64 -0
  79. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  80. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  81. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  82. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  83. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  84. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  85. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  86. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  87. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  88. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  89. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  90. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +110 -0
  91. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +291 -0
  92. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  93. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  94. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  95. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  96. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  97. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  98. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  99. package/src/app/pages/AddEditNodePage/index.jsx +545 -0
  100. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  101. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  102. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  103. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  104. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  105. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  106. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  107. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  108. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  109. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  110. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  111. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  112. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  113. package/src/app/pages/LoginPage/index.jsx +17 -0
  114. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  115. package/src/app/pages/NamespacePage/Explorer.jsx +232 -0
  116. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  117. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +27 -0
  118. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  119. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  120. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  121. package/src/app/pages/NamespacePage/__tests__/AddNamespacePopover.test.jsx +283 -0
  122. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +331 -0
  123. package/src/app/pages/NamespacePage/index.jsx +356 -42
  124. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  125. package/src/app/pages/NodePage/AddComplexDimensionLinkPopover.jsx +367 -0
  126. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  127. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  128. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  129. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  130. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  131. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  132. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  133. package/src/app/pages/NodePage/ManageDimensionLinksDialog.jsx +526 -0
  134. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  135. package/src/app/pages/NodePage/NodeColumnTab.jsx +421 -30
  136. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  137. package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
  138. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  139. package/src/app/pages/NodePage/NodeInfoTab.jsx +346 -49
  140. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  141. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  142. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  143. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  144. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  145. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  146. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  147. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  148. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  149. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  150. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  151. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  152. package/src/app/pages/NodePage/__tests__/AddComplexDimensionLinkPopover.test.jsx +459 -0
  153. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  154. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  155. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  156. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +144 -0
  157. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +132 -0
  158. package/src/app/pages/NodePage/__tests__/ManageDimensionLinksDialog.test.jsx +390 -0
  159. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  160. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  161. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  162. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  163. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  164. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +882 -0
  165. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  166. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  167. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  168. package/src/app/pages/NodePage/index.jsx +190 -44
  169. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  170. package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
  171. package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
  172. package/src/app/pages/NotificationsPage/index.jsx +136 -0
  173. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  174. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  175. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  176. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  177. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  178. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  179. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  180. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  181. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  182. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  183. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  184. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  185. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  186. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  187. package/src/app/pages/OverviewPage/index.jsx +22 -0
  188. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  189. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +112 -0
  190. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  191. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  192. package/src/app/pages/Root/__tests__/index.test.jsx +44 -0
  193. package/src/app/pages/Root/index.tsx +92 -10
  194. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  195. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  196. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  197. package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
  198. package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
  199. package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
  200. package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
  201. package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
  202. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
  203. package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
  204. package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
  205. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
  206. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
  207. package/src/app/pages/SettingsPage/index.jsx +148 -0
  208. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  209. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  210. package/src/app/pages/TagPage/index.jsx +79 -0
  211. package/src/app/services/DJService.js +1444 -21
  212. package/src/app/services/__tests__/DJService.test.jsx +2118 -0
  213. package/src/app/utils/__tests__/date.test.js +198 -0
  214. package/src/app/utils/date.js +65 -0
  215. package/src/index.tsx +1 -0
  216. package/src/mocks/mockNodes.jsx +1477 -0
  217. package/src/setupTests.ts +31 -1
  218. package/src/styles/dag.css +117 -5
  219. package/src/styles/index.css +1028 -31
  220. package/src/styles/loading.css +34 -0
  221. package/src/styles/login.css +81 -0
  222. package/src/styles/nav-bar.css +274 -0
  223. package/src/styles/node-creation.scss +276 -0
  224. package/src/styles/node-list.css +4 -0
  225. package/src/styles/overview.css +72 -0
  226. package/src/styles/settings.css +787 -0
  227. package/src/styles/sorted-table.css +15 -0
  228. package/src/styles/styles.scss +44 -0
  229. package/src/styles/styles.scss.d.ts +9 -0
  230. package/src/utils/form.jsx +23 -0
  231. package/webpack.config.js +17 -6
  232. package/.babelrc +0 -4
  233. package/.env.local +0 -4
  234. package/.env.production +0 -1
  235. package/.github/pull_request_template.md +0 -11
  236. package/.github/workflows/ci.yml +0 -33
  237. package/.vscode/extensions.json +0 -7
  238. package/.vscode/launch.json +0 -15
  239. package/.vscode/settings.json +0 -25
  240. package/Dockerfile +0 -7
  241. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  242. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  243. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  244. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,232 @@
1
+ import React, { useContext, useEffect, useRef, useState } from 'react';
2
+ import CollapsedIcon from '../../icons/CollapsedIcon';
3
+ import ExpandedIcon from '../../icons/ExpandedIcon';
4
+ import AddItemIcon from '../../icons/AddItemIcon';
5
+ import DJClientContext from '../../providers/djclient';
6
+
7
+ const Explorer = ({ item = [], current, isTopLevel = false }) => {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+ const [items, setItems] = useState([]);
10
+ const [expand, setExpand] = useState(false);
11
+ const [highlight, setHighlight] = useState(false);
12
+ const [showAddButton, setShowAddButton] = useState(false);
13
+ const [isCreatingChild, setIsCreatingChild] = useState(false);
14
+ const [newNamespace, setNewNamespace] = useState('');
15
+ const [error, setError] = useState('');
16
+ const inputRef = useRef(null);
17
+ const formRef = useRef(null);
18
+
19
+ useEffect(() => {
20
+ setItems(item);
21
+ setHighlight(current);
22
+ if (current !== undefined && current?.startsWith(item.path)) {
23
+ setExpand(true);
24
+ } else setExpand(false);
25
+ }, [current, item]);
26
+
27
+ useEffect(() => {
28
+ if (isCreatingChild && inputRef.current) {
29
+ inputRef.current.focus();
30
+ }
31
+ }, [isCreatingChild]);
32
+
33
+ useEffect(() => {
34
+ const handleClickOutside = event => {
35
+ if (formRef.current && !formRef.current.contains(event.target)) {
36
+ handleCancelAdd();
37
+ }
38
+ };
39
+
40
+ if (isCreatingChild) {
41
+ document.addEventListener('mousedown', handleClickOutside);
42
+ return () => {
43
+ document.removeEventListener('mousedown', handleClickOutside);
44
+ };
45
+ }
46
+ }, [isCreatingChild]);
47
+
48
+ const handleClickOnParent = e => {
49
+ e.stopPropagation();
50
+ setExpand(prev => {
51
+ return !prev;
52
+ });
53
+ };
54
+
55
+ const handleAddNamespace = async e => {
56
+ e.preventDefault();
57
+ if (!newNamespace.trim()) {
58
+ setError('Namespace cannot be empty');
59
+ return;
60
+ }
61
+
62
+ const fullNamespace = items.path
63
+ ? `${items.path}.${newNamespace}`
64
+ : newNamespace;
65
+
66
+ const response = await djClient.addNamespace(fullNamespace);
67
+ if (response.status === 200 || response.status === 201) {
68
+ setIsCreatingChild(false);
69
+ setNewNamespace('');
70
+ setError('');
71
+ window.location.href = `/namespaces/${fullNamespace}`;
72
+ } else {
73
+ setError(response.json?.message || 'Failed to create namespace');
74
+ }
75
+ };
76
+
77
+ const handleCancelAdd = () => {
78
+ setIsCreatingChild(false);
79
+ setNewNamespace('');
80
+ setError('');
81
+ };
82
+
83
+ const handleKeyDown = e => {
84
+ if (e.key === 'Enter') {
85
+ handleAddNamespace(e);
86
+ } else if (e.key === 'Escape') {
87
+ handleCancelAdd();
88
+ }
89
+ };
90
+
91
+ return (
92
+ <>
93
+ <div className="namespace-item" style={{ position: 'relative' }}>
94
+ <div
95
+ className={`select-name ${
96
+ highlight === items.path ? 'select-name-highlight' : ''
97
+ }`}
98
+ onClick={handleClickOnParent}
99
+ onMouseEnter={() => setShowAddButton(true)}
100
+ onMouseLeave={() => setShowAddButton(false)}
101
+ style={{
102
+ display: 'inline-flex',
103
+ alignItems: 'center',
104
+ width: '100%',
105
+ position: 'relative',
106
+ }}
107
+ >
108
+ {items.children && items.children.length > 0 ? (
109
+ <span style={{ marginRight: '4px' }}>
110
+ {!expand ? <CollapsedIcon /> : <ExpandedIcon />}
111
+ </span>
112
+ ) : (
113
+ <span style={{ left: '-18px' }} />
114
+ )}
115
+ <a href={`/namespaces/${items.path}`}>{items.namespace}</a>
116
+ <button
117
+ className="namespace-add-button"
118
+ onClick={e => {
119
+ e.stopPropagation();
120
+ setIsCreatingChild(true);
121
+ setExpand(true);
122
+ }}
123
+ title="Add child namespace"
124
+ style={{
125
+ position: 'absolute',
126
+ right: '0',
127
+ padding: '2px 6px',
128
+ border: 'none',
129
+ background: 'transparent',
130
+ cursor: 'pointer',
131
+ opacity: showAddButton ? 0.6 : 0,
132
+ visibility: showAddButton ? 'visible' : 'hidden',
133
+ display: 'inline-flex',
134
+ alignItems: 'center',
135
+ transition: 'opacity 0.15s ease',
136
+ }}
137
+ >
138
+ <AddItemIcon />
139
+ </button>
140
+ </div>
141
+ </div>
142
+ {(items.children || isCreatingChild) && (
143
+ <div>
144
+ {isCreatingChild && (
145
+ <div
146
+ style={{
147
+ paddingLeft: '1.4rem',
148
+ marginLeft: '1rem',
149
+ borderLeft: '1px solid rgb(218 233 255)',
150
+ marginTop: '5px',
151
+ }}
152
+ >
153
+ <form
154
+ ref={formRef}
155
+ onSubmit={handleAddNamespace}
156
+ style={{
157
+ display: 'flex',
158
+ flexDirection: 'column',
159
+ gap: '4px',
160
+ }}
161
+ >
162
+ <div
163
+ style={{ display: 'flex', gap: '4px', alignItems: 'center' }}
164
+ >
165
+ <input
166
+ ref={inputRef}
167
+ type="text"
168
+ value={newNamespace}
169
+ onChange={e => setNewNamespace(e.target.value)}
170
+ onKeyDown={handleKeyDown}
171
+ placeholder="New namespace name"
172
+ style={{
173
+ padding: '4px 8px',
174
+ fontSize: '0.875rem',
175
+ border: '1px solid #ccc',
176
+ borderRadius: '4px',
177
+ flex: 1,
178
+ }}
179
+ />
180
+ <button
181
+ type="submit"
182
+ style={{
183
+ padding: '4px 8px',
184
+ fontSize: '0.75rem',
185
+ background: '#007bff',
186
+ color: 'white',
187
+ border: 'none',
188
+ borderRadius: '4px',
189
+ cursor: 'pointer',
190
+ margin: '0 1em',
191
+ }}
192
+ >
193
+
194
+ </button>
195
+ </div>
196
+ {error && (
197
+ <span style={{ color: 'red', fontSize: '0.75rem' }}>
198
+ {error}
199
+ </span>
200
+ )}
201
+ </form>
202
+ </div>
203
+ )}
204
+ {items.children &&
205
+ items.children.map((item, index) => (
206
+ <div
207
+ style={{
208
+ paddingLeft: '1.4rem',
209
+ marginLeft: '1rem',
210
+ borderLeft: '1px solid rgb(218 233 255)',
211
+ }}
212
+ key={index}
213
+ >
214
+ <div
215
+ className={`${expand ? '' : 'inactive'}`}
216
+ key={`nested-${index}`}
217
+ >
218
+ <Explorer
219
+ item={item}
220
+ current={highlight}
221
+ isTopLevel={false}
222
+ />
223
+ </div>
224
+ </div>
225
+ ))}
226
+ </div>
227
+ )}
228
+ </>
229
+ );
230
+ };
231
+
232
+ export default Explorer;
@@ -0,0 +1,21 @@
1
+ import { components } from 'react-select';
2
+
3
+ const Control = ({ children, ...props }) => {
4
+ const { label, onLabelClick } = props.selectProps;
5
+ const style = {
6
+ cursor: 'pointer',
7
+ padding: '10px 5px 10px 12px',
8
+ color: 'rgb(112, 110, 115)',
9
+ };
10
+
11
+ return (
12
+ <components.Control {...props}>
13
+ <span onMouseDown={onLabelClick} style={style}>
14
+ {label}
15
+ </span>
16
+ {children}
17
+ </components.Control>
18
+ );
19
+ };
20
+
21
+ export default Control;
@@ -0,0 +1,27 @@
1
+ import Select from 'react-select';
2
+ import Control from './FieldControl';
3
+
4
+ export default function NodeModeSelect({ onChange }) {
5
+ return (
6
+ <span
7
+ className="menu-link"
8
+ style={{ marginLeft: '30px', width: '300px' }}
9
+ data-testid="select-node-mode"
10
+ >
11
+ <Select
12
+ name="node_mode"
13
+ isClearable
14
+ label="Mode"
15
+ components={{ Control }}
16
+ onChange={e => onChange(e)}
17
+ styles={{
18
+ control: styles => ({ ...styles, backgroundColor: 'white' }),
19
+ }}
20
+ options={[
21
+ { value: 'published', label: 'Published' },
22
+ { value: 'draft', label: 'Draft' },
23
+ ]}
24
+ />
25
+ </span>
26
+ );
27
+ }
@@ -0,0 +1,30 @@
1
+ import Select from 'react-select';
2
+ import Control from './FieldControl';
3
+
4
+ export default function NodeTypeSelect({ onChange }) {
5
+ return (
6
+ <span
7
+ className="menu-link"
8
+ style={{ marginLeft: '30px', width: '300px' }}
9
+ data-testid="select-node-type"
10
+ >
11
+ <Select
12
+ name="node_type"
13
+ isClearable
14
+ label="Type"
15
+ components={{ Control }}
16
+ onChange={e => onChange(e)}
17
+ styles={{
18
+ control: styles => ({ ...styles, backgroundColor: 'white' }),
19
+ }}
20
+ options={[
21
+ { value: 'source', label: 'Source' },
22
+ { value: 'transform', label: 'Transform' },
23
+ { value: 'dimension', label: 'Dimension' },
24
+ { value: 'metric', label: 'Metric' },
25
+ { value: 'cube', label: 'Cube' },
26
+ ]}
27
+ />
28
+ </span>
29
+ );
30
+ }
@@ -0,0 +1,44 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+ import Control from './FieldControl';
4
+
5
+ import Select from 'react-select';
6
+
7
+ export default function TagSelect({ onChange }) {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+
10
+ const [retrieved, setRetrieved] = useState(false);
11
+ const [tags, setTags] = useState([]);
12
+
13
+ useEffect(() => {
14
+ const fetchData = async () => {
15
+ const tags = await djClient.listTags();
16
+ setTags(tags);
17
+ setRetrieved(true);
18
+ };
19
+ fetchData().catch(console.error);
20
+ }, [djClient]);
21
+
22
+ return (
23
+ <span
24
+ className="menu-link"
25
+ style={{ marginLeft: '30px', width: '350px' }}
26
+ data-testid="select-tag"
27
+ >
28
+ <Select
29
+ name="tags"
30
+ isClearable
31
+ isMulti
32
+ label="Tags"
33
+ components={{ Control }}
34
+ onChange={e => onChange(e)}
35
+ options={tags?.map(tag => {
36
+ return {
37
+ value: tag.name,
38
+ label: tag.display_name,
39
+ };
40
+ })}
41
+ />
42
+ </span>
43
+ );
44
+ }
@@ -0,0 +1,47 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+ import Control from './FieldControl';
4
+
5
+ import Select from 'react-select';
6
+
7
+ export default function UserSelect({ onChange, currentUser }) {
8
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
9
+ const [retrieved, setRetrieved] = useState(false);
10
+ const [users, setUsers] = useState([]);
11
+
12
+ useEffect(() => {
13
+ const fetchData = async () => {
14
+ const users = await djClient.users();
15
+ setUsers(users);
16
+ setRetrieved(true);
17
+ };
18
+ fetchData().catch(console.error);
19
+ }, [djClient]);
20
+
21
+ return (
22
+ <span
23
+ className="menu-link"
24
+ style={{ marginLeft: '30px', width: '400px' }}
25
+ data-testid="select-user"
26
+ >
27
+ {retrieved ? (
28
+ <Select
29
+ name="edited_by"
30
+ isClearable
31
+ label="Edited By"
32
+ components={{ Control }}
33
+ onChange={e => onChange(e)}
34
+ defaultValue={{
35
+ value: currentUser,
36
+ label: currentUser,
37
+ }}
38
+ options={users?.map(user => {
39
+ return { value: user.username, label: user.username };
40
+ })}
41
+ />
42
+ ) : (
43
+ ''
44
+ )}
45
+ </span>
46
+ );
47
+ }
@@ -0,0 +1,283 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, act } from '@testing-library/react';
3
+ import AddNamespacePopover from '../AddNamespacePopover';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ // Mock window.location.reload
7
+ delete window.location;
8
+ window.location = { reload: jest.fn() };
9
+
10
+ const mockDjClient = {
11
+ DataJunctionAPI: {
12
+ addNamespace: jest.fn(),
13
+ },
14
+ };
15
+
16
+ describe('<AddNamespacePopover />', () => {
17
+ beforeEach(() => {
18
+ jest.clearAllMocks();
19
+ });
20
+
21
+ const defaultProps = {
22
+ namespace: 'default',
23
+ };
24
+
25
+ it('renders the toggle button', () => {
26
+ const { getByLabelText } = render(
27
+ <DJClientContext.Provider value={mockDjClient}>
28
+ <AddNamespacePopover {...defaultProps} />
29
+ </DJClientContext.Provider>,
30
+ );
31
+
32
+ expect(getByLabelText('AddNamespaceTogglePopover')).toBeInTheDocument();
33
+ });
34
+
35
+ it('opens popover when toggle button is clicked', async () => {
36
+ const { getByLabelText, getByRole } = render(
37
+ <DJClientContext.Provider value={mockDjClient}>
38
+ <AddNamespacePopover {...defaultProps} />
39
+ </DJClientContext.Provider>,
40
+ );
41
+
42
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
43
+
44
+ await waitFor(() => {
45
+ expect(
46
+ getByRole('dialog', { name: 'AddNamespacePopover' }),
47
+ ).toBeVisible();
48
+ });
49
+ });
50
+
51
+ it('pre-fills namespace field with parent namespace', async () => {
52
+ const { getByLabelText, getByDisplayValue } = render(
53
+ <DJClientContext.Provider value={mockDjClient}>
54
+ <AddNamespacePopover namespace="parent" />
55
+ </DJClientContext.Provider>,
56
+ );
57
+
58
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
59
+
60
+ await waitFor(() => {
61
+ expect(getByDisplayValue('parent.')).toBeInTheDocument();
62
+ });
63
+ });
64
+
65
+ it('displays namespace input field and save button', async () => {
66
+ const { getByLabelText, getByText } = render(
67
+ <DJClientContext.Provider value={mockDjClient}>
68
+ <AddNamespacePopover {...defaultProps} />
69
+ </DJClientContext.Provider>,
70
+ );
71
+
72
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
73
+
74
+ await waitFor(() => {
75
+ expect(getByLabelText('Namespace')).toBeInTheDocument();
76
+ expect(getByLabelText('SaveNamespace')).toBeInTheDocument();
77
+ expect(getByText('Save')).toBeInTheDocument();
78
+ });
79
+ });
80
+
81
+ it('allows typing in namespace field', async () => {
82
+ const { getByLabelText, getByPlaceholderText } = render(
83
+ <DJClientContext.Provider value={mockDjClient}>
84
+ <AddNamespacePopover {...defaultProps} />
85
+ </DJClientContext.Provider>,
86
+ );
87
+
88
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
89
+
90
+ const namespaceInput = getByPlaceholderText('New namespace');
91
+
92
+ await act(async () => {
93
+ fireEvent.change(namespaceInput, {
94
+ target: { value: 'default.new_namespace' },
95
+ });
96
+ });
97
+
98
+ expect(namespaceInput.value).toBe('default.new_namespace');
99
+ });
100
+
101
+ it('calls addNamespace with correct value on form submission - success', async () => {
102
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
103
+ status: 200,
104
+ json: { message: 'Namespace created' },
105
+ });
106
+
107
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
108
+ <DJClientContext.Provider value={mockDjClient}>
109
+ <AddNamespacePopover {...defaultProps} />
110
+ </DJClientContext.Provider>,
111
+ );
112
+
113
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
114
+
115
+ const namespaceInput = getByPlaceholderText('New namespace');
116
+
117
+ await act(async () => {
118
+ fireEvent.change(namespaceInput, { target: { value: 'default.child' } });
119
+ });
120
+
121
+ const saveButton = getByLabelText('SaveNamespace');
122
+
123
+ await act(async () => {
124
+ fireEvent.click(saveButton);
125
+ });
126
+
127
+ await waitFor(() => {
128
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
129
+ 'default.child',
130
+ );
131
+ expect(getByText('Saved')).toBeInTheDocument();
132
+ });
133
+
134
+ // Should reload page after success
135
+ expect(window.location.reload).toHaveBeenCalled();
136
+ });
137
+
138
+ it('calls addNamespace with correct value on form submission - status 201', async () => {
139
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
140
+ status: 201,
141
+ json: { message: 'Namespace created' },
142
+ });
143
+
144
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
145
+ <DJClientContext.Provider value={mockDjClient}>
146
+ <AddNamespacePopover {...defaultProps} />
147
+ </DJClientContext.Provider>,
148
+ );
149
+
150
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
151
+
152
+ const namespaceInput = getByPlaceholderText('New namespace');
153
+
154
+ await act(async () => {
155
+ fireEvent.change(namespaceInput, {
156
+ target: { value: 'default.another' },
157
+ });
158
+ });
159
+
160
+ const saveButton = getByLabelText('SaveNamespace');
161
+
162
+ await act(async () => {
163
+ fireEvent.click(saveButton);
164
+ });
165
+
166
+ await waitFor(() => {
167
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
168
+ 'default.another',
169
+ );
170
+ expect(getByText('Saved')).toBeInTheDocument();
171
+ });
172
+
173
+ expect(window.location.reload).toHaveBeenCalled();
174
+ });
175
+
176
+ it('displays error message when addNamespace fails', async () => {
177
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
178
+ status: 400,
179
+ json: { message: 'Namespace already exists' },
180
+ });
181
+
182
+ const { getByLabelText, getByPlaceholderText, getByText } = render(
183
+ <DJClientContext.Provider value={mockDjClient}>
184
+ <AddNamespacePopover {...defaultProps} />
185
+ </DJClientContext.Provider>,
186
+ );
187
+
188
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
189
+
190
+ const namespaceInput = getByPlaceholderText('New namespace');
191
+
192
+ await act(async () => {
193
+ fireEvent.change(namespaceInput, {
194
+ target: { value: 'default.duplicate' },
195
+ });
196
+ });
197
+
198
+ const saveButton = getByLabelText('SaveNamespace');
199
+
200
+ await act(async () => {
201
+ fireEvent.click(saveButton);
202
+ });
203
+
204
+ await waitFor(() => {
205
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
206
+ 'default.duplicate',
207
+ );
208
+ expect(getByText('Namespace already exists')).toBeInTheDocument();
209
+ });
210
+
211
+ // Should still reload page even on failure
212
+ expect(window.location.reload).toHaveBeenCalled();
213
+ });
214
+
215
+ it('closes popover when toggle button is clicked again', async () => {
216
+ const { getByLabelText, getByRole, container } = render(
217
+ <DJClientContext.Provider value={mockDjClient}>
218
+ <AddNamespacePopover {...defaultProps} />
219
+ </DJClientContext.Provider>,
220
+ );
221
+
222
+ // Open popover
223
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
224
+
225
+ await waitFor(() => {
226
+ expect(getByRole('dialog')).toBeVisible();
227
+ });
228
+
229
+ // Close popover
230
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
231
+
232
+ // Popover should still exist but be hidden
233
+ const popover = container.querySelector('[role="dialog"]');
234
+ expect(popover).toHaveStyle({ display: 'none' });
235
+ });
236
+
237
+ it('handles nested namespace creation', async () => {
238
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
239
+ status: 200,
240
+ json: { message: 'Namespace created' },
241
+ });
242
+
243
+ const { getByLabelText, getByDisplayValue } = render(
244
+ <DJClientContext.Provider value={mockDjClient}>
245
+ <AddNamespacePopover namespace="parent.child" />
246
+ </DJClientContext.Provider>,
247
+ );
248
+
249
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
250
+
251
+ await waitFor(() => {
252
+ expect(getByDisplayValue('parent.child.')).toBeInTheDocument();
253
+ });
254
+ });
255
+
256
+ it('submits with initial value if not changed', async () => {
257
+ mockDjClient.DataJunctionAPI.addNamespace.mockResolvedValue({
258
+ status: 200,
259
+ json: { message: 'Namespace created' },
260
+ });
261
+
262
+ const { getByLabelText, getByText } = render(
263
+ <DJClientContext.Provider value={mockDjClient}>
264
+ <AddNamespacePopover namespace="test" />
265
+ </DJClientContext.Provider>,
266
+ );
267
+
268
+ fireEvent.click(getByLabelText('AddNamespaceTogglePopover'));
269
+
270
+ const saveButton = getByLabelText('SaveNamespace');
271
+
272
+ await act(async () => {
273
+ fireEvent.click(saveButton);
274
+ });
275
+
276
+ await waitFor(() => {
277
+ expect(mockDjClient.DataJunctionAPI.addNamespace).toHaveBeenCalledWith(
278
+ 'test.',
279
+ );
280
+ expect(getByText('Saved')).toBeInTheDocument();
281
+ });
282
+ });
283
+ });