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
@@ -1,44 +1,435 @@
1
- import { Component } from 'react';
2
-
3
- export default class NodeColumnTab extends Component {
4
- columnList = node => {
5
- return node.columns.map(col => (
6
- <tr>
7
- <td className="text-start">{col.name}</td>
8
- <td>
9
- <span className="node_type__transform badge node_type">
10
- {col.type}
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import EditColumnPopover from './EditColumnPopover';
4
+ import EditColumnDescriptionPopover from './EditColumnDescriptionPopover';
5
+ import ManageDimensionLinksDialog from './ManageDimensionLinksDialog';
6
+ import AddComplexDimensionLinkPopover from './AddComplexDimensionLinkPopover';
7
+ import { labelize } from '../../../utils/form';
8
+ import PartitionColumnPopover from './PartitionColumnPopover';
9
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
10
+ import { foundation } from 'react-syntax-highlighter/src/styles/hljs';
11
+
12
+ export default function NodeColumnTab({ node, djClient }) {
13
+ const [attributes, setAttributes] = useState([]);
14
+ const [dimensions, setDimensions] = useState([]);
15
+ const [columns, setColumns] = useState([]);
16
+ const [links, setLinks] = useState([]);
17
+
18
+ useEffect(() => {
19
+ const fetchData = async () => {
20
+ if (node) {
21
+ setColumns(await djClient.columns(node));
22
+ }
23
+ };
24
+ fetchData().catch(console.error);
25
+ }, [djClient, node]);
26
+
27
+ useEffect(() => {
28
+ const fetchData = async () => {
29
+ const attributes = await djClient.attributes();
30
+ const options = attributes.map(attr => {
31
+ return { value: attr.name, label: labelize(attr.name) };
32
+ });
33
+ setAttributes(options);
34
+ };
35
+ fetchData().catch(console.error);
36
+ }, [djClient]);
37
+
38
+ useEffect(() => {
39
+ const fetchData = async () => {
40
+ const dimensions = await djClient.dimensions();
41
+ const options = dimensions.map(dim => {
42
+ return {
43
+ value: dim.name,
44
+ label: `${dim.name} (${dim.indegree} links)`,
45
+ };
46
+ });
47
+ setDimensions(options);
48
+ };
49
+ fetchData().catch(console.error);
50
+ }, [djClient]);
51
+
52
+ const showColumnAttributes = col => {
53
+ return col.attributes.map((attr, idx) => (
54
+ <span
55
+ className="node_type__dimension badge node_type"
56
+ key={`col-attr-${col.name}-${idx}`}
57
+ >
58
+ {attr.attribute_type.name.replace(/_/, ' ')}
59
+ </span>
60
+ ));
61
+ };
62
+
63
+ const showColumnPartition = col => {
64
+ if (col.partition) {
65
+ return (
66
+ <>
67
+ <span className="node_type badge node_type__blank">
68
+ <span className="partition_value badge">
69
+ <b>Type:</b> {col.partition.type_}
70
+ </span>
71
+ <br />
72
+ <span className="partition_value badge">
73
+ <b>Format:</b> <code>{col.partition.format}</code>
74
+ </span>
75
+ <br />
76
+ <span className="partition_value badge">
77
+ <b>Granularity:</b> <code>{col.partition.granularity}</code>
78
+ </span>
11
79
  </span>
12
- </td>
13
- <td>{col.dimension ? col.dimension.name : ''}</td>
14
- <td>
15
- {col.attributes.find(
16
- attr => attr.attribute_type.name === 'dimension',
17
- ) ? (
18
- <span className="node_type__dimension badge node_type">
19
- dimensional
80
+ </>
81
+ );
82
+ }
83
+ return '';
84
+ };
85
+
86
+ const columnList = columns => {
87
+ return columns?.map(col => {
88
+ // FK Links: Only show links that specifically reference THIS column's foreign keys
89
+ // Filter out complex dimension links that may join on multiple columns
90
+ const fkLinksForColumn = (
91
+ links.length > 0 ? links : node?.dimension_links
92
+ )
93
+ .filter(link => {
94
+ // Check if this link has a foreign key entry for this specific column
95
+ const foreignKeys = Object.keys(link.foreign_keys || {});
96
+ const columnKey = `${node.name}.${col.name}`;
97
+ return foreignKeys.includes(columnKey);
98
+ })
99
+ .map(link => link.dimension.name);
100
+
101
+ // Check if this column has a reference dimension link
102
+ const referenceLink = col.dimension
103
+ ? {
104
+ dimension: col.dimension.name,
105
+ dimension_column: col.dimension_column,
106
+ role: col.dimension.role,
107
+ }
108
+ : null;
109
+ return (
110
+ <tr key={col.name} className="column-row">
111
+ <td
112
+ className="text-start"
113
+ role="columnheader"
114
+ aria-label="ColumnName"
115
+ aria-hidden="false"
116
+ >
117
+ {col.name}
118
+ </td>
119
+ <td>
120
+ <span
121
+ className=""
122
+ role="columnheader"
123
+ aria-label="ColumnDisplayName"
124
+ aria-hidden="false"
125
+ >
126
+ {col.display_name}
127
+ </span>
128
+ </td>
129
+ <td>
130
+ <span
131
+ className=""
132
+ role="columnheader"
133
+ aria-label="ColumnDescription"
134
+ aria-hidden="false"
135
+ >
136
+ {col.description || ''}
137
+ <EditColumnDescriptionPopover
138
+ column={col}
139
+ node={node}
140
+ onSubmit={async () => {
141
+ const res = await djClient.node(node.name);
142
+ setColumns(res.columns);
143
+ }}
144
+ />
145
+ </span>
146
+ </td>
147
+ <td>
148
+ <span
149
+ className={`node_type__${
150
+ node.type === 'cube' ? col.type : 'transform'
151
+ } badge node_type`}
152
+ role="columnheader"
153
+ aria-label="ColumnType"
154
+ aria-hidden="false"
155
+ >
156
+ {col.type}
20
157
  </span>
158
+ </td>
159
+ {node.type !== 'cube' ? (
160
+ <td className="dimension-links-cell">
161
+ <div
162
+ style={{
163
+ display: 'flex',
164
+ alignItems: 'center',
165
+ flexWrap: 'wrap',
166
+ gap: '0.25rem',
167
+ position: 'relative',
168
+ }}
169
+ >
170
+ {fkLinksForColumn.length > 0 && (
171
+ <div
172
+ style={{
173
+ display: 'flex',
174
+ flexWrap: 'wrap',
175
+ gap: '0.25rem',
176
+ }}
177
+ >
178
+ {fkLinksForColumn.map(dimName => (
179
+ <span
180
+ className="rounded-pill badge bg-secondary-soft dimension-badge"
181
+ style={{ fontSize: '14px', position: 'relative' }}
182
+ key={dimName}
183
+ title="FK Link (via primary key)"
184
+ >
185
+ <a href={`/nodes/${dimName}`}>{dimName}</a>
186
+ </span>
187
+ ))}
188
+ </div>
189
+ )}
190
+ {referenceLink && (
191
+ <span
192
+ className="rounded-pill badge bg-info dimension-badge"
193
+ style={{ fontSize: '14px', position: 'relative' }}
194
+ title={`Reference Link: ${referenceLink.dimension}.${referenceLink.dimension_column}`}
195
+ >
196
+ <a href={`/nodes/${referenceLink.dimension}`}>
197
+ {referenceLink.dimension}.{referenceLink.dimension_column}
198
+ </a>
199
+ </span>
200
+ )}
201
+ <ManageDimensionLinksDialog
202
+ column={col}
203
+ node={node}
204
+ dimensions={dimensions}
205
+ fkLinks={fkLinksForColumn}
206
+ referenceLink={referenceLink}
207
+ onSubmit={async () => {
208
+ const res = await djClient.node(node.name);
209
+ setLinks(res.dimension_links);
210
+ setColumns(res.columns);
211
+ }}
212
+ />
213
+ </div>
214
+ </td>
21
215
  ) : (
22
216
  ''
23
217
  )}
24
- </td>
25
- </tr>
26
- ));
218
+ {node.type !== 'cube' ? (
219
+ <td>
220
+ {showColumnAttributes(col)}
221
+ <EditColumnPopover
222
+ column={col}
223
+ node={node}
224
+ options={attributes}
225
+ onSubmit={async () => {
226
+ const res = await djClient.node(node.name);
227
+ setColumns(res.columns);
228
+ }}
229
+ />
230
+ </td>
231
+ ) : (
232
+ ''
233
+ )}
234
+ <td>
235
+ {showColumnPartition(col)}
236
+ <PartitionColumnPopover
237
+ column={col}
238
+ node={node}
239
+ onSubmit={async () => {
240
+ const res = await djClient.node(node.name);
241
+ setColumns(res.columns);
242
+ }}
243
+ />
244
+ </td>
245
+ </tr>
246
+ );
247
+ });
27
248
  };
28
249
 
29
- render() {
30
- return (
250
+ return (
251
+ <>
252
+ <style>
253
+ {`
254
+ .dimension-link-edit:hover {
255
+ opacity: 1 !important;
256
+ color: #007bff !important;
257
+ }
258
+ .dimension-badge a {
259
+ max-width: 300px;
260
+ display: inline-block;
261
+ overflow: hidden;
262
+ text-overflow: ellipsis;
263
+ white-space: nowrap;
264
+ vertical-align: bottom;
265
+ direction: rtl;
266
+ text-align: left;
267
+ }
268
+ `}
269
+ </style>
31
270
  <div className="table-responsive">
32
271
  <table className="card-inner-table table">
33
272
  <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
34
- <th className="text-start">Column</th>
35
- <th>Type</th>
36
- <th>Dimension</th>
37
- <th>Attributes</th>
273
+ <tr>
274
+ <th className="text-start">Column</th>
275
+ <th>Display Name</th>
276
+ <th>Description</th>
277
+ <th>Type</th>
278
+ {node?.type !== 'cube' ? (
279
+ <>
280
+ <th>Dimension Links</th>
281
+ <th>Attributes</th>
282
+ </>
283
+ ) : (
284
+ ''
285
+ )}
286
+ <th>Partition</th>
287
+ </tr>
288
+ </thead>
289
+ <tbody>{columnList(columns)}</tbody>
290
+ </table>
291
+ </div>
292
+ <div>
293
+ <div
294
+ style={{
295
+ display: 'flex',
296
+ alignItems: 'center',
297
+ justifyContent: 'space-between',
298
+ marginBottom: '1rem',
299
+ }}
300
+ >
301
+ <h3 style={{ margin: 0 }}>
302
+ Complex Dimension Links (Custom Join SQL)
303
+ </h3>
304
+ <AddComplexDimensionLinkPopover
305
+ node={node}
306
+ dimensions={dimensions}
307
+ onSubmit={async () => {
308
+ const res = await djClient.node(node.name);
309
+ setLinks(res.dimension_links);
310
+ }}
311
+ />
312
+ </div>
313
+ <table className="card-inner-table table">
314
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
315
+ <tr>
316
+ <th className="text-start">Dimension Node</th>
317
+ <th>Join Type</th>
318
+ <th>Join SQL</th>
319
+ <th>Join Cardinality</th>
320
+ <th>Role</th>
321
+ <th>Actions</th>
322
+ </tr>
38
323
  </thead>
39
- {this.columnList(this.props.node)}
324
+ <tbody>
325
+ {node?.dimension_links && node.dimension_links.length > 0 ? (
326
+ node.dimension_links.map((link, idx) => {
327
+ return (
328
+ <tr key={`${link.dimension.name}-${link.role || idx}`}>
329
+ <td>
330
+ <a href={'/nodes/' + link.dimension.name}>
331
+ {link.dimension.name}
332
+ </a>
333
+ </td>
334
+ <td>{link.join_type.toUpperCase()}</td>
335
+ <td style={{ maxWidth: '400px' }}>
336
+ <SyntaxHighlighter
337
+ language="sql"
338
+ style={foundation}
339
+ wrapLongLines={true}
340
+ >
341
+ {link.join_sql?.trim()}
342
+ </SyntaxHighlighter>
343
+ </td>
344
+ <td>
345
+ {link.join_cardinality
346
+ ? link.join_cardinality.replace(/_/g, ' ').toUpperCase()
347
+ : 'N/A'}
348
+ </td>
349
+ <td>{link.role || '-'}</td>
350
+ <td>
351
+ <div style={{ display: 'flex', gap: '0.5rem' }}>
352
+ <AddComplexDimensionLinkPopover
353
+ node={node}
354
+ dimensions={dimensions}
355
+ existingLink={link}
356
+ isEditMode={true}
357
+ onSubmit={async () => {
358
+ const res = await djClient.node(node.name);
359
+ setLinks(res.dimension_links);
360
+ }}
361
+ />
362
+ <button
363
+ onClick={async () => {
364
+ if (
365
+ window.confirm(
366
+ `Remove link to ${link.dimension.name}?`,
367
+ )
368
+ ) {
369
+ try {
370
+ const response =
371
+ await djClient.removeComplexDimensionLink(
372
+ node.name,
373
+ link.dimension.name,
374
+ link.role || null,
375
+ );
376
+ if (
377
+ response.status === 200 ||
378
+ response.status === 201 ||
379
+ response.status === 204
380
+ ) {
381
+ alert(
382
+ 'Complex dimension link removed successfully!',
383
+ );
384
+ window.location.reload();
385
+ } else {
386
+ console.error('Remove link error:', response);
387
+ alert(
388
+ response.json?.message ||
389
+ `Failed to remove link (status: ${response.status})`,
390
+ );
391
+ }
392
+ } catch (error) {
393
+ console.error('Remove link exception:', error);
394
+ alert(`Error removing link: ${error.message}`);
395
+ }
396
+ }
397
+ }}
398
+ style={{
399
+ padding: '0.25rem 0.5rem',
400
+ fontSize: '0.75rem',
401
+ background: '#dc3545',
402
+ color: 'white',
403
+ border: 'none',
404
+ borderRadius: '4px',
405
+ cursor: 'pointer',
406
+ }}
407
+ >
408
+ Remove
409
+ </button>
410
+ </div>
411
+ </td>
412
+ </tr>
413
+ );
414
+ })
415
+ ) : (
416
+ <tr>
417
+ <td
418
+ colSpan="6"
419
+ style={{
420
+ textAlign: 'center',
421
+ padding: '2rem',
422
+ color: '#6c757d',
423
+ }}
424
+ >
425
+ No complex dimension links. Click the + button above to add
426
+ one.
427
+ </td>
428
+ </tr>
429
+ )}
430
+ </tbody>
40
431
  </table>
41
432
  </div>
42
- );
43
- }
433
+ </>
434
+ );
44
435
  }
@@ -0,0 +1,153 @@
1
+ import { useEffect, useState } from 'react';
2
+ import * as React from 'react';
3
+ import { labelize } from '../../../utils/form';
4
+ import LoadingIcon from '../../icons/LoadingIcon';
5
+
6
+ export default function NodeDependenciesTab({ node, djClient }) {
7
+ const [nodeDAG, setNodeDAG] = useState({
8
+ upstreams: [],
9
+ downstreams: [],
10
+ dimensions: [],
11
+ });
12
+
13
+ const [retrieved, setRetrieved] = useState(false);
14
+
15
+ useEffect(() => {
16
+ const fetchData = async () => {
17
+ if (node) {
18
+ let upstreams = await djClient.upstreams(node.name);
19
+ let downstreams = await djClient.downstreams(node.name);
20
+ let dimensions = await djClient.nodeDimensions(node.name);
21
+ setNodeDAG({
22
+ upstreams: upstreams,
23
+ downstreams: downstreams,
24
+ dimensions: dimensions,
25
+ });
26
+ setRetrieved(true);
27
+ }
28
+ };
29
+ fetchData().catch(console.error);
30
+ }, [djClient, node]);
31
+
32
+ // Builds the block of dimensions selectors, grouped by node name + path
33
+ return (
34
+ <div>
35
+ <h2>Upstreams</h2>
36
+ {retrieved ? (
37
+ <NodeList nodes={nodeDAG.upstreams} />
38
+ ) : (
39
+ <span style={{ display: 'block' }}>
40
+ <LoadingIcon centered={false} />
41
+ </span>
42
+ )}
43
+ <h2>Downstreams</h2>
44
+ {retrieved ? (
45
+ <NodeList nodes={nodeDAG.downstreams} />
46
+ ) : (
47
+ <span style={{ display: 'block' }}>
48
+ <LoadingIcon centered={false} />
49
+ </span>
50
+ )}
51
+ <h2>Dimensions</h2>
52
+ {retrieved ? (
53
+ <NodeDimensionsList rawDimensions={nodeDAG.dimensions} />
54
+ ) : (
55
+ <span style={{ display: 'block' }}>
56
+ <LoadingIcon centered={false} />
57
+ </span>
58
+ )}
59
+ </div>
60
+ );
61
+ }
62
+
63
+ export function NodeDimensionsList({ rawDimensions }) {
64
+ const dimensions = Object.entries(
65
+ rawDimensions.reduce((group, dimension) => {
66
+ group[dimension.node_name + dimension.path] =
67
+ group[dimension.node_name + dimension.path] ?? [];
68
+ group[dimension.node_name + dimension.path].push(dimension);
69
+ return group;
70
+ }, {}),
71
+ );
72
+ return (
73
+ <div style={{ padding: '0.5rem' }}>
74
+ {dimensions.map(grouping => {
75
+ const dimensionsInGroup = grouping[1];
76
+ const role = dimensionsInGroup[0].path
77
+ .map(pathItem => pathItem.split('.').slice(-1))
78
+ .join(' → ');
79
+ const fullPath = dimensionsInGroup[0].path.join(' → ');
80
+ const groupHeader = (
81
+ <span
82
+ style={{
83
+ fontWeight: 'normal',
84
+ marginBottom: '15px',
85
+ marginTop: '15px',
86
+ }}
87
+ >
88
+ <a href={`/nodes/${dimensionsInGroup[0].node_name}`}>
89
+ <b>{dimensionsInGroup[0].node_display_name}</b>
90
+ </a>{' '}
91
+ with role{' '}
92
+ <span className="HighlightPath">
93
+ <b>{role}</b>
94
+ </span>{' '}
95
+ via <span className="HighlightPath">{fullPath}</span>
96
+ </span>
97
+ );
98
+ const dimensionGroupOptions = dimensionsInGroup.map(dim => {
99
+ return {
100
+ value: dim.name,
101
+ label:
102
+ labelize(dim.name.split('.').slice(-1)[0]) +
103
+ (dim.properties.includes('primary_key') ? ' (PK)' : ''),
104
+ };
105
+ });
106
+ return (
107
+ <details key={grouping[0]}>
108
+ <summary style={{ marginBottom: '10px' }}>{groupHeader}</summary>
109
+ <div className="dimensionsList">
110
+ {dimensionGroupOptions.map(dimension => {
111
+ return (
112
+ <div key={dimension.value}>
113
+ {dimension.label.split('[').slice(0)[0]} ⇢{' '}
114
+ <code className="DimensionAttribute">
115
+ {dimension.value}
116
+ </code>
117
+ </div>
118
+ );
119
+ })}
120
+ </div>
121
+ </details>
122
+ );
123
+ })}
124
+ </div>
125
+ );
126
+ }
127
+
128
+ export function NodeList({ nodes }) {
129
+ return nodes && nodes.length > 0 ? (
130
+ <ul className="backfills">
131
+ {nodes?.map(node => (
132
+ <li
133
+ className="backfill"
134
+ style={{ marginBottom: '5px' }}
135
+ key={node.name}
136
+ >
137
+ <span
138
+ className={`node_type__${node.type} badge node_type`}
139
+ style={{ marginRight: '5px' }}
140
+ role="dialog"
141
+ aria-hidden="false"
142
+ aria-label="NodeType"
143
+ >
144
+ {node.type}
145
+ </span>
146
+ <a href={`/nodes/${node.name}`}>{node.name}</a>
147
+ </li>
148
+ ))}
149
+ </ul>
150
+ ) : (
151
+ <span style={{ display: 'block' }}>None</span>
152
+ );
153
+ }