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,367 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
5
+ import { FormikSelect } from '../AddEditNodePage/FormikSelect';
6
+ import AddItemIcon from '../../icons/AddItemIcon';
7
+ import { displayMessageAfterSubmit } from '../../../utils/form';
8
+ import LoadingIcon from '../../icons/LoadingIcon';
9
+ import CodeMirror from '@uiw/react-codemirror';
10
+ import { langs } from '@uiw/codemirror-extensions-langs';
11
+
12
+ export default function AddComplexDimensionLinkPopover({
13
+ node,
14
+ dimensions,
15
+ existingLink = null,
16
+ isEditMode = false,
17
+ onSubmit,
18
+ }) {
19
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
20
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
21
+ const ref = useRef(null);
22
+
23
+ useEffect(() => {
24
+ const handleClickOutside = event => {
25
+ if (ref.current && !ref.current.contains(event.target)) {
26
+ setPopoverAnchor(false);
27
+ }
28
+ };
29
+ document.addEventListener('click', handleClickOutside, true);
30
+ return () => {
31
+ document.removeEventListener('click', handleClickOutside, true);
32
+ };
33
+ }, [setPopoverAnchor]);
34
+
35
+ const joinTypeOptions = [
36
+ { value: 'left', label: 'LEFT' },
37
+ { value: 'right', label: 'RIGHT' },
38
+ { value: 'inner', label: 'INNER' },
39
+ { value: 'full', label: 'FULL' },
40
+ { value: 'cross', label: 'CROSS' },
41
+ ];
42
+
43
+ const joinCardinalityOptions = [
44
+ { value: 'one_to_one', label: 'ONE TO ONE' },
45
+ { value: 'one_to_many', label: 'ONE TO MANY' },
46
+ { value: 'many_to_one', label: 'MANY TO ONE' },
47
+ { value: 'many_to_many', label: 'MANY TO MANY' },
48
+ ];
49
+
50
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
51
+ try {
52
+ // If editing, remove the old link first
53
+ if (isEditMode && existingLink) {
54
+ await djClient.removeComplexDimensionLink(
55
+ node.name,
56
+ existingLink.dimension.name,
57
+ existingLink.role,
58
+ );
59
+ }
60
+
61
+ // Add the new/updated link
62
+ const response = await djClient.addComplexDimensionLink(
63
+ node.name,
64
+ values.dimensionNode,
65
+ values.joinOn.trim(),
66
+ values.joinType || 'left',
67
+ values.joinCardinality || 'many_to_one',
68
+ values.role?.trim() || null,
69
+ );
70
+
71
+ if (response.status === 200 || response.status === 201) {
72
+ setStatus({
73
+ success: `Complex dimension link ${
74
+ isEditMode ? 'updated' : 'added'
75
+ } successfully!`,
76
+ });
77
+ setTimeout(() => {
78
+ setPopoverAnchor(false);
79
+ window.location.reload();
80
+ }, 1000);
81
+ } else {
82
+ setStatus({
83
+ failure:
84
+ response.json?.message ||
85
+ `Failed to ${isEditMode ? 'update' : 'add'} link`,
86
+ });
87
+ }
88
+ } catch (error) {
89
+ setStatus({ failure: error.message });
90
+ } finally {
91
+ setSubmitting(false);
92
+ }
93
+ };
94
+
95
+ return (
96
+ <>
97
+ {isEditMode ? (
98
+ <button
99
+ onClick={() => setPopoverAnchor(!popoverAnchor)}
100
+ style={{
101
+ padding: '0.25rem 0.5rem',
102
+ fontSize: '0.75rem',
103
+ background: '#007bff',
104
+ color: 'white',
105
+ border: 'none',
106
+ borderRadius: '4px',
107
+ cursor: 'pointer',
108
+ marginRight: '0.5rem',
109
+ }}
110
+ >
111
+ Edit
112
+ </button>
113
+ ) : (
114
+ <button
115
+ className="edit_button"
116
+ aria-label="AddComplexDimensionLinkTogglePopover"
117
+ tabIndex="0"
118
+ onClick={() => setPopoverAnchor(!popoverAnchor)}
119
+ title="Add complex dimension link with custom SQL"
120
+ style={{
121
+ marginLeft: '0.5rem',
122
+ padding: '0.25rem 0.5rem',
123
+ fontSize: '0.875rem',
124
+ }}
125
+ >
126
+ <AddItemIcon />
127
+ </button>
128
+ )}
129
+ {popoverAnchor && (
130
+ <>
131
+ {/* Backdrop overlay */}
132
+ <div
133
+ style={{
134
+ position: 'fixed',
135
+ top: 0,
136
+ left: 0,
137
+ right: 0,
138
+ bottom: 0,
139
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
140
+ zIndex: 1000,
141
+ display: 'flex',
142
+ alignItems: 'center',
143
+ justifyContent: 'center',
144
+ padding: '2rem',
145
+ }}
146
+ onClick={() => setPopoverAnchor(false)}
147
+ >
148
+ {/* Modal */}
149
+ <div
150
+ role="dialog"
151
+ aria-label="AddComplexDimensionLinkPopover"
152
+ ref={ref}
153
+ onClick={e => e.stopPropagation()}
154
+ style={{
155
+ backgroundColor: 'white',
156
+ borderRadius: '8px',
157
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.15)',
158
+ width: '65%',
159
+ maxWidth: '100%',
160
+ maxHeight: '100%',
161
+ overflow: 'visible',
162
+ padding: '1.5rem',
163
+ textTransform: 'none',
164
+ fontWeight: 'normal',
165
+ position: 'relative',
166
+ zIndex: 1000,
167
+ }}
168
+ >
169
+ <div
170
+ style={{ maxHeight: 'calc(85vh - 4rem)', overflowY: 'auto' }}
171
+ >
172
+ <Formik
173
+ initialValues={{
174
+ dimensionNode: existingLink?.dimension.name || '',
175
+ joinType: existingLink?.join_type || 'left',
176
+ joinOn: existingLink?.join_sql || '',
177
+ joinCardinality:
178
+ existingLink?.join_cardinality || 'many_to_one',
179
+ role: existingLink?.role || '',
180
+ }}
181
+ onSubmit={handleSubmit}
182
+ validate={values => {
183
+ const errors = {};
184
+ if (!values.dimensionNode) {
185
+ errors.dimensionNode = 'Required';
186
+ }
187
+ if (!values.joinOn) {
188
+ errors.joinOn = 'Required';
189
+ }
190
+ return errors;
191
+ }}
192
+ >
193
+ {function Render({ isSubmitting, status, values }) {
194
+ return (
195
+ <Form>
196
+ <h3
197
+ style={{
198
+ margin: '0 0 1rem 0',
199
+ fontSize: '1.25rem',
200
+ fontWeight: '600',
201
+ }}
202
+ >
203
+ {isEditMode ? 'Edit' : 'Add'} Complex Dimension Link
204
+ </h3>
205
+ {displayMessageAfterSubmit(status)}
206
+
207
+ <div style={{ marginBottom: '1rem' }}>
208
+ <ErrorMessage name="dimensionNode" component="span" />
209
+ <label htmlFor="dimensionNode">
210
+ Dimension Node *
211
+ </label>
212
+ {isEditMode ? (
213
+ <div
214
+ style={{
215
+ padding: '0.5rem',
216
+ backgroundColor: '#f5f5f5',
217
+ borderRadius: '4px',
218
+ color: '#666',
219
+ }}
220
+ >
221
+ {existingLink?.dimension.name}
222
+ <small
223
+ style={{
224
+ display: 'block',
225
+ marginTop: '0.25rem',
226
+ fontSize: '0.75rem',
227
+ color: '#999',
228
+ }}
229
+ >
230
+ To link a different dimension node, remove this
231
+ link and create a new one
232
+ </small>
233
+ </div>
234
+ ) : (
235
+ <FormikSelect
236
+ selectOptions={dimensions}
237
+ formikFieldName="dimensionNode"
238
+ placeholder="Select dimension"
239
+ />
240
+ )}
241
+ </div>
242
+
243
+ <div
244
+ style={{
245
+ display: 'flex',
246
+ gap: '1rem',
247
+ marginBottom: '1rem',
248
+ }}
249
+ >
250
+ <div style={{ flex: 1 }}>
251
+ <label htmlFor="joinType">Join Type</label>
252
+ <FormikSelect
253
+ selectOptions={joinTypeOptions}
254
+ formikFieldName="joinType"
255
+ placeholder="Select join type"
256
+ defaultValue={
257
+ values.joinType
258
+ ? joinTypeOptions.find(
259
+ opt => opt.value === values.joinType,
260
+ )
261
+ : null
262
+ }
263
+ />
264
+ </div>
265
+ <div style={{ flex: 1 }}>
266
+ <label htmlFor="joinCardinality">
267
+ Join Cardinality
268
+ </label>
269
+ <FormikSelect
270
+ selectOptions={joinCardinalityOptions}
271
+ formikFieldName="joinCardinality"
272
+ placeholder="Select join cardinality"
273
+ defaultValue={
274
+ values.joinCardinality
275
+ ? joinCardinalityOptions.find(
276
+ opt =>
277
+ opt.value === values.joinCardinality,
278
+ )
279
+ : null
280
+ }
281
+ />
282
+ </div>
283
+ </div>
284
+
285
+ <div style={{ marginBottom: '1rem' }}>
286
+ <ErrorMessage name="joinOn" component="span" />
287
+ <label htmlFor="joinOn">Join SQL *</label>
288
+ <small
289
+ style={{ color: '#6c757d', fontSize: '0.75rem' }}
290
+ >
291
+ Specify the join condition
292
+ </small>
293
+ <Field name="joinOn">
294
+ {({ field, form }) => (
295
+ <div
296
+ role="button"
297
+ tabIndex={0}
298
+ className="relative flex bg-[#282a36]"
299
+ >
300
+ <CodeMirror
301
+ id="joinOn"
302
+ name="joinOn"
303
+ extensions={[langs.sql()]}
304
+ value={field.value?.trim()}
305
+ placeholder="e.g., node_table.dimension_id = dimension_table.id"
306
+ width="100%"
307
+ height="150px"
308
+ style={{
309
+ fontSize: '14px',
310
+ textAlign: 'left',
311
+ }}
312
+ onChange={value => {
313
+ form.setFieldValue('joinOn', value);
314
+ }}
315
+ />
316
+ </div>
317
+ )}
318
+ </Field>
319
+ </div>
320
+
321
+ <div style={{ marginBottom: '1rem' }}>
322
+ <label htmlFor="role">Role (Optional)</label>
323
+ <Field
324
+ type="text"
325
+ name="role"
326
+ id="role"
327
+ placeholder="e.g., birth_date, registration_date"
328
+ style={{
329
+ width: '100%',
330
+ padding: '0.5rem',
331
+ }}
332
+ />
333
+ <small
334
+ style={{ color: '#6c757d', fontSize: '0.75rem' }}
335
+ >
336
+ Optional role if linking the same dimension multiple
337
+ times
338
+ </small>
339
+ </div>
340
+
341
+ <button
342
+ className="add_node"
343
+ type="submit"
344
+ aria-label="SaveComplexDimensionLink"
345
+ disabled={isSubmitting}
346
+ style={{ marginTop: '1rem' }}
347
+ >
348
+ {isSubmitting ? (
349
+ <LoadingIcon />
350
+ ) : isEditMode ? (
351
+ 'Save Changes'
352
+ ) : (
353
+ 'Add Link'
354
+ )}
355
+ </button>
356
+ </Form>
357
+ );
358
+ }}
359
+ </Formik>
360
+ </div>
361
+ </div>
362
+ </div>
363
+ </>
364
+ )}
365
+ </>
366
+ );
367
+ }
@@ -0,0 +1,222 @@
1
+ import { useContext, useEffect, useRef, useState } from 'react';
2
+ import * as React from 'react';
3
+ import DJClientContext from '../../providers/djclient';
4
+ import { ErrorMessage, Field, Form, Formik } from 'formik';
5
+ import { displayMessageAfterSubmit, labelize } from '../../../utils/form';
6
+ import { ConfigField } from './MaterializationConfigField';
7
+ import LoadingIcon from '../../icons/LoadingIcon';
8
+
9
+ export default function AddMaterializationPopover({ node, onSubmit }) {
10
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
11
+ const [popoverAnchor, setPopoverAnchor] = useState(false);
12
+ const [options, setOptions] = useState([]);
13
+ const [jobs, setJobs] = useState([]);
14
+
15
+ const timePartitionColumns = node.columns.filter(col => col.partition);
16
+
17
+ const ref = useRef(null);
18
+
19
+ useEffect(() => {
20
+ const fetchData = async () => {
21
+ const options = await djClient.materializationInfo();
22
+ setOptions(options);
23
+ const allowedJobs = options.job_types?.filter(job =>
24
+ job.allowed_node_types.includes(node.type),
25
+ );
26
+ setJobs(allowedJobs);
27
+ };
28
+ fetchData().catch(console.error);
29
+ const handleClickOutside = event => {
30
+ if (ref.current && !ref.current.contains(event.target)) {
31
+ setPopoverAnchor(false);
32
+ }
33
+ };
34
+ document.addEventListener('click', handleClickOutside, true);
35
+ return () => {
36
+ document.removeEventListener('click', handleClickOutside, true);
37
+ };
38
+ }, [djClient, setPopoverAnchor]);
39
+
40
+ const materialize = async (values, setStatus) => {
41
+ const config = {};
42
+ config.spark = values.spark_config;
43
+ config.lookback_window = values.lookback_window;
44
+ if (!values.job_type) {
45
+ values.job_type = 'spark_sql';
46
+ }
47
+ const { status, json } =
48
+ values.job_type === 'druid_cube'
49
+ ? await djClient.materializeCube(
50
+ values.node,
51
+ values.job_type,
52
+ values.strategy,
53
+ values.schedule,
54
+ values.lookback_window,
55
+ )
56
+ : await djClient.materialize(
57
+ values.node,
58
+ values.job_type,
59
+ values.strategy,
60
+ values.schedule,
61
+ config,
62
+ );
63
+ if (status === 200 || status === 201) {
64
+ setStatus({ success: json.message });
65
+ window.location.reload();
66
+ } else {
67
+ setStatus({
68
+ failure: `${json.message}`,
69
+ });
70
+ }
71
+ };
72
+
73
+ const configureMaterialization = async (
74
+ values,
75
+ { setSubmitting, setStatus },
76
+ ) => {
77
+ await materialize(values, setStatus).then(_ => {
78
+ window.scrollTo({ top: 0, left: 0, behavior: 'smooth' });
79
+ setSubmitting(false);
80
+ });
81
+ };
82
+
83
+ return (
84
+ <>
85
+ <button
86
+ className="edit_button"
87
+ aria-label="AddMaterialization"
88
+ tabIndex="0"
89
+ onClick={() => {
90
+ setPopoverAnchor(!popoverAnchor);
91
+ }}
92
+ >
93
+ <span className="add_node">+ Add Materialization</span>
94
+ </button>
95
+ <div
96
+ className="fade modal-backdrop in"
97
+ style={{ display: popoverAnchor === false ? 'none' : 'block' }}
98
+ ></div>
99
+ <div
100
+ className="centerPopover"
101
+ role="dialog"
102
+ aria-label="client-code"
103
+ style={{
104
+ display: popoverAnchor === false ? 'none' : 'block',
105
+ width: '50%',
106
+ }}
107
+ ref={ref}
108
+ >
109
+ <Formik
110
+ initialValues={{
111
+ node: node?.name,
112
+ job_type: node?.type === 'cube' ? 'druid_cube' : 'spark_sql',
113
+ strategy:
114
+ timePartitionColumns.length == 1 ? 'incremental_time' : 'full',
115
+ schedule: '@daily',
116
+ lookback_window: '1 DAY',
117
+ spark_config: {
118
+ 'spark.executor.memory': '16g',
119
+ 'spark.memory.fraction': '0.3',
120
+ },
121
+ }}
122
+ onSubmit={configureMaterialization}
123
+ >
124
+ {function Render({ isSubmitting, status, setFieldValue }) {
125
+ return (
126
+ <Form>
127
+ <h2>Configure Materialization for the Latest Node Version</h2>
128
+ {displayMessageAfterSubmit(status)}
129
+ {node.type === 'cube' ? (
130
+ <span data-testid="job-type">
131
+ <label htmlFor="job_type">Job Type</label>
132
+
133
+ <Field as="select" name="job_type">
134
+ <>
135
+ <option key={'druid_cube'} value={'druid_cube'}>
136
+ Druid
137
+ </option>
138
+ </>
139
+ </Field>
140
+ <br />
141
+ <br />
142
+ </span>
143
+ ) : (
144
+ ''
145
+ )}
146
+ <input
147
+ hidden={true}
148
+ name="node"
149
+ value={node?.name}
150
+ readOnly={true}
151
+ />
152
+ <span data-testid="edit-partition">
153
+ <label htmlFor="strategy">Strategy</label>
154
+ <Field as="select" name="strategy" id="strategy">
155
+ <>
156
+ <option key={'full'} value={'full'}>
157
+ Full
158
+ </option>
159
+ <option
160
+ key={'incremental_time'}
161
+ value={'incremental_time'}
162
+ >
163
+ Incremental Time
164
+ </option>
165
+ </>
166
+ </Field>
167
+ </span>
168
+ <br />
169
+ <br />
170
+ <label htmlFor="schedule">Schedule</label>
171
+ <Field
172
+ type="text"
173
+ name="schedule"
174
+ id="schedule"
175
+ placeholder="Cron"
176
+ default="@daily"
177
+ />
178
+ <br />
179
+ <br />
180
+ <div className="DescriptionInput">
181
+ <ErrorMessage name="description" component="span" />
182
+ <label htmlFor="lookback_window">Lookback Window</label>
183
+ <Field
184
+ type="text"
185
+ name="lookback_window"
186
+ id="lookback_window"
187
+ placeholder="1 DAY"
188
+ default="1 DAY"
189
+ />
190
+ </div>
191
+ <br />
192
+ <ConfigField
193
+ value={{
194
+ 'spark.executor.memory': '16g',
195
+ 'spark.memory.fraction': '0.3',
196
+ }}
197
+ />
198
+ <div
199
+ style={{
200
+ display: 'flex',
201
+ justifyContent: 'flex-end',
202
+ marginTop: '20px',
203
+ }}
204
+ >
205
+ <button
206
+ className="add_node"
207
+ type="submit"
208
+ aria-label="SaveEditColumn"
209
+ aria-hidden="false"
210
+ disabled={isSubmitting}
211
+ >
212
+ {isSubmitting ? <LoadingIcon /> : 'Save'}
213
+ </button>
214
+ </div>
215
+ </Form>
216
+ );
217
+ }}
218
+ </Formik>
219
+ </div>
220
+ </>
221
+ );
222
+ }
@@ -0,0 +1,67 @@
1
+ import TableIcon from '../../icons/TableIcon';
2
+
3
+ export default function AvailabilityStateBlock({ availability }) {
4
+ return (
5
+ <table
6
+ className="card-inner-table table"
7
+ aria-label="Availability"
8
+ aria-hidden="false"
9
+ style={{ marginBottom: '20px' }}
10
+ >
11
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
12
+ <tr>
13
+ <th className="text-start">Output Dataset</th>
14
+ <th>Valid Through</th>
15
+ <th>Partitions</th>
16
+ <th>Links</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <tr>
21
+ <td>
22
+ <div className={`table__full`} key={availability.table}>
23
+ <div className="table__header">
24
+ <TableIcon />{' '}
25
+ <span className={`entity-info`}>
26
+ {availability.catalog + '.' + availability.schema_}
27
+ </span>
28
+ </div>
29
+ <div className={`table__body upstream_tables`}>
30
+ <a href={availability.url}>{availability.table}</a>
31
+ </div>
32
+ </div>
33
+ </td>
34
+ <td>{new Date(availability.valid_through_ts).toISOString()}</td>
35
+ <td>
36
+ <span
37
+ className={`badge partition_value`}
38
+ style={{ fontSize: '100%' }}
39
+ >
40
+ <span className={`badge partition_value_highlight`}>
41
+ {availability.min_temporal_partition?.join(', ') || 'N/A'}
42
+ </span>
43
+ to
44
+ <span className={`badge partition_value_highlight`}>
45
+ {availability.max_temporal_partition?.join(', ') || 'N/A'}
46
+ </span>
47
+ </span>
48
+ </td>
49
+ <td>
50
+ {availability.links &&
51
+ Object.keys(availability.links).length > 0 ? (
52
+ Object.entries(availability.links).map(([key, value]) => (
53
+ <div key={key}>
54
+ <a href={value} target="_blank" rel="noreferrer">
55
+ {key}
56
+ </a>
57
+ </div>
58
+ ))
59
+ ) : (
60
+ <></>
61
+ )}
62
+ </td>
63
+ </tr>
64
+ </tbody>
65
+ </table>
66
+ );
67
+ }