datajunction-ui 0.0.1-rc.9 → 0.0.2-3.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 (238) 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/NotificationBell.tsx +229 -0
  16. package/src/app/components/QueryInfo.jsx +172 -0
  17. package/src/app/components/Search.jsx +94 -0
  18. package/src/app/components/Tab.jsx +8 -1
  19. package/src/app/components/ToggleSwitch.jsx +20 -0
  20. package/src/app/components/UserMenu.tsx +92 -0
  21. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  22. package/src/app/components/__tests__/NodeMaterializationDelete.test.jsx +263 -0
  23. package/src/app/components/__tests__/NotificationBell.test.tsx +313 -0
  24. package/src/app/components/__tests__/QueryInfo.test.jsx +183 -0
  25. package/src/app/components/__tests__/Search.test.jsx +307 -0
  26. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  27. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  28. package/src/app/components/__tests__/UserMenu.test.tsx +248 -0
  29. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  30. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  31. package/src/app/components/djgraph/Collapse.jsx +47 -0
  32. package/src/app/components/djgraph/DJNode.jsx +61 -83
  33. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  34. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  35. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  36. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  37. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  38. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  39. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  40. package/src/app/components/forms/Action.jsx +8 -0
  41. package/src/app/components/forms/NodeNameField.jsx +64 -0
  42. package/src/app/components/search.css +17 -0
  43. package/src/app/constants.js +2 -0
  44. package/src/app/icons/AddItemIcon.jsx +16 -0
  45. package/src/app/icons/AlertIcon.jsx +33 -0
  46. package/src/app/icons/CollapsedIcon.jsx +15 -0
  47. package/src/app/icons/CommitIcon.jsx +45 -0
  48. package/src/app/icons/DJLogo.jsx +36 -0
  49. package/src/app/icons/DeleteIcon.jsx +21 -0
  50. package/src/app/icons/DiffIcon.jsx +63 -0
  51. package/src/app/icons/EditIcon.jsx +18 -0
  52. package/src/app/icons/ExpandedIcon.jsx +15 -0
  53. package/src/app/icons/EyeIcon.jsx +20 -0
  54. package/src/app/icons/FilterIcon.jsx +7 -0
  55. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  56. package/src/app/icons/InvalidIcon.jsx +16 -0
  57. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  58. package/src/app/icons/LoadingIcon.jsx +14 -0
  59. package/src/app/icons/NodeIcon.jsx +49 -0
  60. package/src/app/icons/NotificationIcon.jsx +27 -0
  61. package/src/app/icons/PythonIcon.jsx +14 -0
  62. package/src/app/icons/SettingsIcon.jsx +28 -0
  63. package/src/app/icons/TableIcon.jsx +14 -0
  64. package/src/app/icons/ValidIcon.jsx +16 -0
  65. package/src/app/index.tsx +138 -38
  66. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  67. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  68. package/src/app/pages/AddEditNodePage/CustomMetadataField.jsx +144 -0
  69. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  70. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  71. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +64 -0
  72. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  73. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  74. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  75. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  76. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  77. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  78. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  79. package/src/app/pages/AddEditNodePage/OwnersField.jsx +53 -0
  80. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  81. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  82. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  83. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +110 -0
  84. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +291 -0
  85. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  86. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  87. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  88. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  89. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  90. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  91. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  92. package/src/app/pages/AddEditNodePage/index.jsx +545 -0
  93. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  94. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  95. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  96. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  97. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  98. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  99. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  100. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  101. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  102. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  103. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  104. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  105. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  106. package/src/app/pages/LoginPage/index.jsx +17 -0
  107. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  108. package/src/app/pages/NamespacePage/Explorer.jsx +232 -0
  109. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  110. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +27 -0
  111. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  112. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  113. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  114. package/src/app/pages/NamespacePage/__tests__/AddNamespacePopover.test.jsx +283 -0
  115. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +331 -0
  116. package/src/app/pages/NamespacePage/index.jsx +354 -42
  117. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  118. package/src/app/pages/NodePage/AddComplexDimensionLinkPopover.jsx +367 -0
  119. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  120. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  121. package/src/app/pages/NodePage/ClientCodePopover.jsx +116 -0
  122. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  123. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  124. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  125. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  126. package/src/app/pages/NodePage/ManageDimensionLinksDialog.jsx +526 -0
  127. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  128. package/src/app/pages/NodePage/NodeColumnTab.jsx +421 -30
  129. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +155 -0
  130. package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
  131. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  132. package/src/app/pages/NodePage/NodeInfoTab.jsx +404 -49
  133. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  134. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  135. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  136. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  137. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  138. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  139. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  140. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  141. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  142. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  143. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  144. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  145. package/src/app/pages/NodePage/__tests__/AddComplexDimensionLinkPopover.test.jsx +459 -0
  146. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  147. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  148. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  149. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +144 -0
  150. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +132 -0
  151. package/src/app/pages/NodePage/__tests__/ManageDimensionLinksDialog.test.jsx +390 -0
  152. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  153. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +157 -0
  154. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  155. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  156. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  157. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +892 -0
  158. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  159. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  160. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  161. package/src/app/pages/NodePage/index.jsx +186 -45
  162. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  163. package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
  164. package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
  165. package/src/app/pages/NotificationsPage/index.jsx +136 -0
  166. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  167. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  168. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  169. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  170. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  171. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  172. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  173. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  174. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  175. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  176. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  177. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  178. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  179. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  180. package/src/app/pages/OverviewPage/index.jsx +22 -0
  181. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  182. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +112 -0
  183. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  184. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  185. package/src/app/pages/Root/__tests__/index.test.jsx +44 -0
  186. package/src/app/pages/Root/index.tsx +92 -10
  187. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  188. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  189. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  190. package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
  191. package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
  192. package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
  193. package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
  194. package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
  195. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
  196. package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
  197. package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
  198. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
  199. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +187 -0
  200. package/src/app/pages/SettingsPage/index.jsx +148 -0
  201. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  202. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  203. package/src/app/pages/TagPage/index.jsx +79 -0
  204. package/src/app/providers/UserProvider.tsx +78 -0
  205. package/src/app/services/DJService.js +1487 -21
  206. package/src/app/services/__tests__/DJService.test.jsx +2194 -0
  207. package/src/app/utils/__tests__/date.test.js +198 -0
  208. package/src/app/utils/date.js +65 -0
  209. package/src/index.tsx +1 -0
  210. package/src/mocks/mockNodes.jsx +1477 -0
  211. package/src/setupTests.ts +31 -1
  212. package/src/styles/dag.css +117 -5
  213. package/src/styles/index.css +1028 -31
  214. package/src/styles/loading.css +34 -0
  215. package/src/styles/login.css +81 -0
  216. package/src/styles/nav-bar.css +274 -0
  217. package/src/styles/node-creation.scss +276 -0
  218. package/src/styles/node-list.css +4 -0
  219. package/src/styles/overview.css +72 -0
  220. package/src/styles/settings.css +787 -0
  221. package/src/styles/sorted-table.css +15 -0
  222. package/src/styles/styles.scss +44 -0
  223. package/src/styles/styles.scss.d.ts +9 -0
  224. package/src/utils/form.jsx +23 -0
  225. package/webpack.config.js +20 -7
  226. package/.babelrc +0 -4
  227. package/.env.local +0 -4
  228. package/.env.production +0 -1
  229. package/.github/pull_request_template.md +0 -11
  230. package/.github/workflows/ci.yml +0 -33
  231. package/.vscode/extensions.json +0 -7
  232. package/.vscode/launch.json +0 -15
  233. package/.vscode/settings.json +0 -25
  234. package/Dockerfile +0 -7
  235. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  236. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  237. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  238. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,94 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import userEvent from '@testing-library/user-event';
5
+ import { render } from '../../../setupTests';
6
+ import DJClientContext from '../../providers/djclient';
7
+ import NodeListActions from '../NodeListActions';
8
+
9
+ describe('<NodeListActions />', () => {
10
+ beforeEach(() => {
11
+ fetchMock.resetMocks();
12
+ jest.clearAllMocks();
13
+ window.scrollTo = jest.fn();
14
+ });
15
+
16
+ const renderElement = djClient => {
17
+ return render(
18
+ <DJClientContext.Provider value={djClient}>
19
+ <NodeListActions nodeName="default.hard_hat" />
20
+ </DJClientContext.Provider>,
21
+ );
22
+ };
23
+
24
+ const initializeMockDJClient = () => {
25
+ return {
26
+ DataJunctionAPI: {
27
+ deactivate: jest.fn(),
28
+ },
29
+ };
30
+ };
31
+
32
+ it('deletes a node when clicked', async () => {
33
+ global.confirm = () => true;
34
+ const mockDjClient = initializeMockDJClient();
35
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
36
+ status: 204,
37
+ json: { name: 'source.warehouse.schema.some_table' },
38
+ });
39
+
40
+ renderElement(mockDjClient);
41
+
42
+ await userEvent.click(screen.getByRole('button'));
43
+
44
+ await waitFor(() => {
45
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
46
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
47
+ 'default.hard_hat',
48
+ );
49
+ });
50
+ expect(
51
+ screen.getByText('Successfully deleted node default.hard_hat'),
52
+ ).toBeInTheDocument();
53
+ }, 60000);
54
+
55
+ it('skips a node deletion during confirm', async () => {
56
+ global.confirm = () => false;
57
+ const mockDjClient = initializeMockDJClient();
58
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
59
+ status: 204,
60
+ json: { name: 'source.warehouse.schema.some_table' },
61
+ });
62
+
63
+ renderElement(mockDjClient);
64
+
65
+ await userEvent.click(screen.getByRole('button'));
66
+
67
+ await waitFor(() => {
68
+ expect(mockDjClient.DataJunctionAPI.deactivate).not.toBeCalled();
69
+ });
70
+ }, 60000);
71
+
72
+ it('fail deleting a node when clicked', async () => {
73
+ global.confirm = () => true;
74
+ const mockDjClient = initializeMockDJClient();
75
+ mockDjClient.DataJunctionAPI.deactivate.mockReturnValue({
76
+ status: 777,
77
+ json: { message: 'source.warehouse.schema.some_table' },
78
+ });
79
+
80
+ renderElement(mockDjClient);
81
+
82
+ await userEvent.click(screen.getByRole('button'));
83
+
84
+ await waitFor(() => {
85
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalled();
86
+ expect(mockDjClient.DataJunctionAPI.deactivate).toBeCalledWith(
87
+ 'default.hard_hat',
88
+ );
89
+ });
90
+ expect(
91
+ screen.getByText('source.warehouse.schema.some_table'),
92
+ ).toBeInTheDocument();
93
+ }, 60000);
94
+ });
@@ -0,0 +1,263 @@
1
+ import React from 'react';
2
+ import { render, fireEvent, waitFor, act } from '@testing-library/react';
3
+ import NodeMaterializationDelete from '../NodeMaterializationDelete';
4
+ import DJClientContext from '../../providers/djclient';
5
+
6
+ // Mock window.location.reload
7
+ delete window.location;
8
+ window.location = { reload: jest.fn() };
9
+
10
+ // Mock window.confirm
11
+ window.confirm = jest.fn();
12
+
13
+ const mockDjClient = {
14
+ DataJunctionAPI: {
15
+ deleteMaterialization: jest.fn(),
16
+ },
17
+ };
18
+
19
+ describe('<NodeMaterializationDelete />', () => {
20
+ beforeEach(() => {
21
+ jest.clearAllMocks();
22
+ window.confirm.mockReturnValue(true);
23
+ });
24
+
25
+ const defaultProps = {
26
+ nodeName: 'default.test_node',
27
+ materializationName: 'test_materialization',
28
+ nodeVersion: 'v1.0',
29
+ };
30
+
31
+ it('renders delete button', () => {
32
+ const { container } = render(
33
+ <DJClientContext.Provider value={mockDjClient}>
34
+ <NodeMaterializationDelete {...defaultProps} />
35
+ </DJClientContext.Provider>,
36
+ );
37
+
38
+ const deleteButton = container.querySelector('button[type="submit"]');
39
+ expect(deleteButton).toBeInTheDocument();
40
+ });
41
+
42
+ it('renders with null nodeVersion', () => {
43
+ const { container } = render(
44
+ <DJClientContext.Provider value={mockDjClient}>
45
+ <NodeMaterializationDelete
46
+ nodeName="default.test_node"
47
+ materializationName="test_materialization"
48
+ nodeVersion={null}
49
+ />
50
+ </DJClientContext.Provider>,
51
+ );
52
+
53
+ const deleteButton = container.querySelector('button[type="submit"]');
54
+ expect(deleteButton).toBeInTheDocument();
55
+ });
56
+
57
+ it('shows confirm dialog when delete button is clicked', async () => {
58
+ const { container } = render(
59
+ <DJClientContext.Provider value={mockDjClient}>
60
+ <NodeMaterializationDelete {...defaultProps} />
61
+ </DJClientContext.Provider>,
62
+ );
63
+
64
+ const deleteButton = container.querySelector('button[type="submit"]');
65
+
66
+ await act(async () => {
67
+ fireEvent.click(deleteButton);
68
+ });
69
+
70
+ expect(window.confirm).toHaveBeenCalledWith(
71
+ expect.stringContaining(
72
+ 'Deleting materialization job test_materialization',
73
+ ),
74
+ );
75
+ });
76
+
77
+ it('does not call deleteMaterialization when user cancels confirm', async () => {
78
+ window.confirm.mockReturnValueOnce(false);
79
+
80
+ const { container } = render(
81
+ <DJClientContext.Provider value={mockDjClient}>
82
+ <NodeMaterializationDelete {...defaultProps} />
83
+ </DJClientContext.Provider>,
84
+ );
85
+
86
+ const deleteButton = container.querySelector('button[type="submit"]');
87
+
88
+ await act(async () => {
89
+ fireEvent.click(deleteButton);
90
+ });
91
+
92
+ expect(window.confirm).toHaveBeenCalled();
93
+ expect(
94
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
95
+ ).not.toHaveBeenCalled();
96
+ });
97
+
98
+ it('calls deleteMaterialization with correct params on success - status 200', async () => {
99
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
100
+ status: 200,
101
+ json: { message: 'Deleted successfully' },
102
+ });
103
+
104
+ const { container, getByText } = render(
105
+ <DJClientContext.Provider value={mockDjClient}>
106
+ <NodeMaterializationDelete {...defaultProps} />
107
+ </DJClientContext.Provider>,
108
+ );
109
+
110
+ const deleteButton = container.querySelector('button[type="submit"]');
111
+
112
+ await act(async () => {
113
+ fireEvent.click(deleteButton);
114
+ });
115
+
116
+ await waitFor(() => {
117
+ expect(
118
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
119
+ ).toHaveBeenCalledWith(
120
+ 'default.test_node',
121
+ 'test_materialization',
122
+ 'v1.0',
123
+ );
124
+ expect(
125
+ getByText(/Successfully deleted materialization job/),
126
+ ).toBeInTheDocument();
127
+ expect(window.location.reload).toHaveBeenCalled();
128
+ });
129
+ });
130
+
131
+ it('calls deleteMaterialization with correct params on success - status 201', async () => {
132
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
133
+ status: 201,
134
+ json: { message: 'Deleted successfully' },
135
+ });
136
+
137
+ const { container } = render(
138
+ <DJClientContext.Provider value={mockDjClient}>
139
+ <NodeMaterializationDelete {...defaultProps} />
140
+ </DJClientContext.Provider>,
141
+ );
142
+
143
+ const deleteButton = container.querySelector('button[type="submit"]');
144
+
145
+ await act(async () => {
146
+ fireEvent.click(deleteButton);
147
+ });
148
+
149
+ await waitFor(() => {
150
+ expect(
151
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
152
+ ).toHaveBeenCalled();
153
+ expect(window.location.reload).toHaveBeenCalled();
154
+ });
155
+ });
156
+
157
+ it('calls deleteMaterialization with correct params on success - status 204', async () => {
158
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
159
+ status: 204,
160
+ json: {},
161
+ });
162
+
163
+ const { container } = render(
164
+ <DJClientContext.Provider value={mockDjClient}>
165
+ <NodeMaterializationDelete {...defaultProps} />
166
+ </DJClientContext.Provider>,
167
+ );
168
+
169
+ const deleteButton = container.querySelector('button[type="submit"]');
170
+
171
+ await act(async () => {
172
+ fireEvent.click(deleteButton);
173
+ });
174
+
175
+ await waitFor(() => {
176
+ expect(
177
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
178
+ ).toHaveBeenCalled();
179
+ expect(window.location.reload).toHaveBeenCalled();
180
+ });
181
+ });
182
+
183
+ it('displays error message when deletion fails', async () => {
184
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
185
+ status: 500,
186
+ json: { message: 'Internal server error' },
187
+ });
188
+
189
+ const { container, getByText } = render(
190
+ <DJClientContext.Provider value={mockDjClient}>
191
+ <NodeMaterializationDelete {...defaultProps} />
192
+ </DJClientContext.Provider>,
193
+ );
194
+
195
+ const deleteButton = container.querySelector('button[type="submit"]');
196
+
197
+ await act(async () => {
198
+ fireEvent.click(deleteButton);
199
+ });
200
+
201
+ await waitFor(() => {
202
+ expect(
203
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
204
+ ).toHaveBeenCalled();
205
+ expect(getByText('Internal server error')).toBeInTheDocument();
206
+ expect(window.location.reload).not.toHaveBeenCalled();
207
+ });
208
+ });
209
+
210
+ it('hides delete button after successful deletion', async () => {
211
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
212
+ status: 200,
213
+ json: { message: 'Deleted successfully' },
214
+ });
215
+
216
+ const { container } = render(
217
+ <DJClientContext.Provider value={mockDjClient}>
218
+ <NodeMaterializationDelete {...defaultProps} />
219
+ </DJClientContext.Provider>,
220
+ );
221
+
222
+ const deleteButton = container.querySelector('button[type="submit"]');
223
+ expect(deleteButton).toBeInTheDocument();
224
+
225
+ await act(async () => {
226
+ fireEvent.click(deleteButton);
227
+ });
228
+
229
+ await waitFor(() => {
230
+ expect(
231
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
232
+ ).toHaveBeenCalled();
233
+ });
234
+ });
235
+
236
+ it('passes null nodeVersion to deleteMaterialization when not provided', async () => {
237
+ mockDjClient.DataJunctionAPI.deleteMaterialization.mockResolvedValue({
238
+ status: 200,
239
+ json: { message: 'Deleted successfully' },
240
+ });
241
+
242
+ const { container } = render(
243
+ <DJClientContext.Provider value={mockDjClient}>
244
+ <NodeMaterializationDelete
245
+ nodeName="default.test_node"
246
+ materializationName="test_mat"
247
+ />
248
+ </DJClientContext.Provider>,
249
+ );
250
+
251
+ const deleteButton = container.querySelector('button[type="submit"]');
252
+
253
+ await act(async () => {
254
+ fireEvent.click(deleteButton);
255
+ });
256
+
257
+ await waitFor(() => {
258
+ expect(
259
+ mockDjClient.DataJunctionAPI.deleteMaterialization,
260
+ ).toHaveBeenCalledWith('default.test_node', 'test_mat', null);
261
+ });
262
+ });
263
+ });
@@ -0,0 +1,313 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import NotificationBell from '../NotificationBell';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import { UserProvider } from '../../providers/UserProvider';
6
+
7
+ describe('<NotificationBell />', () => {
8
+ const mockNotifications = [
9
+ {
10
+ id: 1,
11
+ entity_type: 'node',
12
+ entity_name: 'default.metrics.revenue',
13
+ node: 'default.metrics.revenue',
14
+ activity_type: 'update',
15
+ user: 'alice',
16
+ created_at: new Date().toISOString(),
17
+ details: { version: 'v2' },
18
+ },
19
+ {
20
+ id: 2,
21
+ entity_type: 'node',
22
+ entity_name: 'default.dimensions.country',
23
+ node: 'default.dimensions.country',
24
+ activity_type: 'create',
25
+ user: 'bob',
26
+ created_at: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
27
+ details: { version: 'v1' },
28
+ },
29
+ ];
30
+
31
+ const mockNodes = [
32
+ {
33
+ name: 'default.metrics.revenue',
34
+ type: 'metric',
35
+ current: { displayName: 'Revenue Metric' },
36
+ },
37
+ {
38
+ name: 'default.dimensions.country',
39
+ type: 'dimension',
40
+ current: { displayName: 'Country' },
41
+ },
42
+ ];
43
+
44
+ const createMockDjClient = (overrides = {}) => ({
45
+ whoami: jest.fn().mockResolvedValue({
46
+ id: 1,
47
+ username: 'testuser',
48
+ last_viewed_notifications_at: null,
49
+ }),
50
+ getSubscribedHistory: jest.fn().mockResolvedValue(mockNotifications),
51
+ getNodesByNames: jest.fn().mockResolvedValue(mockNodes),
52
+ markNotificationsRead: jest.fn().mockResolvedValue({}),
53
+ ...overrides,
54
+ });
55
+
56
+ const renderWithContext = (mockDjClient: any, props = {}) => {
57
+ return render(
58
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
59
+ <UserProvider>
60
+ <NotificationBell {...props} />
61
+ </UserProvider>
62
+ </DJClientContext.Provider>,
63
+ );
64
+ };
65
+
66
+ beforeEach(() => {
67
+ jest.clearAllMocks();
68
+ });
69
+
70
+ it('renders the notification bell button', async () => {
71
+ const mockDjClient = createMockDjClient();
72
+ renderWithContext(mockDjClient);
73
+
74
+ const button = screen.getByRole('button');
75
+ expect(button).toBeInTheDocument();
76
+ });
77
+
78
+ it('shows unread badge when there are unread notifications', async () => {
79
+ const mockDjClient = createMockDjClient();
80
+ renderWithContext(mockDjClient);
81
+
82
+ // Wait for notifications to load
83
+ await waitFor(() => {
84
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
85
+ });
86
+
87
+ // Badge should show count of 2 (all notifications are unread since last_viewed is null)
88
+ const badge = await screen.findByText('2');
89
+ expect(badge).toHaveClass('notification-badge');
90
+ });
91
+
92
+ it('does not show badge when all notifications have been viewed', async () => {
93
+ const mockDjClient = createMockDjClient({
94
+ whoami: jest.fn().mockResolvedValue({
95
+ id: 1,
96
+ username: 'testuser',
97
+ // Set last_viewed to future date so all notifications are "read"
98
+ last_viewed_notifications_at: new Date(
99
+ Date.now() + 10000,
100
+ ).toISOString(),
101
+ }),
102
+ });
103
+ renderWithContext(mockDjClient);
104
+
105
+ await waitFor(() => {
106
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
107
+ });
108
+
109
+ // Badge should not be present (no unread count shown)
110
+ const badge = document.querySelector('.notification-badge');
111
+ expect(badge).toBeNull();
112
+ });
113
+
114
+ it('opens dropdown when bell is clicked', async () => {
115
+ const mockDjClient = createMockDjClient();
116
+ renderWithContext(mockDjClient);
117
+
118
+ await waitFor(() => {
119
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
120
+ });
121
+
122
+ const button = screen.getByRole('button');
123
+ fireEvent.click(button);
124
+
125
+ expect(screen.getByText('Updates')).toBeInTheDocument();
126
+ });
127
+
128
+ it('displays notifications in the dropdown', async () => {
129
+ const mockDjClient = createMockDjClient();
130
+ renderWithContext(mockDjClient);
131
+
132
+ await waitFor(() => {
133
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
134
+ });
135
+
136
+ const button = screen.getByRole('button');
137
+ fireEvent.click(button);
138
+
139
+ // Check that display names are shown
140
+ expect(screen.getByText('Revenue Metric')).toBeInTheDocument();
141
+ expect(screen.getByText('Country')).toBeInTheDocument();
142
+
143
+ // Check that entity names are shown below
144
+ expect(screen.getByText('default.metrics.revenue')).toBeInTheDocument();
145
+ expect(screen.getByText('default.dimensions.country')).toBeInTheDocument();
146
+ });
147
+
148
+ it('shows empty state when no notifications', async () => {
149
+ const mockDjClient = createMockDjClient({
150
+ getSubscribedHistory: jest.fn().mockResolvedValue([]),
151
+ });
152
+ renderWithContext(mockDjClient);
153
+
154
+ await waitFor(() => {
155
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
156
+ });
157
+
158
+ const button = screen.getByRole('button');
159
+ fireEvent.click(button);
160
+
161
+ expect(screen.getByText('No updates on watched nodes')).toBeInTheDocument();
162
+ });
163
+
164
+ it('marks notifications as read when dropdown is opened', async () => {
165
+ const mockDjClient = createMockDjClient();
166
+ renderWithContext(mockDjClient);
167
+
168
+ await waitFor(() => {
169
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
170
+ });
171
+
172
+ const button = screen.getByRole('button');
173
+ fireEvent.click(button);
174
+
175
+ expect(mockDjClient.markNotificationsRead).toHaveBeenCalled();
176
+ });
177
+
178
+ it('does not mark as read if already all read', async () => {
179
+ const mockDjClient = createMockDjClient({
180
+ whoami: jest.fn().mockResolvedValue({
181
+ id: 1,
182
+ username: 'testuser',
183
+ last_viewed_notifications_at: new Date(
184
+ Date.now() + 10000,
185
+ ).toISOString(),
186
+ }),
187
+ });
188
+ renderWithContext(mockDjClient);
189
+
190
+ await waitFor(() => {
191
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
192
+ });
193
+
194
+ const button = screen.getByRole('button');
195
+ fireEvent.click(button);
196
+
197
+ // Should not call markNotificationsRead since unreadCount is 0
198
+ expect(mockDjClient.markNotificationsRead).not.toHaveBeenCalled();
199
+ });
200
+
201
+ it('shows View all link when there are notifications', async () => {
202
+ const mockDjClient = createMockDjClient();
203
+ renderWithContext(mockDjClient);
204
+
205
+ await waitFor(() => {
206
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
207
+ });
208
+
209
+ const button = screen.getByRole('button');
210
+ fireEvent.click(button);
211
+
212
+ const viewAllLink = screen.getByText('View all');
213
+ expect(viewAllLink).toHaveAttribute('href', '/notifications');
214
+ });
215
+
216
+ it('calls onDropdownToggle when dropdown state changes', async () => {
217
+ const mockDjClient = createMockDjClient();
218
+ const onDropdownToggle = jest.fn();
219
+
220
+ render(
221
+ <DJClientContext.Provider
222
+ value={{ DataJunctionAPI: mockDjClient as any }}
223
+ >
224
+ <UserProvider>
225
+ <NotificationBell onDropdownToggle={onDropdownToggle} />
226
+ </UserProvider>
227
+ </DJClientContext.Provider>,
228
+ );
229
+
230
+ await waitFor(() => {
231
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
232
+ });
233
+
234
+ const button = screen.getByRole('button');
235
+ fireEvent.click(button);
236
+
237
+ expect(onDropdownToggle).toHaveBeenCalledWith(true);
238
+ });
239
+
240
+ it('closes dropdown when forceClose becomes true', async () => {
241
+ const mockDjClient = createMockDjClient();
242
+
243
+ const { rerender } = render(
244
+ <DJClientContext.Provider
245
+ value={{ DataJunctionAPI: mockDjClient as any }}
246
+ >
247
+ <UserProvider>
248
+ <NotificationBell forceClose={false} />
249
+ </UserProvider>
250
+ </DJClientContext.Provider>,
251
+ );
252
+
253
+ await waitFor(() => {
254
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
255
+ });
256
+
257
+ // Open the dropdown
258
+ const button = screen.getByRole('button');
259
+ fireEvent.click(button);
260
+
261
+ // Verify dropdown is open
262
+ expect(screen.getByText('Updates')).toBeInTheDocument();
263
+
264
+ // Rerender with forceClose=true
265
+ rerender(
266
+ <DJClientContext.Provider
267
+ value={{ DataJunctionAPI: mockDjClient as any }}
268
+ >
269
+ <UserProvider>
270
+ <NotificationBell forceClose={true} />
271
+ </UserProvider>
272
+ </DJClientContext.Provider>,
273
+ );
274
+
275
+ // Dropdown should be closed
276
+ expect(screen.queryByText('Updates')).not.toBeInTheDocument();
277
+ });
278
+
279
+ it('closes dropdown when clicking outside', async () => {
280
+ const mockDjClient = createMockDjClient();
281
+ const onDropdownToggle = jest.fn();
282
+
283
+ render(
284
+ <DJClientContext.Provider
285
+ value={{ DataJunctionAPI: mockDjClient as any }}
286
+ >
287
+ <UserProvider>
288
+ <NotificationBell onDropdownToggle={onDropdownToggle} />
289
+ </UserProvider>
290
+ </DJClientContext.Provider>,
291
+ );
292
+
293
+ await waitFor(() => {
294
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
295
+ });
296
+
297
+ // Open the dropdown
298
+ const button = screen.getByRole('button');
299
+ fireEvent.click(button);
300
+
301
+ // Verify dropdown is open
302
+ expect(screen.getByText('Updates')).toBeInTheDocument();
303
+
304
+ // Click outside the dropdown
305
+ fireEvent.click(document.body);
306
+
307
+ // Dropdown should be closed
308
+ expect(screen.queryByText('Updates')).not.toBeInTheDocument();
309
+
310
+ // onDropdownToggle should have been called with false
311
+ expect(onDropdownToggle).toHaveBeenCalledWith(false);
312
+ });
313
+ });