datajunction-ui 0.0.1-rc.9 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (205) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/dj-logo.svg +10 -0
  5. package/package.json +53 -14
  6. package/public/favicon.ico +0 -0
  7. package/public/index.html +1 -1
  8. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  9. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  10. package/src/app/components/AddNodeDropdown.jsx +44 -0
  11. package/src/app/components/ListGroupItem.jsx +9 -1
  12. package/src/app/components/NamespaceHeader.jsx +4 -13
  13. package/src/app/components/NodeListActions.jsx +69 -0
  14. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  15. package/src/app/components/QueryInfo.jsx +172 -0
  16. package/src/app/components/Search.jsx +94 -0
  17. package/src/app/components/Tab.jsx +8 -1
  18. package/src/app/components/ToggleSwitch.jsx +20 -0
  19. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  20. package/src/app/components/__tests__/QueryInfo.test.jsx +55 -0
  21. package/src/app/components/__tests__/Search.test.jsx +63 -0
  22. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  23. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  24. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  25. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  26. package/src/app/components/djgraph/Collapse.jsx +47 -0
  27. package/src/app/components/djgraph/DJNode.jsx +61 -83
  28. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  29. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  30. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  31. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  32. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  33. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  34. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  35. package/src/app/components/forms/Action.jsx +8 -0
  36. package/src/app/components/forms/NodeNameField.jsx +64 -0
  37. package/src/app/components/search.css +17 -0
  38. package/src/app/constants.js +2 -0
  39. package/src/app/icons/AddItemIcon.jsx +16 -0
  40. package/src/app/icons/AlertIcon.jsx +33 -0
  41. package/src/app/icons/CollapsedIcon.jsx +15 -0
  42. package/src/app/icons/CommitIcon.jsx +45 -0
  43. package/src/app/icons/DJLogo.jsx +36 -0
  44. package/src/app/icons/DeleteIcon.jsx +21 -0
  45. package/src/app/icons/DiffIcon.jsx +63 -0
  46. package/src/app/icons/EditIcon.jsx +18 -0
  47. package/src/app/icons/ExpandedIcon.jsx +15 -0
  48. package/src/app/icons/EyeIcon.jsx +20 -0
  49. package/src/app/icons/FilterIcon.jsx +7 -0
  50. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  51. package/src/app/icons/InvalidIcon.jsx +16 -0
  52. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  53. package/src/app/icons/LoadingIcon.jsx +14 -0
  54. package/src/app/icons/NodeIcon.jsx +49 -0
  55. package/src/app/icons/PythonIcon.jsx +14 -0
  56. package/src/app/icons/TableIcon.jsx +14 -0
  57. package/src/app/icons/ValidIcon.jsx +16 -0
  58. package/src/app/index.tsx +118 -37
  59. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  60. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  61. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  62. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  63. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +51 -0
  64. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  65. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  66. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  67. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  68. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  69. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  70. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  71. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  72. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  73. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  74. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  75. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +109 -0
  76. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +287 -0
  77. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  78. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  79. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  80. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  81. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  82. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  83. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  84. package/src/app/pages/AddEditNodePage/index.jsx +506 -0
  85. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  86. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  87. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  88. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  89. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  90. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  91. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  92. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  93. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  94. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  95. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  96. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  97. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  98. package/src/app/pages/LoginPage/index.jsx +17 -0
  99. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  100. package/src/app/pages/NamespacePage/Explorer.jsx +61 -0
  101. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  102. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  103. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  104. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  105. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +297 -0
  106. package/src/app/pages/NamespacePage/index.jsx +319 -42
  107. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  108. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  109. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  110. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  111. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  112. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  113. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  114. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  115. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  116. package/src/app/pages/NodePage/NodeColumnTab.jsx +256 -30
  117. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  118. package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
  119. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  120. package/src/app/pages/NodePage/NodeInfoTab.jsx +325 -49
  121. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  122. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  123. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  124. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  125. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  126. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  127. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  128. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  129. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  130. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  131. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  132. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  133. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  134. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  135. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  136. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +148 -0
  137. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +161 -0
  138. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  139. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  140. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  141. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  142. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  143. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +872 -0
  144. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  145. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  146. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  147. package/src/app/pages/NodePage/index.jsx +190 -44
  148. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  149. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  150. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  151. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  152. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  153. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  154. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  155. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  156. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  157. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  158. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  159. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  160. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  161. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  162. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  163. package/src/app/pages/OverviewPage/index.jsx +22 -0
  164. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  165. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +110 -0
  166. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  167. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  168. package/src/app/pages/Root/__tests__/index.test.jsx +79 -0
  169. package/src/app/pages/Root/index.tsx +84 -6
  170. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  171. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  172. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  173. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  174. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  175. package/src/app/pages/TagPage/index.jsx +79 -0
  176. package/src/app/services/DJService.js +1314 -21
  177. package/src/app/services/__tests__/DJService.test.jsx +1559 -0
  178. package/src/index.tsx +1 -0
  179. package/src/mocks/mockNodes.jsx +1474 -0
  180. package/src/setupTests.ts +31 -1
  181. package/src/styles/dag.css +117 -5
  182. package/src/styles/index.css +1027 -30
  183. package/src/styles/loading.css +34 -0
  184. package/src/styles/login.css +81 -0
  185. package/src/styles/node-creation.scss +276 -0
  186. package/src/styles/node-list.css +4 -0
  187. package/src/styles/overview.css +72 -0
  188. package/src/styles/sorted-table.css +15 -0
  189. package/src/styles/styles.scss +44 -0
  190. package/src/styles/styles.scss.d.ts +9 -0
  191. package/src/utils/form.jsx +23 -0
  192. package/webpack.config.js +16 -6
  193. package/.babelrc +0 -4
  194. package/.env.local +0 -4
  195. package/.env.production +0 -1
  196. package/.github/pull_request_template.md +0 -11
  197. package/.github/workflows/ci.yml +0 -33
  198. package/.vscode/extensions.json +0 -7
  199. package/.vscode/launch.json +0 -15
  200. package/.vscode/settings.json +0 -25
  201. package/Dockerfile +0 -7
  202. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  203. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  204. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  205. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -1,81 +1,58 @@
1
- import React, { memo } from 'react';
1
+ import { memo } from 'react';
2
2
  import { Handle, Position } from 'reactflow';
3
-
4
- function renderBasedOnDJNodeType(param) {
5
- switch (param) {
6
- case 'source':
7
- return { backgroundColor: '#7EB46150', color: '#7EB461' };
8
- case 'transform':
9
- return { backgroundColor: '#6DAAA750', color: '#6DAAA7' };
10
- case 'dimension':
11
- return { backgroundColor: '#CF7D2950', color: '#CF7D29' };
12
- case 'metric':
13
- return { backgroundColor: '#A27E8650', color: '#A27E86' };
14
- case 'cube':
15
- return { backgroundColor: '#C2180750', color: '#C21807' };
16
- default:
17
- return {};
18
- }
19
- }
3
+ import Collapse from './Collapse';
20
4
 
21
5
  function capitalize(string) {
22
6
  return string.charAt(0).toUpperCase() + string.slice(1);
23
7
  }
24
8
 
25
- const Collapse = ({ collapsed, text, children }) => {
26
- const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
27
-
28
- return (
29
- <>
30
- <div className="collapse">
31
- <button
32
- className="collapse-button"
33
- onClick={() => setIsCollapsed(!isCollapsed)}
34
- >
35
- {isCollapsed ? '\u25B6 Show' : '\u25BC Hide'} {text}
36
- </button>
37
- <div
38
- className={`collapse-content ${
39
- isCollapsed ? 'collapsed' : 'expanded'
40
- }`}
41
- aria-expanded={isCollapsed}
42
- >
43
- {children}
44
- </div>
45
- </div>
46
- </>
47
- );
48
- };
49
-
50
9
  export function DJNode({ id, data }) {
51
- const columnsRenderer = data =>
52
- data.column_names.map(col => (
53
- <tr>
54
- <td>
55
- {data.primary_key.includes(col.name) ? (
56
- <b>{col.name} (PK)</b>
57
- ) : (
58
- <>{col.name}</>
59
- )}
60
- </td>
61
- <td style={{ textAlign: 'right' }}>{col.type}</td>
62
- </tr>
63
- ));
64
- // const dimensionsRenderer = data =>
65
- // data.dimensions.map(dim => (
66
- // <tr>
67
- // <td>{dim}</td>
68
- // </tr>
69
- // ));
10
+ const handleWrapperStyle = {
11
+ display: 'flex',
12
+ position: 'absolute',
13
+ height: '100%',
14
+ flexDirection: 'column',
15
+ top: '50%',
16
+ justifyContent: 'space-between',
17
+ };
18
+ const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
70
19
 
20
+ const handleStyle = {
21
+ width: '12px',
22
+ height: '12px',
23
+ borderRadius: '12px',
24
+ background: 'transparent',
25
+ border: '4px solid transparent',
26
+ cursor: 'pointer',
27
+ position: 'absolute',
28
+ top: '0px',
29
+ left: 0,
30
+ };
31
+ const handleStyleLeft = percentage => {
32
+ return {
33
+ ...handleStyle,
34
+ ...{
35
+ transform: 'translate(-' + percentage + '%, -50%)',
36
+ },
37
+ };
38
+ };
39
+ const highlightNodeClass =
40
+ data.is_current === true ? ' dj-node_highlight' : '';
71
41
  return (
72
42
  <>
73
- <Handle
74
- type="target"
75
- position={Position.Left}
76
- style={{ backgroundColor: '#ccc' }}
77
- />
78
- <div className="dj-node__full" style={renderBasedOnDJNodeType(data.type)}>
43
+ <div
44
+ className={'dj-node__full node_type__' + data.type + highlightNodeClass}
45
+ key={data.name}
46
+ style={{ width: '450px' }}
47
+ >
48
+ <div style={handleWrapperStyle}>
49
+ <Handle
50
+ type="target"
51
+ id={data.name}
52
+ position={Position.Left}
53
+ style={handleStyleLeft(100)}
54
+ />
55
+ </div>
79
56
  <div className="dj-node__header">
80
57
  <div className="serif">
81
58
  {data.name
@@ -85,25 +62,26 @@ export function DJNode({ id, data }) {
85
62
  </div>
86
63
  </div>
87
64
  <div className="dj-node__body">
88
- <b>{capitalize(data.type)}</b>:{' '}
89
- {data.type === 'source' ? data.table : data.display_name}
65
+ <b>{capitalize(data.type)}</b>
66
+ <br />{' '}
67
+ <a href={`/nodes/${data.name}`}>
68
+ {data.type === 'source' ? data.table : data.display_name}
69
+ </a>
90
70
  <Collapse
91
- collapsed={true}
71
+ collapsed={data.is_current && data.type != 'metric' ? false : true}
92
72
  text={data.type !== 'metric' ? 'columns' : 'dimensions'}
93
- >
94
- <div className="dj-node__metadata">
95
- {
96
- data.type !== 'metric' ? columnsRenderer(data) : '' // dimensionsRenderer(data)
97
- }
98
- </div>
99
- </Collapse>
73
+ data={data}
74
+ />
75
+ </div>
76
+ <div style={handleWrapperStyleRight}>
77
+ <Handle
78
+ type="source"
79
+ id={data.name}
80
+ position={Position.Right}
81
+ style={handleStyleLeft(90)}
82
+ />
100
83
  </div>
101
84
  </div>
102
- <Handle
103
- type="source"
104
- position={Position.Right}
105
- style={{ backgroundColor: '#ccc' }}
106
- />
107
85
  </>
108
86
  );
109
87
  }
@@ -0,0 +1,75 @@
1
+ import { Handle } from 'reactflow';
2
+ import React from 'react';
3
+
4
+ export function DJNodeColumns({ data, limit }) {
5
+ const handleWrapperStyle = {
6
+ display: 'flex',
7
+ position: 'absolute',
8
+ height: '100%',
9
+ flexDirection: 'column',
10
+ top: '50%',
11
+ justifyContent: 'space-between',
12
+ };
13
+ const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
14
+
15
+ const handleStyle = {
16
+ width: '12px',
17
+ height: '12px',
18
+ borderRadius: '12px',
19
+ background: 'transparent',
20
+ border: '4px solid transparent',
21
+ cursor: 'pointer',
22
+ position: 'absolute',
23
+ top: '0px',
24
+ left: 0,
25
+ };
26
+ const handleStyleLeft = percentage => {
27
+ return {
28
+ ...handleStyle,
29
+ ...{
30
+ transform: 'translate(-' + percentage + '%, -50%)',
31
+ },
32
+ };
33
+ };
34
+ return data.column_names.slice(0, limit).map(col => (
35
+ <div
36
+ className={
37
+ 'custom-node-subheader node_type__' +
38
+ data.type +
39
+ (col.order <= 0 ? ' custom-node-emphasis' : '')
40
+ }
41
+ key={`${data.name}.${col.name}`}
42
+ >
43
+ <div style={handleWrapperStyle}>
44
+ <Handle
45
+ type="target"
46
+ position="left"
47
+ id={data.name + '.' + col.name}
48
+ style={handleStyleLeft(100)}
49
+ />
50
+ </div>
51
+ <div
52
+ className="custom-node-port"
53
+ id={data.name + '.' + col.name}
54
+ key={'i-' + data.name + '.' + col.name}
55
+ >
56
+ {data.primary_key.includes(col.name) ? (
57
+ <b>{col.name} (PK)</b>
58
+ ) : (
59
+ <>{col.name}</>
60
+ )}
61
+ <span style={{ marginLeft: '0.25rem' }} className={'badge'}>
62
+ {col.type}
63
+ </span>
64
+ </div>
65
+ <div style={handleWrapperStyleRight}>
66
+ <Handle
67
+ type="source"
68
+ position="right"
69
+ id={data.name + '.' + col.name}
70
+ style={handleStyle}
71
+ />
72
+ </div>
73
+ </div>
74
+ ));
75
+ }
@@ -0,0 +1,75 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+
4
+ export function DJNodeDimensions(data) {
5
+ const [dimensions, setDimensions] = useState([]);
6
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
7
+ useEffect(() => {
8
+ if (data.type === 'metric') {
9
+ async function getDimensions() {
10
+ try {
11
+ const metricData = await djClient.metric(data.name);
12
+ setDimensions(metricData.dimensions);
13
+ } catch (err) {
14
+ console.log(err);
15
+ }
16
+ }
17
+ getDimensions();
18
+ }
19
+ }, [data, djClient]);
20
+ const dimensionsToObject = dimensions => {
21
+ return dimensions.map(dim => {
22
+ const [attribute, ...nodeName] = dim.name.split('.').reverse();
23
+ return {
24
+ dimension: nodeName.reverse().join('.'),
25
+ path: dim.path,
26
+ column: attribute,
27
+ };
28
+ });
29
+ };
30
+ const groupedDimensions = dims =>
31
+ dims.reduce((acc, current) => {
32
+ const dimKey = current.dimension + ' via ' + current.path.slice(-1);
33
+ acc[dimKey] = acc[dimKey] || {
34
+ dimension: current.dimension,
35
+ path: current.path.slice(-1),
36
+ columns: [],
37
+ };
38
+ acc[dimKey].columns.push(current.column);
39
+ return acc;
40
+ }, {});
41
+ const dimensionsRenderer = grouped =>
42
+ Object.entries(grouped).map(([dimKey, dimValue]) => {
43
+ if (Array.isArray(dimValue.columns)) {
44
+ const attributes = dimValue.columns.map(col => {
45
+ return (
46
+ <span className={'badge white_badge'} key={`attr-${col}`}>
47
+ {col}
48
+ </span>
49
+ );
50
+ });
51
+ return (
52
+ <div
53
+ className={'custom-node-subheader node_type__' + data.type}
54
+ key={`dim-${dimValue.path}-${dimValue.dimension}`}
55
+ >
56
+ <div className="custom-node-port">
57
+ <a href={`/nodes/${dimValue.dimension}`}>{dimValue.dimension}</a>{' '}
58
+ <div className={'badge node_type__metric text-black'}>
59
+ {dimValue.path}
60
+ </div>
61
+ </div>
62
+ <div className={'dimension_attributes'}>{attributes}</div>
63
+ </div>
64
+ );
65
+ }
66
+ return <></>;
67
+ });
68
+ return (
69
+ <>
70
+ {dimensions.length <= 0
71
+ ? ''
72
+ : dimensionsRenderer(groupedDimensions(dimensionsToObject(dimensions)))}
73
+ </>
74
+ );
75
+ }
@@ -0,0 +1,106 @@
1
+ import React, { useCallback, useEffect, useMemo } from 'react';
2
+ import ReactFlow, {
3
+ addEdge,
4
+ MiniMap,
5
+ Controls,
6
+ Background,
7
+ useNodesState,
8
+ useEdgesState,
9
+ } from 'reactflow';
10
+
11
+ import '../../../styles/dag.css';
12
+ import 'reactflow/dist/style.css';
13
+ import DJNode from '../../components/djgraph/DJNode';
14
+ import dagre from 'dagre';
15
+
16
+ const getLayoutedElements = (
17
+ nodes,
18
+ edges,
19
+ direction = 'LR',
20
+ nodeWidth = 600,
21
+ ) => {
22
+ const dagreGraph = new dagre.graphlib.Graph();
23
+ dagreGraph.setDefaultEdgeLabel(() => ({}));
24
+
25
+ const isHorizontal = direction === 'TB';
26
+ dagreGraph.setGraph({
27
+ rankdir: direction,
28
+ nodesep: 40,
29
+ ranksep: 10,
30
+ ranker: 'longest-path',
31
+ });
32
+ const nodeHeightTracker = {};
33
+
34
+ nodes.forEach(node => {
35
+ const minColumnsLength = node.data.column_names.filter(
36
+ col => col.order > 0,
37
+ ).length;
38
+ nodeHeightTracker[node.id] = Math.min(minColumnsLength, 5) * 40 + 250;
39
+ dagreGraph.setNode(node.id, {
40
+ width: nodeWidth,
41
+ height: nodeHeightTracker[node.id],
42
+ });
43
+ });
44
+
45
+ edges.forEach(edge => {
46
+ dagreGraph.setEdge(edge.source, edge.target);
47
+ });
48
+
49
+ dagre.layout(dagreGraph);
50
+
51
+ nodes.forEach(node => {
52
+ const nodeWithPosition = dagreGraph.node(node.id);
53
+ node.targetPosition = isHorizontal ? 'left' : 'top';
54
+ node.sourcePosition = isHorizontal ? 'right' : 'bottom';
55
+ node.position = {
56
+ x: nodeWithPosition.x - nodeWidth / 2,
57
+ y: nodeWithPosition.y - nodeHeightTracker[node.id] / 3,
58
+ };
59
+ node.width = nodeWidth;
60
+ node.height = nodeHeightTracker[node.id];
61
+ return node;
62
+ });
63
+
64
+ return { nodes: nodes, edges: edges };
65
+ };
66
+
67
+ const LayoutFlow = (djNode, saveGraph) => {
68
+ const nodeTypes = useMemo(() => ({ DJNode: DJNode }), []);
69
+
70
+ // These are used internally by ReactFlow (to update the nodes on the ReactFlow pane)
71
+ const [nodes, setNodes, onNodesChange] = useNodesState([]);
72
+ const [edges, setEdges, onEdgesChange] = useEdgesState([]);
73
+
74
+ const minimapStyle = {
75
+ height: 100,
76
+ width: 150,
77
+ };
78
+
79
+ useEffect(() => {
80
+ saveGraph(getLayoutedElements, setNodes, setEdges).catch(console.error);
81
+ }, [djNode]);
82
+
83
+ const onConnect = useCallback(
84
+ params => setEdges(eds => addEdge(params, eds)),
85
+ [setEdges],
86
+ );
87
+ return (
88
+ <div style={{ height: '800px' }}>
89
+ <ReactFlow
90
+ nodes={nodes}
91
+ edges={edges}
92
+ nodeTypes={nodeTypes}
93
+ onNodesChange={onNodesChange}
94
+ onEdgesChange={onEdgesChange}
95
+ onConnect={onConnect}
96
+ snapToGrid={true}
97
+ fitView
98
+ >
99
+ <MiniMap style={minimapStyle} zoomable pannable />
100
+ <Controls />
101
+ <Background color="#aaa" gap={16} />
102
+ </ReactFlow>
103
+ </div>
104
+ );
105
+ };
106
+ export default LayoutFlow;
@@ -0,0 +1,51 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, screen, waitFor } from '@testing-library/react';
3
+ import Collapse from '../Collapse';
4
+ import { mocks } from '../../../../mocks/mockNodes';
5
+ import { ReactFlowProvider } from 'reactflow';
6
+
7
+ jest.mock('../DJNodeDimensions', () => ({
8
+ DJNodeDimensions: jest.fn(data => <div>DJNodeDimensions content</div>),
9
+ }));
10
+
11
+ describe('<Collapse />', () => {
12
+ const defaultProps = {
13
+ collapsed: true,
14
+ text: 'Dimensions',
15
+ data: mocks.mockMetricNode,
16
+ };
17
+
18
+ it('renders without crashing', () => {
19
+ render(<Collapse {...defaultProps} />);
20
+ });
21
+
22
+ it('renders toggle for metric type', () => {
23
+ const { getByText } = render(
24
+ <Collapse {...defaultProps} data={mocks.mockMetricNode} />,
25
+ );
26
+ const button = screen.getByText('▶ Show Dimensions');
27
+ fireEvent.click(button);
28
+ expect(getByText('▼ Hide Dimensions')).toBeInTheDocument();
29
+ });
30
+
31
+ it('toggles More/Less button text for non-metric type on click', () => {
32
+ defaultProps.text = 'Columns';
33
+ const { getByText } = render(
34
+ <ReactFlowProvider>
35
+ <Collapse
36
+ {...defaultProps}
37
+ data={{
38
+ type: 'transform',
39
+ column_names: Array(11).fill(idx => {
40
+ return `column-${idx}`;
41
+ }),
42
+ primary_key: [],
43
+ }}
44
+ />
45
+ </ReactFlowProvider>,
46
+ );
47
+ const button = getByText('▶ More Columns');
48
+ fireEvent.click(button);
49
+ expect(getByText('▼ Less Columns')).toBeInTheDocument();
50
+ });
51
+ });
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { DJNodeColumns } from '../DJNodeColumns';
4
+ import { ReactFlowProvider } from 'reactflow';
5
+
6
+ describe('<DJNodeColumns />', () => {
7
+ const defaultProps = {
8
+ data: {
9
+ name: 'TestName',
10
+ type: 'metric',
11
+ column_names: [
12
+ { name: 'col1', type: 'int' },
13
+ { name: 'col2', type: 'string' },
14
+ { name: 'col3', type: 'float' },
15
+ ],
16
+ primary_key: ['col1'],
17
+ },
18
+ limit: 10,
19
+ };
20
+
21
+ const domTestingLib = require('@testing-library/dom');
22
+ const { queryHelpers } = domTestingLib;
23
+
24
+ const queryByAttribute = attribute =>
25
+ queryHelpers.queryAllByAttribute.bind(null, attribute);
26
+
27
+ function getByAttribute(container, id, attribute, ...rest) {
28
+ const result = queryByAttribute(attribute)(container, id, ...rest);
29
+ return result[0];
30
+ }
31
+
32
+ function DJNodeColumnsWithProvider(props) {
33
+ return (
34
+ <ReactFlowProvider>
35
+ <DJNodeColumns {...props} />
36
+ </ReactFlowProvider>
37
+ );
38
+ }
39
+
40
+ it('renders without crashing', () => {
41
+ render(<DJNodeColumnsWithProvider {...defaultProps} />);
42
+ });
43
+
44
+ it('renders columns correctly', () => {
45
+ const { container } = render(
46
+ <DJNodeColumnsWithProvider {...defaultProps} />,
47
+ );
48
+
49
+ // renders column names
50
+ defaultProps.data.column_names.forEach(col => {
51
+ expect(screen.getByText(col.name, { exact: false })).toBeInTheDocument();
52
+ });
53
+
54
+ // appends (PK) to primary key column name
55
+ expect(screen.getByText('col1 (PK)')).toBeInTheDocument();
56
+
57
+ // renders column type badge correctly
58
+ defaultProps.data.column_names.forEach(col => {
59
+ expect(screen.getByText(col.type)).toBeInTheDocument();
60
+ });
61
+
62
+ // renders handles correctly
63
+ defaultProps.data.column_names.forEach(col => {
64
+ expect(
65
+ getByAttribute(
66
+ container,
67
+ defaultProps.data.name + '.' + col.name,
68
+ 'data-handleid',
69
+ ),
70
+ ).toBeInTheDocument();
71
+ });
72
+ });
73
+
74
+ it('renders limited columns based on the limit prop', () => {
75
+ const limitedProps = {
76
+ ...defaultProps,
77
+ limit: 2,
78
+ };
79
+
80
+ render(<DJNodeColumnsWithProvider {...limitedProps} />);
81
+ expect(screen.queryByText('col3')).not.toBeInTheDocument();
82
+ });
83
+ });
@@ -0,0 +1,118 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { DJNodeDimensions } from '../DJNodeDimensions';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ const mockMetric = jest.fn();
7
+ describe('<DJNodeDimensions />', () => {
8
+ const defaultProps = {
9
+ type: 'metric',
10
+ name: 'TestMetric',
11
+ };
12
+ const mockDJClient = () => {
13
+ return {
14
+ DataJunctionAPI: {
15
+ metric: mockMetric,
16
+ },
17
+ };
18
+ };
19
+
20
+ const DJNodeDimensionsWithContext = (djClient, props) => {
21
+ return (
22
+ <DJClientContext.Provider value={djClient}>
23
+ <DJNodeDimensions {...props} />
24
+ </DJClientContext.Provider>
25
+ );
26
+ };
27
+
28
+ beforeEach(() => {
29
+ // Reset the mock before each test
30
+ mockMetric.mockReset();
31
+ });
32
+
33
+ it('fetches dimensions for metric type', async () => {
34
+ mockMetric.mockResolvedValueOnce({
35
+ dimensions: [{ name: 'test.dimension' }],
36
+ });
37
+ const djClient = mockDJClient();
38
+
39
+ render(
40
+ <DJClientContext.Provider value={djClient}>
41
+ <DJNodeDimensions {...defaultProps} />
42
+ </DJClientContext.Provider>,
43
+ );
44
+ waitFor(() => {
45
+ expect(mockMetric).toHaveBeenCalledWith(defaultProps.name);
46
+ });
47
+ });
48
+
49
+ it('renders dimensions correctly after processing', async () => {
50
+ const testDimensions = [
51
+ {
52
+ name: 'default.us_state.state_name',
53
+ type: 'string',
54
+ path: [
55
+ 'default.repair_order_details.repair_order_id',
56
+ 'default.repair_order.hard_hat_id',
57
+ 'default.hard_hat.state',
58
+ ],
59
+ },
60
+ {
61
+ name: 'default.us_state.state_region',
62
+ type: 'int',
63
+ path: [
64
+ 'default.repair_order_details.repair_order_id',
65
+ 'default.repair_order.hard_hat_id',
66
+ 'default.hard_hat.state',
67
+ ],
68
+ },
69
+ {
70
+ name: 'default.us_state.state_region_description',
71
+ type: 'string',
72
+ path: [
73
+ 'default.repair_order_details.repair_order_id',
74
+ 'default.repair_order.hard_hat_id',
75
+ 'default.hard_hat.state',
76
+ ],
77
+ },
78
+ ];
79
+ mockMetric.mockResolvedValueOnce({ dimensions: testDimensions });
80
+ const djClient = mockDJClient();
81
+
82
+ const { findByText } = render(
83
+ <DJClientContext.Provider value={djClient}>
84
+ <DJNodeDimensions {...defaultProps} />
85
+ </DJClientContext.Provider>,
86
+ );
87
+
88
+ for (const dim of testDimensions) {
89
+ const [attribute, ...nodeName] = dim.name.split('.').reverse();
90
+ const dimension = nodeName.reverse().join('.');
91
+ expect(await findByText(attribute)).toBeInTheDocument();
92
+ expect(await findByText(dimension)).toBeInTheDocument();
93
+ }
94
+ });
95
+
96
+ it('does not fetch dimensions if type is not metric', () => {
97
+ const djClient = mockDJClient();
98
+ render(
99
+ <DJClientContext.Provider value={djClient}>
100
+ <DJNodeDimensions {...defaultProps} type="transform" />
101
+ </DJClientContext.Provider>,
102
+ );
103
+ expect(mockMetric).not.toHaveBeenCalled();
104
+ });
105
+
106
+ it('handles errors gracefully', async () => {
107
+ mockMetric.mockRejectedValueOnce(new Error('API error'));
108
+
109
+ const djClient = mockDJClient();
110
+ render(
111
+ <DJClientContext.Provider value={djClient}>
112
+ <DJNodeDimensions {...defaultProps} />
113
+ </DJClientContext.Provider>,
114
+ );
115
+
116
+ expect(await mockMetric).toHaveBeenCalledWith(defaultProps.name);
117
+ });
118
+ });