datajunction-ui 0.0.1-rc.8 → 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 +123 -150
  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,585 @@
1
+ import { useEffect, useState, useMemo } from 'react';
2
+ import TableIcon from '../../icons/TableIcon';
3
+ import AddMaterializationPopover from './AddMaterializationPopover';
4
+ import * as React from 'react';
5
+ import AddBackfillPopover from './AddBackfillPopover';
6
+ import { labelize } from '../../../utils/form';
7
+ import NodeMaterializationDelete from '../../components/NodeMaterializationDelete';
8
+ import Tab from '../../components/Tab';
9
+ import NodeRevisionMaterializationTab from './NodeRevisionMaterializationTab';
10
+ import AvailabilityStateBlock from './AvailabilityStateBlock';
11
+
12
+ const cronstrue = require('cronstrue');
13
+
14
+ export default function NodeMaterializationTab({ node, djClient }) {
15
+ const [rawMaterializations, setRawMaterializations] = useState([]);
16
+ const [selectedRevisionTab, setSelectedRevisionTab] = useState(null);
17
+ const [showInactive, setShowInactive] = useState(false);
18
+ const [availabilityStates, setAvailabilityStates] = useState([]);
19
+ const [availabilityStatesByRevision, setAvailabilityStatesByRevision] =
20
+ useState({});
21
+ const [isRebuilding, setIsRebuilding] = useState(() => {
22
+ // Check if we're in the middle of a rebuild operation
23
+ return localStorage.getItem(`rebuilding-${node?.name}`) === 'true';
24
+ });
25
+
26
+ const filteredMaterializations = useMemo(() => {
27
+ return showInactive
28
+ ? rawMaterializations
29
+ : rawMaterializations.filter(mat => !mat.deactivated_at);
30
+ }, [rawMaterializations, showInactive]);
31
+
32
+ const materializationsByRevision = useMemo(() => {
33
+ return filteredMaterializations.reduce((acc, mat) => {
34
+ // Extract version from materialization config
35
+ const matVersion = mat.config?.cube?.version || node?.version;
36
+
37
+ if (!acc[matVersion]) {
38
+ acc[matVersion] = [];
39
+ }
40
+ acc[matVersion].push(mat);
41
+ return acc;
42
+ }, {});
43
+ }, [filteredMaterializations, node?.version]);
44
+
45
+ useEffect(() => {
46
+ const fetchData = async () => {
47
+ if (node) {
48
+ const data = await djClient.materializations(node.name);
49
+
50
+ // Store raw data
51
+ setRawMaterializations(data);
52
+
53
+ // Fetch availability states
54
+ const availabilityData = await djClient.availabilityStates(node.name);
55
+ setAvailabilityStates(availabilityData);
56
+
57
+ // Group availability states by version
58
+ const availabilityGrouped = availabilityData.reduce((acc, avail) => {
59
+ const version = avail.node_version || node.version;
60
+ if (!acc[version]) {
61
+ acc[version] = [];
62
+ }
63
+ acc[version].push(avail);
64
+ return acc;
65
+ }, {});
66
+
67
+ setAvailabilityStatesByRevision(availabilityGrouped);
68
+
69
+ // Clear rebuilding state once data is loaded after a page reload
70
+ if (localStorage.getItem(`rebuilding-${node.name}`) === 'true') {
71
+ localStorage.removeItem(`rebuilding-${node.name}`);
72
+ setIsRebuilding(false);
73
+ }
74
+ }
75
+ };
76
+ fetchData().catch(console.error);
77
+ }, [djClient, node]);
78
+
79
+ // Separate useEffect to set default selected tab
80
+ useEffect(() => {
81
+ if (
82
+ !selectedRevisionTab &&
83
+ Object.keys(materializationsByRevision).length > 0
84
+ ) {
85
+ // First try to find current node version
86
+ if (materializationsByRevision[node?.version]) {
87
+ setSelectedRevisionTab(node.version);
88
+ } else {
89
+ // Otherwise, select the most recent version (sort by version string)
90
+ const sortedVersions = Object.keys(materializationsByRevision).sort(
91
+ (a, b) => b.localeCompare(a),
92
+ );
93
+ setSelectedRevisionTab(sortedVersions[0]);
94
+ }
95
+ }
96
+ }, [materializationsByRevision, selectedRevisionTab, node?.version]);
97
+
98
+ const partitionColumnsMap = node
99
+ ? Object.fromEntries(
100
+ node?.columns
101
+ .filter(col => col.partition !== null)
102
+ .map(col => [col.name, col.display_name]),
103
+ )
104
+ : {};
105
+ const cron = materialization => {
106
+ var parsedCron = '';
107
+ try {
108
+ parsedCron = cronstrue.toString(materialization.schedule);
109
+ } catch (e) {}
110
+ return parsedCron;
111
+ };
112
+
113
+ const onClickRevisionTab = revisionId => () => {
114
+ setSelectedRevisionTab(revisionId);
115
+ };
116
+
117
+ const buildRevisionTabs = () => {
118
+ const versions = Object.keys(materializationsByRevision);
119
+
120
+ // Check if there are any materializations at all (including inactive ones)
121
+ const hasAnyMaterializations = rawMaterializations.length > 0;
122
+
123
+ // Determine which versions have only inactive materializations
124
+ const versionHasOnlyInactive = {};
125
+ rawMaterializations.forEach(mat => {
126
+ const matVersion = mat.config?.cube?.version || node.version;
127
+ if (!versionHasOnlyInactive[matVersion]) {
128
+ versionHasOnlyInactive[matVersion] = {
129
+ hasActive: false,
130
+ hasInactive: false,
131
+ };
132
+ }
133
+ if (mat.deactivated_at) {
134
+ versionHasOnlyInactive[matVersion].hasInactive = true;
135
+ } else {
136
+ versionHasOnlyInactive[matVersion].hasActive = true;
137
+ }
138
+ });
139
+
140
+ // If no active versions but there are inactive materializations, show checkbox and button
141
+ if (versions.length === 0) {
142
+ return (
143
+ <div
144
+ style={{
145
+ display: 'flex',
146
+ alignItems: 'center',
147
+ justifyContent: 'flex-end',
148
+ marginBottom: '20px',
149
+ }}
150
+ >
151
+ <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
152
+ {hasAnyMaterializations && (
153
+ <label
154
+ style={{
155
+ display: 'flex',
156
+ alignItems: 'center',
157
+ gap: '5px',
158
+ fontSize: '14px',
159
+ color: '#333',
160
+ padding: '4px 8px',
161
+ borderRadius: '12px',
162
+ backgroundColor: '#f5f5f5',
163
+ border: '1px solid #ddd',
164
+ }}
165
+ title="Shows inactive materializations for the latest cube."
166
+ >
167
+ <input
168
+ type="checkbox"
169
+ checked={showInactive}
170
+ onChange={e => setShowInactive(e.target.checked)}
171
+ />
172
+ Show Inactive
173
+ </label>
174
+ )}
175
+ {node && <AddMaterializationPopover node={node} />}
176
+ </div>
177
+ </div>
178
+ );
179
+ }
180
+
181
+ // Sort versions: current version first, then by version string (most recent first)
182
+ const sortedVersions = versions.sort((a, b) => {
183
+ // Current node version always comes first
184
+ if (a === node?.version) return -1;
185
+ if (b === node?.version) return 1;
186
+
187
+ // Then sort by version string (descending)
188
+ return b.localeCompare(a);
189
+ });
190
+
191
+ // Check if latest version has active materializations
192
+ const hasLatestVersionMaterialization =
193
+ materializationsByRevision[node?.version] &&
194
+ materializationsByRevision[node?.version].length > 0;
195
+
196
+ // Refresh latest materialization function
197
+ const refreshLatestMaterialization = async () => {
198
+ if (
199
+ !window.confirm(
200
+ 'This will create a new version of the cube and build new materialization workflows. The previous version of the cube and its materialization will be accessible using a specific version label. Would you like to continue?',
201
+ )
202
+ ) {
203
+ return;
204
+ }
205
+
206
+ // Set loading state in both React state and localStorage
207
+ setIsRebuilding(true);
208
+ localStorage.setItem(`rebuilding-${node.name}`, 'true');
209
+
210
+ try {
211
+ const { status, json } = await djClient.refreshLatestMaterialization(
212
+ node.name,
213
+ );
214
+
215
+ if (status === 200 || status === 201) {
216
+ // Keep the loading state during page reload
217
+ window.location.reload(); // Reload to show the updated materialization
218
+ } else {
219
+ alert(`Failed to rebuild materialization: ${json.message}`);
220
+ // Clear loading state on error
221
+ localStorage.removeItem(`rebuilding-${node.name}`);
222
+ setIsRebuilding(false);
223
+ }
224
+ } catch (error) {
225
+ alert(`Error rebuilding materialization: ${error.message}`);
226
+ // Clear loading state on error
227
+ localStorage.removeItem(`rebuilding-${node.name}`);
228
+ setIsRebuilding(false);
229
+ }
230
+ };
231
+
232
+ return (
233
+ <div
234
+ style={{
235
+ display: 'flex',
236
+ alignItems: 'center',
237
+ justifyContent: 'space-between',
238
+ marginBottom: '20px',
239
+ }}
240
+ >
241
+ <div className="align-items-center row">
242
+ {sortedVersions.map(version => (
243
+ <NodeRevisionMaterializationTab
244
+ key={version}
245
+ version={version}
246
+ node={node}
247
+ selectedRevisionTab={selectedRevisionTab}
248
+ onClickRevisionTab={onClickRevisionTab}
249
+ showInactive={showInactive}
250
+ versionHasOnlyInactive={versionHasOnlyInactive}
251
+ />
252
+ ))}
253
+ </div>
254
+ <div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
255
+ <label
256
+ style={{
257
+ display: 'flex',
258
+ alignItems: 'center',
259
+ gap: '5px',
260
+ fontSize: '14px',
261
+ color: '#333',
262
+ padding: '4px 8px',
263
+ borderRadius: '12px',
264
+ backgroundColor: '#f5f5f5',
265
+ border: '1px solid #ddd',
266
+ }}
267
+ title="Shows inactive materializations for the latest cube."
268
+ >
269
+ <input
270
+ type="checkbox"
271
+ checked={showInactive}
272
+ onChange={e => setShowInactive(e.target.checked)}
273
+ />
274
+ Show Inactive
275
+ </label>
276
+ {node &&
277
+ (hasLatestVersionMaterialization ? (
278
+ <button
279
+ className="edit_button"
280
+ aria-label="RefreshLatestMaterialization"
281
+ tabIndex="0"
282
+ onClick={refreshLatestMaterialization}
283
+ disabled={isRebuilding}
284
+ title="Create a new version of the cube and re-create its materialization workflows."
285
+ style={{
286
+ opacity: isRebuilding ? 0.7 : 1,
287
+ cursor: isRebuilding ? 'not-allowed' : 'pointer',
288
+ }}
289
+ >
290
+ <span className="add_node">
291
+ Rebuild (latest) Materialization
292
+ </span>
293
+ </button>
294
+ ) : (
295
+ <AddMaterializationPopover node={node} />
296
+ ))}
297
+ </div>
298
+ </div>
299
+ );
300
+ };
301
+
302
+ const materializationRows = materializations => {
303
+ return materializations.map((materialization, index) => (
304
+ <div key={`${materialization.name}-${index}`}>
305
+ <div className="tr">
306
+ <div key={materialization.name} style={{ fontSize: 'large' }}>
307
+ <div
308
+ className="text-start node_name td"
309
+ style={{ fontWeight: '600' }}
310
+ >
311
+ {materialization.job
312
+ ?.replace('MaterializationJob', '')
313
+ .match(/[A-Z][a-z]+/g)
314
+ .join(' ')}
315
+ </div>
316
+ <div className="td">
317
+ <NodeMaterializationDelete
318
+ nodeName={node.name}
319
+ materializationName={materialization.name}
320
+ nodeVersion={selectedRevisionTab}
321
+ />
322
+ </div>
323
+ <div className="td">
324
+ <span className={`badge cron`}>{materialization.schedule}</span>
325
+ <div className={`cron-description`}>{cron(materialization)} </div>
326
+ </div>
327
+ <div className="td">
328
+ <span className={`badge strategy`}>
329
+ {labelize(materialization.strategy)}
330
+ </span>
331
+ </div>
332
+ </div>
333
+ </div>
334
+ <div style={{ display: 'table-row' }}>
335
+ <div style={{ display: 'inline-flex' }}>
336
+ <ul className="backfills">
337
+ <li className="backfill">
338
+ <div className="backfills_header">Output Tables</div>{' '}
339
+ {materialization.output_tables.map(table => (
340
+ <div className={`table__full`} key={table}>
341
+ <div className="table__header">
342
+ <TableIcon />{' '}
343
+ <span className={`entity-info`}>
344
+ {table.split('.')[0] + '.' + table.split('.')[1]}
345
+ </span>
346
+ </div>
347
+ <div className={`table__body upstream_tables`}>
348
+ {table.split('.')[2]}
349
+ </div>
350
+ </div>
351
+ ))}
352
+ </li>
353
+ </ul>
354
+ </div>
355
+
356
+ <div style={{ display: 'inline-flex' }}>
357
+ <ul className="backfills">
358
+ <li>
359
+ <div className="backfills_header">Workflows</div>{' '}
360
+ <ul>
361
+ {materialization.urls.map((url, idx) => (
362
+ <li style={{ listStyle: 'none' }} key={idx}>
363
+ <div
364
+ className="partitionLink"
365
+ style={{ fontSize: 'revert' }}
366
+ >
367
+ <a
368
+ href={url}
369
+ key={`url-${idx}`}
370
+ className=""
371
+ target="blank"
372
+ >
373
+ {idx === 0 ? 'main' : 'backfill'}
374
+ </a>
375
+ </div>
376
+ </li>
377
+ ))}
378
+ </ul>
379
+ </li>
380
+ </ul>
381
+ </div>
382
+
383
+ <div style={{ display: 'inline-flex' }}>
384
+ <ul className="backfills">
385
+ <li className="backfill">
386
+ <details open>
387
+ <summary>
388
+ <span className="backfills_header">Backfills</span>{' '}
389
+ </summary>
390
+ {materialization.strategy === 'incremental_time' ? (
391
+ <ul>
392
+ <li>
393
+ <AddBackfillPopover
394
+ node={node}
395
+ materialization={materialization}
396
+ />
397
+ </li>
398
+ {materialization.backfills.map(backfill => (
399
+ <li className="backfill">
400
+ <div className="partitionLink">
401
+ <a href={backfill.urls[0]}>
402
+ {backfill.spec.map(partition => {
403
+ const partitionBody =
404
+ 'range' in partition &&
405
+ partition['range'] !== null ? (
406
+ <>
407
+ <span className="badge partition_value">
408
+ {partition.range[0]}
409
+ </span>
410
+ to
411
+ <span className="badge partition_value">
412
+ {partition.range[1]}
413
+ </span>
414
+ </>
415
+ ) : (
416
+ <span className="badge partition_value">
417
+ {partition.values.join(', ')}
418
+ </span>
419
+ );
420
+ return (
421
+ <>
422
+ <div>
423
+ {
424
+ partitionColumnsMap[
425
+ partition.column_name.replaceAll(
426
+ '_DOT_',
427
+ '.',
428
+ )
429
+ ]
430
+ }{' '}
431
+ {partitionBody}
432
+ </div>
433
+ </>
434
+ );
435
+ })}
436
+ </a>
437
+ </div>
438
+ </li>
439
+ ))}
440
+ </ul>
441
+ ) : (
442
+ <ul>
443
+ <li>N/A</li>
444
+ </ul>
445
+ )}
446
+ </details>
447
+ </li>
448
+ </ul>
449
+ </div>
450
+ <div className="td">
451
+ <ul className="backfills">
452
+ <li className="backfill">
453
+ <div className="backfills_header">Partitions</div>{' '}
454
+ <ul>
455
+ {node.columns
456
+ .filter(col => col.partition !== null)
457
+ .map(column => {
458
+ return (
459
+ <li key={column.name}>
460
+ <div className="partitionLink">
461
+ {column.display_name}
462
+ <span className="badge partition_value">
463
+ {column.partition.type_}
464
+ </span>
465
+ </div>
466
+ </li>
467
+ );
468
+ })}
469
+ </ul>
470
+ </li>
471
+ </ul>
472
+ </div>
473
+ </div>
474
+ </div>
475
+ ));
476
+ };
477
+ const currentRevisionMaterializations = selectedRevisionTab
478
+ ? materializationsByRevision[selectedRevisionTab] || []
479
+ : filteredMaterializations;
480
+
481
+ const currentRevisionAvailability = selectedRevisionTab
482
+ ? availabilityStatesByRevision[selectedRevisionTab] || []
483
+ : availabilityStates;
484
+
485
+ const renderMaterializedDatasets = availabilityStates => {
486
+ if (!availabilityStates || availabilityStates.length === 0) {
487
+ return (
488
+ <div className="message alert" style={{ marginTop: '10px' }}>
489
+ No materialized datasets available for this revision.
490
+ </div>
491
+ );
492
+ }
493
+
494
+ return availabilityStates.map((availability, index) => (
495
+ <AvailabilityStateBlock
496
+ key={`availability-${index}`}
497
+ availability={availability}
498
+ />
499
+ ));
500
+ };
501
+
502
+ return (
503
+ <>
504
+ <div
505
+ className="table-vertical"
506
+ role="table"
507
+ aria-label="Materializations"
508
+ style={{ position: 'relative' }}
509
+ >
510
+ {/* Loading overlay */}
511
+ {isRebuilding && (
512
+ <div
513
+ style={{
514
+ position: 'absolute',
515
+ top: 0,
516
+ left: 0,
517
+ right: 0,
518
+ bottom: 0,
519
+ backgroundColor: 'rgba(255, 255, 255, 0.8)',
520
+ display: 'flex',
521
+ flexDirection: 'column',
522
+ justifyContent: 'center',
523
+ alignItems: 'center',
524
+ zIndex: 1000,
525
+ minHeight: '200px',
526
+ }}
527
+ >
528
+ <div
529
+ style={{
530
+ width: '40px',
531
+ height: '40px',
532
+ border: '4px solid #f3f3f3',
533
+ borderTop: '4px solid #3498db',
534
+ borderRadius: '50%',
535
+ animation: 'spin 1s linear infinite',
536
+ marginBottom: '16px',
537
+ }}
538
+ />
539
+ <div
540
+ style={{ fontSize: '16px', color: '#666', textAlign: 'center' }}
541
+ >
542
+ Rebuilding materialization...
543
+ <br />
544
+ <small style={{ fontSize: '14px' }}>
545
+ This may take a few moments
546
+ </small>
547
+ </div>
548
+ </div>
549
+ )}
550
+
551
+ <div>
552
+ {buildRevisionTabs()}
553
+ {currentRevisionMaterializations.length > 0 ? (
554
+ <div
555
+ className="card-inner-table table"
556
+ aria-label="Materializations"
557
+ aria-hidden="false"
558
+ >
559
+ <div style={{ display: 'table' }}>
560
+ {materializationRows(
561
+ currentRevisionMaterializations.filter(
562
+ materialization =>
563
+ !(
564
+ materialization.name === 'default' &&
565
+ node.type === 'cube'
566
+ ),
567
+ ),
568
+ )}
569
+ </div>
570
+ </div>
571
+ ) : (
572
+ <div className="message alert" style={{ marginTop: '10px' }}>
573
+ No materialization workflows configured for this revision.
574
+ </div>
575
+ )}
576
+ {Object.keys(materializationsByRevision).length > 0 && (
577
+ <div style={{ marginTop: '30px' }}>
578
+ {renderMaterializedDatasets(currentRevisionAvailability)}
579
+ </div>
580
+ )}
581
+ </div>
582
+ </div>
583
+ </>
584
+ );
585
+ }
@@ -0,0 +1,58 @@
1
+ import Tab from '../../components/Tab';
2
+
3
+ export default function NodeRevisionMaterializationTab({
4
+ version,
5
+ node,
6
+ selectedRevisionTab,
7
+ onClickRevisionTab,
8
+ showInactive,
9
+ versionHasOnlyInactive,
10
+ }) {
11
+ const isCurrentVersion = version === node?.version;
12
+ const tabName = isCurrentVersion ? `${version} (latest)` : version;
13
+ const versionInfo = versionHasOnlyInactive[version];
14
+ const isOnlyInactive =
15
+ versionInfo && !versionInfo.hasActive && versionInfo.hasInactive;
16
+
17
+ // For inactive-only versions, render with oval styling
18
+ if (isOnlyInactive && showInactive) {
19
+ return (
20
+ <div
21
+ key={version}
22
+ className={selectedRevisionTab === version ? 'col active' : 'col'}
23
+ >
24
+ <div className="header-tabs nav-overflow nav nav-tabs">
25
+ <div className="nav-item">
26
+ <button
27
+ id={version}
28
+ className="nav-link"
29
+ tabIndex="0"
30
+ onClick={onClickRevisionTab(version)}
31
+ aria-label={tabName}
32
+ aria-hidden="false"
33
+ style={{
34
+ padding: '4px 8px',
35
+ borderRadius: '12px',
36
+ backgroundColor: '#f5f5f5',
37
+ border: '1px solid #ddd',
38
+ margin: '0 2px',
39
+ }}
40
+ >
41
+ {tabName}
42
+ </button>
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ return (
50
+ <Tab
51
+ key={version}
52
+ id={version}
53
+ name={tabName}
54
+ onClick={onClickRevisionTab(version)}
55
+ selectedTab={selectedRevisionTab}
56
+ />
57
+ );
58
+ }