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,84 @@
1
+ /**
2
+ * Component for selecting node columns based on the current form state
3
+ */
4
+ import { ErrorMessage, useFormikContext } from 'formik';
5
+ import { useContext, useMemo, useState, useEffect } from 'react';
6
+ import DJClientContext from '../../providers/djclient';
7
+ import { FormikSelect } from './FormikSelect';
8
+
9
+ export const ColumnsSelect = ({
10
+ defaultValue,
11
+ fieldName,
12
+ label,
13
+ labelStyle = {},
14
+ isMulti = false,
15
+ isClearable = true,
16
+ }) => {
17
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
18
+
19
+ // Used to pull out current form values for node validation
20
+ const { values } = useFormikContext();
21
+
22
+ // The available columns, determined from validating the node query
23
+ const [availableColumns, setAvailableColumns] = useState([]);
24
+ const selectableOptions = useMemo(() => {
25
+ if (availableColumns && availableColumns.length > 0) {
26
+ return availableColumns;
27
+ }
28
+ }, [availableColumns]);
29
+
30
+ // Fetch columns by validating the latest node query
31
+ const fetchColumns = async () => {
32
+ try {
33
+ const { status, json } = await djClient.validateNode(
34
+ values.type,
35
+ values.name,
36
+ values.display_name,
37
+ values.description,
38
+ values.query,
39
+ );
40
+ if (json?.columns) {
41
+ setAvailableColumns(
42
+ json.columns.map(col => ({ value: col.name, label: col.name })),
43
+ );
44
+ }
45
+ } catch (error) {
46
+ console.error('Error fetching columns:', error);
47
+ }
48
+ };
49
+
50
+ useEffect(() => {
51
+ fetchColumns();
52
+ }, [values.type, values.name, values.query]);
53
+
54
+ return (
55
+ <div className="CubeCreationInput">
56
+ <ErrorMessage name={fieldName} component="span" />
57
+ <label htmlFor={fieldName} style={labelStyle}>
58
+ {label}
59
+ </label>
60
+ <span data-testid={`select-${fieldName}`}>
61
+ <FormikSelect
62
+ className={isMulti ? 'MultiSelectInput' : 'SelectInput'}
63
+ defaultValue={
64
+ isMulti
65
+ ? defaultValue.map(val => {
66
+ return {
67
+ value: val,
68
+ label: val,
69
+ };
70
+ })
71
+ : defaultValue
72
+ ? { value: defaultValue, label: defaultValue }
73
+ : null
74
+ }
75
+ selectOptions={selectableOptions}
76
+ formikFieldName={fieldName}
77
+ onFocus={event => fetchColumns(event)}
78
+ isMulti={isMulti}
79
+ isClearable={isClearable}
80
+ />
81
+ </span>
82
+ </div>
83
+ );
84
+ };
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Custom metadata field component for nodes
3
+ */
4
+ import { ErrorMessage, Field, useFormikContext } from 'formik';
5
+ import CodeMirror from '@uiw/react-codemirror';
6
+ import { langs } from '@uiw/codemirror-extensions-langs';
7
+ import { useState, useEffect } from 'react';
8
+
9
+ export const CustomMetadataField = ({ value }) => {
10
+ const formik = useFormikContext();
11
+ const jsonExt = langs.json();
12
+ const [hasError, setHasError] = useState(false);
13
+
14
+ useEffect(() => {
15
+ if (!value || value === '') {
16
+ setHasError(false);
17
+ return;
18
+ }
19
+
20
+ const stringValue =
21
+ typeof value === 'string' ? value : JSON.stringify(value, null, 2);
22
+
23
+ try {
24
+ JSON.parse(stringValue);
25
+ setHasError(false);
26
+ } catch (err) {
27
+ setHasError(true);
28
+ }
29
+ }, [value]);
30
+
31
+ const formatValue = value => {
32
+ if (value === null || value === undefined) {
33
+ return '';
34
+ }
35
+ if (typeof value === 'string') {
36
+ return value;
37
+ }
38
+ return JSON.stringify(value, null, 2);
39
+ };
40
+
41
+ const updateFormik = val => {
42
+ formik.setFieldValue('custom_metadata', val);
43
+ formik.setFieldTouched('custom_metadata', true);
44
+
45
+ if (!val || val.trim() === '') {
46
+ setHasError(false);
47
+ } else {
48
+ try {
49
+ JSON.parse(val);
50
+ setHasError(false);
51
+ } catch (err) {
52
+ setHasError(true);
53
+ }
54
+ }
55
+ };
56
+
57
+ return (
58
+ <div className="QueryInput NodeCreationInput">
59
+ <details>
60
+ <summary style={{ cursor: 'pointer' }}>
61
+ <label
62
+ style={{
63
+ paddingLeft: '3px',
64
+ display: 'inline-block',
65
+ pointerEvents: 'none',
66
+ }}
67
+ >
68
+ Custom Metadata (JSON)
69
+ </label>
70
+ </summary>
71
+ <ErrorMessage name="custom_metadata" component="span" />
72
+ <Field
73
+ type="textarea"
74
+ style={{ display: 'none' }}
75
+ as="textarea"
76
+ name="custom_metadata"
77
+ id="CustomMetadata"
78
+ validate={value => {
79
+ if (!value || value.trim() === '') {
80
+ return undefined;
81
+ }
82
+ try {
83
+ const parsed = JSON.parse(value);
84
+
85
+ if (
86
+ typeof parsed === 'object' &&
87
+ parsed !== null &&
88
+ !Array.isArray(parsed)
89
+ ) {
90
+ const keys = Object.keys(parsed);
91
+ const originalKeyMatches = value.match(/"([^"]+)"\s*:/g);
92
+ if (
93
+ originalKeyMatches &&
94
+ originalKeyMatches.length > keys.length
95
+ ) {
96
+ return 'Duplicate keys detected';
97
+ }
98
+ }
99
+
100
+ return undefined;
101
+ } catch (err) {
102
+ return 'Invalid JSON format';
103
+ }
104
+ }}
105
+ />
106
+ <div
107
+ role="button"
108
+ tabIndex={0}
109
+ className={`relative flex ${
110
+ hasError ? 'bg-red-900/20' : 'bg-[#282a36]'
111
+ }`}
112
+ style={{
113
+ border: hasError ? '2px solid #ef4444' : 'none',
114
+ borderRadius: '4px',
115
+ boxShadow: hasError ? '0 0 0 1px rgba(239, 68, 68, 0.3)' : 'none',
116
+ }}
117
+ >
118
+ <CodeMirror
119
+ id={'custom_metadata'}
120
+ name={'custom_metadata'}
121
+ extensions={[jsonExt]}
122
+ value={formatValue(value)}
123
+ placeholder={'{\n "key": "value"\n}'}
124
+ options={{
125
+ theme: 'default',
126
+ lineNumbers: true,
127
+ }}
128
+ width="100%"
129
+ height="200px"
130
+ style={{
131
+ margin: '0 0 23px 0',
132
+ flex: 1,
133
+ fontSize: '150%',
134
+ textAlign: 'left',
135
+ }}
136
+ onChange={(value, viewUpdate) => {
137
+ updateFormik(value);
138
+ }}
139
+ />
140
+ </div>
141
+ </details>
142
+ </div>
143
+ );
144
+ };
@@ -0,0 +1,17 @@
1
+ import { ErrorMessage, Field } from 'formik';
2
+
3
+ export const DescriptionField = () => {
4
+ return (
5
+ <div className="DescriptionInput NodeCreationInput">
6
+ <ErrorMessage name="description" component="span" />
7
+ <label htmlFor="Description">Description</label>
8
+ <Field
9
+ type="textarea"
10
+ as="textarea"
11
+ name="description"
12
+ id="Description"
13
+ placeholder="Describe your node"
14
+ />
15
+ </div>
16
+ );
17
+ };
@@ -0,0 +1,16 @@
1
+ import { ErrorMessage, Field } from 'formik';
2
+
3
+ export const DisplayNameField = () => {
4
+ return (
5
+ <div className="DisplayNameInput NodeCreationInput">
6
+ <ErrorMessage name="display_name" component="span" />
7
+ <label htmlFor="displayName">Display Name *</label>
8
+ <Field
9
+ type="text"
10
+ name="display_name"
11
+ id="displayName"
12
+ placeholder="Human readable display name"
13
+ />
14
+ </div>
15
+ );
16
+ };
@@ -0,0 +1,338 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useFormikContext } from 'formik';
3
+ import { ColumnsSelect } from 'datajunction-ui/src/app/pages/AddEditNodePage/ColumnsSelect';
4
+
5
+ const AllocationJoinHeader = () => {
6
+ return (
7
+ <div
8
+ style={{
9
+ display: 'flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'space-between',
12
+ cursor: 'pointer',
13
+ fontSize: '16px',
14
+ fontWeight: 'bold',
15
+ padding: '10px',
16
+ borderRadius: '6px',
17
+ transition: 'background 0.2s',
18
+ }}
19
+ >
20
+ 🔥 Fields to enable automatic allocation joins for use in ABlaze
21
+ </div>
22
+ );
23
+ };
24
+
25
+ const AllocationUnitTypeHeader = ({ unit }) => {
26
+ return (
27
+ <span
28
+ style={{
29
+ backgroundColor: '#fff7ea',
30
+ color: '#333',
31
+ fontWeight: '600',
32
+ width: '100%',
33
+ borderRadius: '8px 8px 0 0',
34
+ padding: '15px',
35
+ display: 'inline-block',
36
+ marginBottom: '10px',
37
+ borderBottom: '1px solid #ffe2b2',
38
+ textTransform: 'capitalize',
39
+ }}
40
+ >
41
+ Allocation Unit Type: {unit.replace('_', ' ')}
42
+ </span>
43
+ );
44
+ };
45
+
46
+ const AddAllocationJoin = ({ onClick }) => {
47
+ return (
48
+ <button
49
+ onClick={onClick}
50
+ style={{
51
+ marginTop: '10px',
52
+ cursor: 'pointer',
53
+ textTransform: 'none',
54
+ background: '#ffe4b2',
55
+ padding: '15px',
56
+ fontWeight: '400',
57
+ fontSize: '16px',
58
+ display: 'none',
59
+ }}
60
+ >
61
+ ➕ Add another allocation join type
62
+ </button>
63
+ );
64
+ };
65
+
66
+ export const ExperimentationExtension = ({
67
+ node,
68
+ action,
69
+ registerSubmitHandler,
70
+ }) => {
71
+ const AllocationUnitType = Object.freeze({
72
+ ACCOUNT_ID: {
73
+ name: 'account_id',
74
+ node: 'common.dimensions.xp.allocation_day',
75
+ },
76
+ VISITOR_DEVICE_ID: { name: 'visitor_device_id', node: null },
77
+ PARTNER_VISITOR_DEVICE_ID: {
78
+ name: 'partner_visitor_device_id',
79
+ node: null,
80
+ },
81
+ PARTNER_ACCOUNT_ID: { name: 'partner_account_id', node: null },
82
+ });
83
+
84
+ const ALLOCATION_LINK_FIELD = 'allocation_links';
85
+
86
+ const { values, setFieldValue } = useFormikContext();
87
+
88
+ const [allocationConfigFields, setAllocationConfigFields] = useState({});
89
+
90
+ // Extracts any Allocation dimension node links
91
+ const getAllocationLinks = node => {
92
+ return Object.values(AllocationUnitType)
93
+ .map(alloc => ({
94
+ ...alloc,
95
+ link:
96
+ node?.dimension_links?.find(
97
+ link => link.dimension.name === alloc.node,
98
+ ) || null,
99
+ }))
100
+ .filter(alloc => alloc.link !== null);
101
+ };
102
+
103
+ const linkAllocations = async (
104
+ nodeName,
105
+ allocationNode,
106
+ allocationUnit,
107
+ eventTimestamp,
108
+ ) => {
109
+ const response = await fetch(
110
+ `${process.env.REACT_APP_DJ_URL}/nodes/${nodeName}/link/${allocationNode}`,
111
+ {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ },
116
+ body: JSON.stringify({
117
+ event_timestamp_column: eventTimestamp,
118
+ account_id_column: allocationUnit,
119
+ }),
120
+ credentials: 'include',
121
+ },
122
+ );
123
+ return { status: response.status, json: await response.json() };
124
+ };
125
+
126
+ const unlinkAllocations = async (nodeName, allocationNode) => {
127
+ const response = await fetch(
128
+ `${process.env.REACT_APP_DJ_URL}/nodes/${nodeName}/link`,
129
+ {
130
+ method: 'DELETE',
131
+ headers: {
132
+ 'Content-Type': 'application/json',
133
+ },
134
+ body: JSON.stringify({ dimension_node: allocationNode }),
135
+ credentials: 'include',
136
+ },
137
+ );
138
+ return { status: response.status, json: await response.json() };
139
+ };
140
+
141
+ const handleSubmit = async (values, { setSubmitting, setStatus }) => {
142
+ for (const [unit, fields] of Object.entries(
143
+ values[ALLOCATION_LINK_FIELD],
144
+ )) {
145
+ const shouldDelete = Object.values(fields).every(
146
+ value => value === null || value === '',
147
+ );
148
+ const linkResponse = shouldDelete
149
+ ? await unlinkAllocations(
150
+ values.name,
151
+ AllocationUnitType[unit.toUpperCase()].node,
152
+ )
153
+ : await linkAllocations(
154
+ values.name,
155
+ AllocationUnitType[unit.toUpperCase()].node,
156
+ fields.allocation_unit,
157
+ fields.event_timestamp,
158
+ );
159
+ if (
160
+ linkResponse.status === 200 ||
161
+ linkResponse.status === 201 ||
162
+ linkResponse.status === 204
163
+ ) {
164
+ const verbiage = shouldDelete ? 'removed the link' : 'linked it';
165
+ setStatus({
166
+ success: (
167
+ <>
168
+ Successfully {action.name}ed {values.type} node{' '}
169
+ <a href={`/nodes/${values.name}`}>{values.name}</a> and {verbiage}{' '}
170
+ with{' '}
171
+ <a href={`/nodes/${AllocationUnitType[unit.toUpperCase()].node}`}>
172
+ Allocations
173
+ </a>
174
+ !
175
+ </>
176
+ ),
177
+ });
178
+ } else {
179
+ if (linkResponse.status === 404) {
180
+ setStatus({
181
+ failure: `The DJ API is not set up to support linking to ${
182
+ AllocationUnitType[unit.toUpperCase()].node
183
+ }.`,
184
+ });
185
+ }
186
+ }
187
+ }
188
+ };
189
+
190
+ // Register this function so the main form can call it on submit
191
+ useEffect(() => {
192
+ if (registerSubmitHandler) {
193
+ if (action.name === 'edit') {
194
+ registerSubmitHandler(handleSubmit, { prepend: false });
195
+ } else {
196
+ registerSubmitHandler(handleSubmit, { prepend: false });
197
+ }
198
+ }
199
+ }, []);
200
+
201
+ const getAllocationUnitColumn = link => {
202
+ return Object.entries(link.link.foreign_keys)
203
+ .find(([_, value]) => value === `${link.node}.${link.name}`)?.[0]
204
+ ?.replace(node.name + '.', '');
205
+ };
206
+
207
+ const getEventTimestampColumn = link => {
208
+ return Object.entries(link.link.foreign_keys)
209
+ .find(([_, value]) => value === null)?.[0]
210
+ ?.replace(node.name + '.', '');
211
+ };
212
+
213
+ const createColumnsSelect = (
214
+ fieldName,
215
+ label,
216
+ placeholder,
217
+ defaultValue = null,
218
+ ) => (
219
+ <ColumnsSelect
220
+ defaultValue={defaultValue}
221
+ fieldName={fieldName}
222
+ label={label}
223
+ labelStyle={{
224
+ textTransform: 'revert',
225
+ fontFamily: 'inherit',
226
+ fontWeight: 'inherit',
227
+ }}
228
+ placeholder={placeholder}
229
+ isClearable={true}
230
+ />
231
+ );
232
+
233
+ useEffect(() => {
234
+ const allocationLinks = getAllocationLinks(node);
235
+ if (
236
+ action.name === 'add' ||
237
+ (node &&
238
+ !Array.isArray(node) &&
239
+ node.length !== 0 &&
240
+ allocationLinks.length === 0)
241
+ ) {
242
+ setAllocationConfigFields(prevFields => ({
243
+ ...prevFields,
244
+ [AllocationUnitType.ACCOUNT_ID.name]: {
245
+ allocationUnit: createColumnsSelect(
246
+ `${ALLOCATION_LINK_FIELD}.${AllocationUnitType.ACCOUNT_ID.name}.allocation_unit`,
247
+ `Column that maps to ${AllocationUnitType.ACCOUNT_ID.name.replace(
248
+ '_',
249
+ ' ',
250
+ )}`,
251
+ 'Choose Account ID Column',
252
+ ),
253
+ eventTimestamp: createColumnsSelect(
254
+ `${ALLOCATION_LINK_FIELD}.${AllocationUnitType.ACCOUNT_ID.name}.event_timestamp`,
255
+ 'Event Timestamp Column',
256
+ 'Choose Event Timestamp Column',
257
+ ),
258
+ },
259
+ }));
260
+ } else {
261
+ for (const link of allocationLinks) {
262
+ const allocationUnit = getAllocationUnitColumn(link);
263
+ const eventTimestamp = getEventTimestampColumn(link);
264
+
265
+ setFieldValue(
266
+ `${ALLOCATION_LINK_FIELD}.${link.name}.allocation_unit`,
267
+ allocationUnit || '',
268
+ false,
269
+ );
270
+ setFieldValue(
271
+ `${ALLOCATION_LINK_FIELD}.${link.name}.event_timestamp`,
272
+ eventTimestamp || '',
273
+ false,
274
+ );
275
+
276
+ setAllocationConfigFields(prevFields => ({
277
+ ...prevFields,
278
+ [link.name]: {
279
+ allocationUnit: createColumnsSelect(
280
+ `${ALLOCATION_LINK_FIELD}.${link.name}.allocation_unit`,
281
+ `Column that maps to ${link.name.replace('_', ' ')}`,
282
+ 'Choose Account ID Column',
283
+ allocationUnit,
284
+ ),
285
+ eventTimestamp: createColumnsSelect(
286
+ `${ALLOCATION_LINK_FIELD}.${link.name}.event_timestamp`,
287
+ 'Event Timestamp Column',
288
+ 'Choose Event Timestamp Column',
289
+ eventTimestamp,
290
+ ),
291
+ },
292
+ }));
293
+ }
294
+ }
295
+ }, [node]);
296
+
297
+ if (
298
+ node?.type !== 'transform' &&
299
+ window.location.pathname.split('/')[2] !== 'transform'
300
+ ) {
301
+ return <></>;
302
+ }
303
+
304
+ return (
305
+ <>
306
+ {
307
+ <div
308
+ style={{
309
+ borderRadius: '8px',
310
+ padding: '10px 10px 20px 10px',
311
+ margin: '32px 0',
312
+ background: '#f9f9f9',
313
+ width: 'max-content',
314
+ }}
315
+ >
316
+ <AllocationJoinHeader />
317
+ {Object.keys(allocationConfigFields).map(unit => (
318
+ <>
319
+ <div
320
+ style={{
321
+ backgroundColor: '#ffffff',
322
+ borderRadius: '8px',
323
+ margin: '15px 25px',
324
+ paddingBottom: '10px',
325
+ border: '1px solid #ffe2b2',
326
+ }}
327
+ >
328
+ <AllocationUnitTypeHeader unit={unit} />
329
+ {Object.values(allocationConfigFields[unit])}
330
+ </div>
331
+ <AddAllocationJoin />
332
+ </>
333
+ ))}
334
+ </div>
335
+ }
336
+ </>
337
+ );
338
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * A React Select component for use in Formik forms.
3
+ */
4
+ import { useField } from 'formik';
5
+ import Select from 'react-select';
6
+
7
+ export const FormikSelect = ({
8
+ selectOptions,
9
+ formikFieldName,
10
+ placeholder,
11
+ defaultValue,
12
+ style,
13
+ className = 'SelectInput',
14
+ isMulti = false,
15
+ isClearable = false,
16
+ onFocus = event => {},
17
+ onChange: customOnChange,
18
+ menuPortalTarget,
19
+ styles,
20
+ ...rest
21
+ }) => {
22
+ // eslint-disable-next-line no-unused-vars
23
+ const [field, _, helpers] = useField(formikFieldName);
24
+ const { setValue } = helpers;
25
+
26
+ // handles both multi-select and single-select cases
27
+ const getValue = options => {
28
+ if (options) {
29
+ return isMulti ? options.map(option => option.value) : options.value;
30
+ } else {
31
+ return isMulti ? [] : '';
32
+ }
33
+ };
34
+
35
+ const handleChange = selected => {
36
+ setValue(getValue(selected));
37
+ if (customOnChange) {
38
+ customOnChange(selected);
39
+ }
40
+ };
41
+
42
+ return (
43
+ <Select
44
+ className={className}
45
+ defaultValue={defaultValue}
46
+ options={selectOptions}
47
+ name={field.name}
48
+ placeholder={placeholder}
49
+ onBlur={field.onBlur}
50
+ onChange={handleChange}
51
+ styles={styles || style}
52
+ isMulti={isMulti}
53
+ isClearable={isClearable}
54
+ onFocus={event => onFocus(event)}
55
+ id={field.name}
56
+ menuPortalTarget={menuPortalTarget}
57
+ {...rest}
58
+ />
59
+ );
60
+ };
61
+
62
+ FormikSelect.defaultProps = {
63
+ placeholder: '',
64
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * A field for the full node name, which is generated based on the node's input
3
+ * namespace and display name.
4
+ */
5
+ import { useField, useFormikContext } from 'formik';
6
+ import { useEffect } from 'react';
7
+
8
+ export const FullNameField = props => {
9
+ const { values, setFieldValue } = useFormikContext();
10
+ const [field, meta] = useField(props);
11
+
12
+ useEffect(() => {
13
+ // Set the value of the node's full name based on its namespace and display name
14
+ if (values.namespace || values.display_name) {
15
+ setFieldValue(
16
+ props.name,
17
+ `${values.namespace}.${values.display_name
18
+ .toLowerCase()
19
+ .replace(/ /g, '_')
20
+ .replace(/[^a-zA-Z0-9_]/g, '')}` || '',
21
+ );
22
+ }
23
+ }, [setFieldValue, props.name, values]);
24
+
25
+ return (
26
+ <>
27
+ <input
28
+ {...props}
29
+ {...field}
30
+ className="FullNameField"
31
+ disabled="disabled"
32
+ id="FullName"
33
+ value={values.name || ''}
34
+ />
35
+ {!!meta.touched && !!meta.error && <div>{meta.error}</div>}
36
+ </>
37
+ );
38
+ };