datajunction-ui 0.0.1-rc.9 → 0.0.2-0.dev1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (244) hide show
  1. package/.env +2 -0
  2. package/.prettierignore +3 -1
  3. package/Makefile +9 -0
  4. package/cleanup-deps.sh +70 -0
  5. package/dj-logo.svg +10 -0
  6. package/package.json +53 -14
  7. package/public/favicon.ico +0 -0
  8. package/public/index.html +1 -1
  9. package/runit.sh +30 -0
  10. package/runit2.sh +30 -0
  11. package/src/__tests__/reportWebVitals.test.jsx +44 -0
  12. package/src/app/__tests__/__snapshots__/index.test.tsx.snap +5 -109
  13. package/src/app/components/AddNodeDropdown.jsx +44 -0
  14. package/src/app/components/ListGroupItem.jsx +9 -1
  15. package/src/app/components/NamespaceHeader.jsx +4 -13
  16. package/src/app/components/NodeListActions.jsx +69 -0
  17. package/src/app/components/NodeMaterializationDelete.jsx +90 -0
  18. package/src/app/components/NotificationBell.tsx +223 -0
  19. package/src/app/components/QueryInfo.jsx +172 -0
  20. package/src/app/components/Search.jsx +94 -0
  21. package/src/app/components/Tab.jsx +8 -1
  22. package/src/app/components/ToggleSwitch.jsx +20 -0
  23. package/src/app/components/UserMenu.tsx +100 -0
  24. package/src/app/components/__tests__/NodeListActions.test.jsx +94 -0
  25. package/src/app/components/__tests__/NodeMaterializationDelete.test.jsx +263 -0
  26. package/src/app/components/__tests__/NotificationBell.test.tsx +302 -0
  27. package/src/app/components/__tests__/QueryInfo.test.jsx +183 -0
  28. package/src/app/components/__tests__/Search.test.jsx +307 -0
  29. package/src/app/components/__tests__/Tab.test.jsx +27 -0
  30. package/src/app/components/__tests__/ToggleSwitch.test.jsx +43 -0
  31. package/src/app/components/__tests__/UserMenu.test.tsx +241 -0
  32. package/src/app/components/__tests__/__snapshots__/ListGroupItem.test.tsx.snap +8 -3
  33. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +2 -18
  34. package/src/app/components/djgraph/Collapse.jsx +47 -0
  35. package/src/app/components/djgraph/DJNode.jsx +61 -83
  36. package/src/app/components/djgraph/DJNodeColumns.jsx +75 -0
  37. package/src/app/components/djgraph/DJNodeDimensions.jsx +75 -0
  38. package/src/app/components/djgraph/LayoutFlow.jsx +106 -0
  39. package/src/app/components/djgraph/__tests__/Collapse.test.jsx +51 -0
  40. package/src/app/components/djgraph/__tests__/DJNodeColumns.test.jsx +83 -0
  41. package/src/app/components/djgraph/__tests__/DJNodeDimensions.test.jsx +118 -0
  42. package/src/app/components/djgraph/__tests__/__snapshots__/DJNode.test.tsx.snap +84 -40
  43. package/src/app/components/forms/Action.jsx +8 -0
  44. package/src/app/components/forms/NodeNameField.jsx +64 -0
  45. package/src/app/components/search.css +17 -0
  46. package/src/app/constants.js +2 -0
  47. package/src/app/icons/AddItemIcon.jsx +16 -0
  48. package/src/app/icons/AlertIcon.jsx +33 -0
  49. package/src/app/icons/CollapsedIcon.jsx +15 -0
  50. package/src/app/icons/CommitIcon.jsx +45 -0
  51. package/src/app/icons/DJLogo.jsx +36 -0
  52. package/src/app/icons/DeleteIcon.jsx +21 -0
  53. package/src/app/icons/DiffIcon.jsx +63 -0
  54. package/src/app/icons/EditIcon.jsx +18 -0
  55. package/src/app/icons/ExpandedIcon.jsx +15 -0
  56. package/src/app/icons/EyeIcon.jsx +20 -0
  57. package/src/app/icons/FilterIcon.jsx +7 -0
  58. package/src/app/icons/HorizontalHierarchyIcon.jsx +15 -0
  59. package/src/app/icons/InvalidIcon.jsx +16 -0
  60. package/src/app/icons/JupyterExportIcon.jsx +25 -0
  61. package/src/app/icons/LoadingIcon.jsx +14 -0
  62. package/src/app/icons/NodeIcon.jsx +49 -0
  63. package/src/app/icons/NotificationIcon.jsx +27 -0
  64. package/src/app/icons/PythonIcon.jsx +14 -0
  65. package/src/app/icons/SettingsIcon.jsx +28 -0
  66. package/src/app/icons/TableIcon.jsx +14 -0
  67. package/src/app/icons/ValidIcon.jsx +16 -0
  68. package/src/app/icons/WrenchIcon.jsx +36 -0
  69. package/src/app/index.tsx +130 -37
  70. package/src/app/pages/AddEditNodePage/AlertMessage.jsx +10 -0
  71. package/src/app/pages/AddEditNodePage/ColumnMetadata.jsx +61 -0
  72. package/src/app/pages/AddEditNodePage/ColumnsMetadataInput.jsx +72 -0
  73. package/src/app/pages/AddEditNodePage/ColumnsSelect.jsx +84 -0
  74. package/src/app/pages/AddEditNodePage/CustomMetadataField.jsx +144 -0
  75. package/src/app/pages/AddEditNodePage/DescriptionField.jsx +17 -0
  76. package/src/app/pages/AddEditNodePage/DisplayNameField.jsx +16 -0
  77. package/src/app/pages/AddEditNodePage/ExperimentationExtension.jsx +338 -0
  78. package/src/app/pages/AddEditNodePage/FormikSelect.jsx +64 -0
  79. package/src/app/pages/AddEditNodePage/FullNameField.jsx +38 -0
  80. package/src/app/pages/AddEditNodePage/Loadable.jsx +20 -0
  81. package/src/app/pages/AddEditNodePage/MetricMetadataFields.jsx +75 -0
  82. package/src/app/pages/AddEditNodePage/MetricQueryField.jsx +71 -0
  83. package/src/app/pages/AddEditNodePage/NamespaceField.jsx +40 -0
  84. package/src/app/pages/AddEditNodePage/NodeModeField.jsx +14 -0
  85. package/src/app/pages/AddEditNodePage/NodeQueryField.jsx +94 -0
  86. package/src/app/pages/AddEditNodePage/OwnersField.jsx +54 -0
  87. package/src/app/pages/AddEditNodePage/RequiredDimensionsSelect.jsx +54 -0
  88. package/src/app/pages/AddEditNodePage/TagsField.jsx +47 -0
  89. package/src/app/pages/AddEditNodePage/UpstreamNodeField.jsx +49 -0
  90. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormFailed.test.jsx +110 -0
  91. package/src/app/pages/AddEditNodePage/__tests__/AddEditNodePageFormSuccess.test.jsx +291 -0
  92. package/src/app/pages/AddEditNodePage/__tests__/FormikSelect.test.jsx +75 -0
  93. package/src/app/pages/AddEditNodePage/__tests__/FullNameField.test.jsx +31 -0
  94. package/src/app/pages/AddEditNodePage/__tests__/NodeQueryField.test.jsx +30 -0
  95. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormFailed.test.jsx.snap +54 -0
  96. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/AddEditNodePageFormSuccess.test.jsx.snap +3 -0
  97. package/src/app/pages/AddEditNodePage/__tests__/__snapshots__/index.test.jsx.snap +3 -0
  98. package/src/app/pages/AddEditNodePage/__tests__/index.test.jsx +224 -0
  99. package/src/app/pages/AddEditNodePage/index.jsx +545 -0
  100. package/src/app/pages/AddEditTagPage/Loadable.jsx +16 -0
  101. package/src/app/pages/AddEditTagPage/__tests__/AddEditTagPage.test.jsx +107 -0
  102. package/src/app/pages/AddEditTagPage/index.jsx +132 -0
  103. package/src/app/pages/CubeBuilderPage/DimensionsSelect.jsx +152 -0
  104. package/src/app/pages/CubeBuilderPage/Loadable.jsx +16 -0
  105. package/src/app/pages/CubeBuilderPage/MetricsSelect.jsx +75 -0
  106. package/src/app/pages/CubeBuilderPage/__tests__/index.test.jsx +373 -0
  107. package/src/app/pages/CubeBuilderPage/index.jsx +291 -0
  108. package/src/app/pages/LoginPage/LoginForm.jsx +124 -0
  109. package/src/app/pages/LoginPage/SignupForm.jsx +156 -0
  110. package/src/app/pages/LoginPage/__tests__/index.test.jsx +97 -0
  111. package/src/app/pages/LoginPage/assets/sign-in-with-github.png +0 -0
  112. package/src/app/pages/LoginPage/assets/sign-in-with-google.png +0 -0
  113. package/src/app/pages/LoginPage/index.jsx +17 -0
  114. package/src/app/pages/NamespacePage/AddNamespacePopover.jsx +85 -0
  115. package/src/app/pages/NamespacePage/Explorer.jsx +232 -0
  116. package/src/app/pages/NamespacePage/FieldControl.jsx +21 -0
  117. package/src/app/pages/NamespacePage/NodeModeSelect.jsx +27 -0
  118. package/src/app/pages/NamespacePage/NodeTypeSelect.jsx +30 -0
  119. package/src/app/pages/NamespacePage/TagSelect.jsx +44 -0
  120. package/src/app/pages/NamespacePage/UserSelect.jsx +47 -0
  121. package/src/app/pages/NamespacePage/__tests__/AddNamespacePopover.test.jsx +283 -0
  122. package/src/app/pages/NamespacePage/__tests__/index.test.jsx +331 -0
  123. package/src/app/pages/NamespacePage/index.jsx +356 -42
  124. package/src/app/pages/NodePage/AddBackfillPopover.jsx +165 -0
  125. package/src/app/pages/NodePage/AddComplexDimensionLinkPopover.jsx +367 -0
  126. package/src/app/pages/NodePage/AddMaterializationPopover.jsx +222 -0
  127. package/src/app/pages/NodePage/AvailabilityStateBlock.jsx +67 -0
  128. package/src/app/pages/NodePage/ClientCodePopover.jsx +94 -0
  129. package/src/app/pages/NodePage/DimensionFilter.jsx +86 -0
  130. package/src/app/pages/NodePage/EditColumnDescriptionPopover.jsx +116 -0
  131. package/src/app/pages/NodePage/EditColumnPopover.jsx +116 -0
  132. package/src/app/pages/NodePage/LinkDimensionPopover.jsx +164 -0
  133. package/src/app/pages/NodePage/ManageDimensionLinksDialog.jsx +526 -0
  134. package/src/app/pages/NodePage/MaterializationConfigField.jsx +60 -0
  135. package/src/app/pages/NodePage/NodeColumnTab.jsx +421 -30
  136. package/src/app/pages/NodePage/NodeDependenciesTab.jsx +153 -0
  137. package/src/app/pages/NodePage/NodeGraphTab.jsx +119 -148
  138. package/src/app/pages/NodePage/NodeHistory.jsx +236 -0
  139. package/src/app/pages/NodePage/NodeInfoTab.jsx +346 -49
  140. package/src/app/pages/NodePage/NodeLineageTab.jsx +84 -0
  141. package/src/app/pages/NodePage/NodeMaterializationTab.jsx +585 -0
  142. package/src/app/pages/NodePage/NodeRevisionMaterializationTab.jsx +58 -0
  143. package/src/app/pages/NodePage/NodeStatus.jsx +100 -31
  144. package/src/app/pages/NodePage/NodeValidateTab.jsx +367 -0
  145. package/src/app/pages/NodePage/NodesWithDimension.jsx +42 -0
  146. package/src/app/pages/NodePage/NotebookDownload.jsx +36 -0
  147. package/src/app/pages/NodePage/PartitionColumnPopover.jsx +151 -0
  148. package/src/app/pages/NodePage/PartitionValueForm.jsx +60 -0
  149. package/src/app/pages/NodePage/RevisionDiff.jsx +209 -0
  150. package/src/app/pages/NodePage/WatchNodeButton.jsx +226 -0
  151. package/src/app/pages/NodePage/__tests__/AddBackfillPopover.test.jsx +56 -0
  152. package/src/app/pages/NodePage/__tests__/AddComplexDimensionLinkPopover.test.jsx +459 -0
  153. package/src/app/pages/NodePage/__tests__/AddMaterializationPopover.test.jsx +87 -0
  154. package/src/app/pages/NodePage/__tests__/DimensionFilter.test.jsx +74 -0
  155. package/src/app/pages/NodePage/__tests__/EditColumnDescriptionPopover.test.jsx +149 -0
  156. package/src/app/pages/NodePage/__tests__/EditColumnPopover.test.jsx +144 -0
  157. package/src/app/pages/NodePage/__tests__/LinkDimensionPopover.test.jsx +132 -0
  158. package/src/app/pages/NodePage/__tests__/ManageDimensionLinksDialog.test.jsx +390 -0
  159. package/src/app/pages/NodePage/__tests__/NodeColumnTab.test.jsx +166 -0
  160. package/src/app/pages/NodePage/__tests__/NodeDependenciesTab.test.jsx +151 -0
  161. package/src/app/pages/NodePage/__tests__/NodeGraphTab.test.jsx +595 -0
  162. package/src/app/pages/NodePage/__tests__/NodeLineageTab.test.jsx +58 -0
  163. package/src/app/pages/NodePage/__tests__/NodeMaterializationTab.test.jsx +190 -0
  164. package/src/app/pages/NodePage/__tests__/NodePage.test.jsx +882 -0
  165. package/src/app/pages/NodePage/__tests__/NodeWithDimension.test.jsx +175 -0
  166. package/src/app/pages/NodePage/__tests__/RevisionDiff.test.jsx +164 -0
  167. package/src/app/pages/NodePage/__tests__/__snapshots__/NodePage.test.jsx.snap +19 -0
  168. package/src/app/pages/NodePage/index.jsx +190 -44
  169. package/src/app/pages/NotFoundPage/__tests__/index.test.jsx +16 -0
  170. package/src/app/pages/NotificationsPage/Loadable.jsx +6 -0
  171. package/src/app/pages/NotificationsPage/__tests__/index.test.jsx +287 -0
  172. package/src/app/pages/NotificationsPage/index.jsx +136 -0
  173. package/src/app/pages/OverviewPage/ByStatusPanel.jsx +69 -0
  174. package/src/app/pages/OverviewPage/DimensionNodeUsagePanel.jsx +48 -0
  175. package/src/app/pages/OverviewPage/GovernanceWarningsPanel.jsx +107 -0
  176. package/src/app/pages/OverviewPage/Loadable.jsx +16 -0
  177. package/src/app/pages/OverviewPage/NodesByTypePanel.jsx +63 -0
  178. package/src/app/pages/OverviewPage/OverviewPanel.jsx +94 -0
  179. package/src/app/pages/OverviewPage/TrendsPanel.jsx +66 -0
  180. package/src/app/pages/OverviewPage/__tests__/ByStatusPanel.test.jsx +36 -0
  181. package/src/app/pages/OverviewPage/__tests__/DimensionNodeUsagePanel.test.jsx +76 -0
  182. package/src/app/pages/OverviewPage/__tests__/GovernanceWarningsPanel.test.jsx +77 -0
  183. package/src/app/pages/OverviewPage/__tests__/NodesByTypePanel.test.jsx +86 -0
  184. package/src/app/pages/OverviewPage/__tests__/OverviewPanel.test.jsx +78 -0
  185. package/src/app/pages/OverviewPage/__tests__/TrendsPanel.test.jsx +120 -0
  186. package/src/app/pages/OverviewPage/__tests__/index.test.jsx +54 -0
  187. package/src/app/pages/OverviewPage/index.jsx +22 -0
  188. package/src/app/pages/RegisterTablePage/Loadable.jsx +16 -0
  189. package/src/app/pages/RegisterTablePage/__tests__/RegisterTablePage.test.jsx +112 -0
  190. package/src/app/pages/RegisterTablePage/__tests__/__snapshots__/RegisterTablePage.test.jsx.snap +38 -0
  191. package/src/app/pages/RegisterTablePage/index.jsx +142 -0
  192. package/src/app/pages/Root/__tests__/index.test.jsx +44 -0
  193. package/src/app/pages/Root/index.tsx +92 -10
  194. package/src/app/pages/SQLBuilderPage/Loadable.jsx +16 -0
  195. package/src/app/pages/SQLBuilderPage/__tests__/index.test.jsx +173 -0
  196. package/src/app/pages/SQLBuilderPage/index.jsx +390 -0
  197. package/src/app/pages/SettingsPage/CreateServiceAccountModal.jsx +152 -0
  198. package/src/app/pages/SettingsPage/Loadable.jsx +16 -0
  199. package/src/app/pages/SettingsPage/NotificationSubscriptionsSection.jsx +189 -0
  200. package/src/app/pages/SettingsPage/ProfileSection.jsx +41 -0
  201. package/src/app/pages/SettingsPage/ServiceAccountsSection.jsx +95 -0
  202. package/src/app/pages/SettingsPage/__tests__/CreateServiceAccountModal.test.jsx +318 -0
  203. package/src/app/pages/SettingsPage/__tests__/NotificationSubscriptionsSection.test.jsx +233 -0
  204. package/src/app/pages/SettingsPage/__tests__/ProfileSection.test.jsx +65 -0
  205. package/src/app/pages/SettingsPage/__tests__/ServiceAccountsSection.test.jsx +150 -0
  206. package/src/app/pages/SettingsPage/__tests__/index.test.jsx +184 -0
  207. package/src/app/pages/SettingsPage/index.jsx +148 -0
  208. package/src/app/pages/TagPage/Loadable.jsx +16 -0
  209. package/src/app/pages/TagPage/__tests__/TagPage.test.jsx +70 -0
  210. package/src/app/pages/TagPage/index.jsx +79 -0
  211. package/src/app/services/DJService.js +1444 -21
  212. package/src/app/services/__tests__/DJService.test.jsx +2118 -0
  213. package/src/app/utils/__tests__/date.test.js +198 -0
  214. package/src/app/utils/date.js +65 -0
  215. package/src/index.tsx +1 -0
  216. package/src/mocks/mockNodes.jsx +1477 -0
  217. package/src/setupTests.ts +31 -1
  218. package/src/styles/dag.css +117 -5
  219. package/src/styles/index.css +1028 -31
  220. package/src/styles/loading.css +34 -0
  221. package/src/styles/login.css +81 -0
  222. package/src/styles/nav-bar.css +274 -0
  223. package/src/styles/node-creation.scss +276 -0
  224. package/src/styles/node-list.css +4 -0
  225. package/src/styles/overview.css +72 -0
  226. package/src/styles/settings.css +787 -0
  227. package/src/styles/sorted-table.css +15 -0
  228. package/src/styles/styles.scss +44 -0
  229. package/src/styles/styles.scss.d.ts +9 -0
  230. package/src/utils/form.jsx +23 -0
  231. package/webpack.config.js +17 -6
  232. package/.babelrc +0 -4
  233. package/.env.local +0 -4
  234. package/.env.production +0 -1
  235. package/.github/pull_request_template.md +0 -11
  236. package/.github/workflows/ci.yml +0 -33
  237. package/.vscode/extensions.json +0 -7
  238. package/.vscode/launch.json +0 -15
  239. package/.vscode/settings.json +0 -25
  240. package/Dockerfile +0 -7
  241. package/src/app/pages/ListNamespacesPage/Loadable.jsx +0 -14
  242. package/src/app/pages/ListNamespacesPage/index.jsx +0 -62
  243. package/src/app/pages/NamespacePage/__tests__/__snapshots__/index.test.tsx.snap +0 -45
  244. package/src/app/pages/NamespacePage/__tests__/index.test.tsx +0 -14
@@ -0,0 +1,302 @@
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
+
6
+ describe('<NotificationBell />', () => {
7
+ const mockNotifications = [
8
+ {
9
+ id: 1,
10
+ entity_type: 'node',
11
+ entity_name: 'default.metrics.revenue',
12
+ node: 'default.metrics.revenue',
13
+ activity_type: 'update',
14
+ user: 'alice',
15
+ created_at: new Date().toISOString(),
16
+ details: { version: 'v2' },
17
+ },
18
+ {
19
+ id: 2,
20
+ entity_type: 'node',
21
+ entity_name: 'default.dimensions.country',
22
+ node: 'default.dimensions.country',
23
+ activity_type: 'create',
24
+ user: 'bob',
25
+ created_at: new Date(Date.now() - 3600000).toISOString(), // 1 hour ago
26
+ details: { version: 'v1' },
27
+ },
28
+ ];
29
+
30
+ const mockNodes = [
31
+ {
32
+ name: 'default.metrics.revenue',
33
+ type: 'metric',
34
+ current: { displayName: 'Revenue Metric' },
35
+ },
36
+ {
37
+ name: 'default.dimensions.country',
38
+ type: 'dimension',
39
+ current: { displayName: 'Country' },
40
+ },
41
+ ];
42
+
43
+ const createMockDjClient = (overrides = {}) => ({
44
+ whoami: jest.fn().mockResolvedValue({
45
+ id: 1,
46
+ username: 'testuser',
47
+ last_viewed_notifications_at: null,
48
+ }),
49
+ getSubscribedHistory: jest.fn().mockResolvedValue(mockNotifications),
50
+ getNodesByNames: jest.fn().mockResolvedValue(mockNodes),
51
+ markNotificationsRead: jest.fn().mockResolvedValue({}),
52
+ ...overrides,
53
+ });
54
+
55
+ const renderWithContext = (mockDjClient: any) => {
56
+ return render(
57
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
58
+ <NotificationBell />
59
+ </DJClientContext.Provider>,
60
+ );
61
+ };
62
+
63
+ beforeEach(() => {
64
+ jest.clearAllMocks();
65
+ });
66
+
67
+ it('renders the notification bell button', async () => {
68
+ const mockDjClient = createMockDjClient();
69
+ renderWithContext(mockDjClient);
70
+
71
+ const button = screen.getByRole('button');
72
+ expect(button).toBeInTheDocument();
73
+ });
74
+
75
+ it('shows unread badge when there are unread notifications', async () => {
76
+ const mockDjClient = createMockDjClient();
77
+ renderWithContext(mockDjClient);
78
+
79
+ // Wait for notifications to load
80
+ await waitFor(() => {
81
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
82
+ });
83
+
84
+ // Badge should show count of 2 (all notifications are unread since last_viewed is null)
85
+ const badge = await screen.findByText('2');
86
+ expect(badge).toHaveClass('notification-badge');
87
+ });
88
+
89
+ it('does not show badge when all notifications have been viewed', async () => {
90
+ const mockDjClient = createMockDjClient({
91
+ whoami: jest.fn().mockResolvedValue({
92
+ id: 1,
93
+ username: 'testuser',
94
+ // Set last_viewed to future date so all notifications are "read"
95
+ last_viewed_notifications_at: new Date(
96
+ Date.now() + 10000,
97
+ ).toISOString(),
98
+ }),
99
+ });
100
+ renderWithContext(mockDjClient);
101
+
102
+ await waitFor(() => {
103
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
104
+ });
105
+
106
+ // Badge should not be present (no unread count shown)
107
+ const badge = document.querySelector('.notification-badge');
108
+ expect(badge).toBeNull();
109
+ });
110
+
111
+ it('opens dropdown when bell is clicked', async () => {
112
+ const mockDjClient = createMockDjClient();
113
+ renderWithContext(mockDjClient);
114
+
115
+ await waitFor(() => {
116
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
117
+ });
118
+
119
+ const button = screen.getByRole('button');
120
+ fireEvent.click(button);
121
+
122
+ expect(screen.getByText('Updates')).toBeInTheDocument();
123
+ });
124
+
125
+ it('displays notifications in the dropdown', async () => {
126
+ const mockDjClient = createMockDjClient();
127
+ renderWithContext(mockDjClient);
128
+
129
+ await waitFor(() => {
130
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
131
+ });
132
+
133
+ const button = screen.getByRole('button');
134
+ fireEvent.click(button);
135
+
136
+ // Check that display names are shown
137
+ expect(screen.getByText('Revenue Metric')).toBeInTheDocument();
138
+ expect(screen.getByText('Country')).toBeInTheDocument();
139
+
140
+ // Check that entity names are shown below
141
+ expect(screen.getByText('default.metrics.revenue')).toBeInTheDocument();
142
+ expect(screen.getByText('default.dimensions.country')).toBeInTheDocument();
143
+ });
144
+
145
+ it('shows empty state when no notifications', async () => {
146
+ const mockDjClient = createMockDjClient({
147
+ getSubscribedHistory: jest.fn().mockResolvedValue([]),
148
+ });
149
+ renderWithContext(mockDjClient);
150
+
151
+ await waitFor(() => {
152
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
153
+ });
154
+
155
+ const button = screen.getByRole('button');
156
+ fireEvent.click(button);
157
+
158
+ expect(screen.getByText('No updates on watched nodes')).toBeInTheDocument();
159
+ });
160
+
161
+ it('marks notifications as read when dropdown is opened', async () => {
162
+ const mockDjClient = createMockDjClient();
163
+ renderWithContext(mockDjClient);
164
+
165
+ await waitFor(() => {
166
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
167
+ });
168
+
169
+ const button = screen.getByRole('button');
170
+ fireEvent.click(button);
171
+
172
+ expect(mockDjClient.markNotificationsRead).toHaveBeenCalled();
173
+ });
174
+
175
+ it('does not mark as read if already all read', async () => {
176
+ const mockDjClient = createMockDjClient({
177
+ whoami: jest.fn().mockResolvedValue({
178
+ id: 1,
179
+ username: 'testuser',
180
+ last_viewed_notifications_at: new Date(
181
+ Date.now() + 10000,
182
+ ).toISOString(),
183
+ }),
184
+ });
185
+ renderWithContext(mockDjClient);
186
+
187
+ await waitFor(() => {
188
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
189
+ });
190
+
191
+ const button = screen.getByRole('button');
192
+ fireEvent.click(button);
193
+
194
+ // Should not call markNotificationsRead since unreadCount is 0
195
+ expect(mockDjClient.markNotificationsRead).not.toHaveBeenCalled();
196
+ });
197
+
198
+ it('shows View all link when there are notifications', async () => {
199
+ const mockDjClient = createMockDjClient();
200
+ renderWithContext(mockDjClient);
201
+
202
+ await waitFor(() => {
203
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
204
+ });
205
+
206
+ const button = screen.getByRole('button');
207
+ fireEvent.click(button);
208
+
209
+ const viewAllLink = screen.getByText('View all');
210
+ expect(viewAllLink).toHaveAttribute('href', '/notifications');
211
+ });
212
+
213
+ it('calls onDropdownToggle when dropdown state changes', async () => {
214
+ const mockDjClient = createMockDjClient();
215
+ const onDropdownToggle = jest.fn();
216
+
217
+ render(
218
+ <DJClientContext.Provider
219
+ value={{ DataJunctionAPI: mockDjClient as any }}
220
+ >
221
+ <NotificationBell onDropdownToggle={onDropdownToggle} />
222
+ </DJClientContext.Provider>,
223
+ );
224
+
225
+ await waitFor(() => {
226
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
227
+ });
228
+
229
+ const button = screen.getByRole('button');
230
+ fireEvent.click(button);
231
+
232
+ expect(onDropdownToggle).toHaveBeenCalledWith(true);
233
+ });
234
+
235
+ it('closes dropdown when forceClose becomes true', async () => {
236
+ const mockDjClient = createMockDjClient();
237
+
238
+ const { rerender } = render(
239
+ <DJClientContext.Provider
240
+ value={{ DataJunctionAPI: mockDjClient as any }}
241
+ >
242
+ <NotificationBell forceClose={false} />
243
+ </DJClientContext.Provider>,
244
+ );
245
+
246
+ await waitFor(() => {
247
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
248
+ });
249
+
250
+ // Open the dropdown
251
+ const button = screen.getByRole('button');
252
+ fireEvent.click(button);
253
+
254
+ // Verify dropdown is open
255
+ expect(screen.getByText('Updates')).toBeInTheDocument();
256
+
257
+ // Rerender with forceClose=true
258
+ rerender(
259
+ <DJClientContext.Provider
260
+ value={{ DataJunctionAPI: mockDjClient as any }}
261
+ >
262
+ <NotificationBell forceClose={true} />
263
+ </DJClientContext.Provider>,
264
+ );
265
+
266
+ // Dropdown should be closed
267
+ expect(screen.queryByText('Updates')).not.toBeInTheDocument();
268
+ });
269
+
270
+ it('closes dropdown when clicking outside', async () => {
271
+ const mockDjClient = createMockDjClient();
272
+ const onDropdownToggle = jest.fn();
273
+
274
+ render(
275
+ <DJClientContext.Provider
276
+ value={{ DataJunctionAPI: mockDjClient as any }}
277
+ >
278
+ <NotificationBell onDropdownToggle={onDropdownToggle} />
279
+ </DJClientContext.Provider>,
280
+ );
281
+
282
+ await waitFor(() => {
283
+ expect(mockDjClient.getSubscribedHistory).toHaveBeenCalled();
284
+ });
285
+
286
+ // Open the dropdown
287
+ const button = screen.getByRole('button');
288
+ fireEvent.click(button);
289
+
290
+ // Verify dropdown is open
291
+ expect(screen.getByText('Updates')).toBeInTheDocument();
292
+
293
+ // Click outside the dropdown
294
+ fireEvent.click(document.body);
295
+
296
+ // Dropdown should be closed
297
+ expect(screen.queryByText('Updates')).not.toBeInTheDocument();
298
+
299
+ // onDropdownToggle should have been called with false
300
+ expect(onDropdownToggle).toHaveBeenCalledWith(false);
301
+ });
302
+ });
@@ -0,0 +1,183 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import QueryInfo from '../QueryInfo';
4
+
5
+ describe('<QueryInfo />', () => {
6
+ const defaultProps = {
7
+ id: 'query-123',
8
+ state: 'completed',
9
+ engine_name: 'spark',
10
+ engine_version: '3.2.0',
11
+ errors: [],
12
+ links: [],
13
+ output_table: 'output.table',
14
+ scheduled: '2024-01-01 10:00:00',
15
+ started: '2024-01-01 10:05:00',
16
+ finished: '2024-01-01 10:15:00',
17
+ numRows: 1000,
18
+ isList: false,
19
+ };
20
+
21
+ it('renders table view when isList is false', () => {
22
+ const { getByText, container } = render(<QueryInfo {...defaultProps} />);
23
+
24
+ expect(getByText('Query ID')).toBeInTheDocument();
25
+ expect(getByText('query-123')).toBeInTheDocument();
26
+ expect(container.textContent).toContain('spark');
27
+ expect(getByText('completed')).toBeInTheDocument();
28
+ });
29
+
30
+ it('renders list view when isList is true', () => {
31
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
32
+
33
+ expect(getByText('Query ID')).toBeInTheDocument();
34
+ expect(getByText('State')).toBeInTheDocument();
35
+ expect(getByText('Engine')).toBeInTheDocument();
36
+ });
37
+
38
+ it('displays errors in table view', () => {
39
+ const propsWithErrors = {
40
+ ...defaultProps,
41
+ errors: ['Error 1', 'Error 2'],
42
+ };
43
+
44
+ const { getByText } = render(<QueryInfo {...propsWithErrors} />);
45
+
46
+ expect(getByText('Error 1')).toBeInTheDocument();
47
+ expect(getByText('Error 2')).toBeInTheDocument();
48
+ });
49
+
50
+ it('displays links in table view', () => {
51
+ const propsWithLinks = {
52
+ ...defaultProps,
53
+ links: ['https://example.com/query1', 'https://example.com/query2'],
54
+ };
55
+
56
+ const { getByText } = render(<QueryInfo {...propsWithLinks} />);
57
+
58
+ expect(getByText('https://example.com/query1')).toBeInTheDocument();
59
+ expect(getByText('https://example.com/query2')).toBeInTheDocument();
60
+ });
61
+
62
+ it('renders empty state when no errors', () => {
63
+ const { container } = render(<QueryInfo {...defaultProps} />);
64
+
65
+ const errorCell = container.querySelector('td:nth-child(6)');
66
+ expect(errorCell).toBeInTheDocument();
67
+ });
68
+
69
+ it('renders empty state when no links', () => {
70
+ const { container } = render(<QueryInfo {...defaultProps} />);
71
+
72
+ const linksCell = container.querySelector('td:nth-child(7)');
73
+ expect(linksCell).toBeInTheDocument();
74
+ });
75
+
76
+ it('displays all query information in table view', () => {
77
+ const { getByText } = render(<QueryInfo {...defaultProps} />);
78
+
79
+ expect(getByText('output.table')).toBeInTheDocument();
80
+ expect(getByText('1000')).toBeInTheDocument();
81
+ expect(getByText('2024-01-01 10:00:00')).toBeInTheDocument();
82
+ expect(getByText('2024-01-01 10:05:00')).toBeInTheDocument();
83
+ });
84
+
85
+ it('renders list view with query ID link when links present', () => {
86
+ const propsWithLinks = {
87
+ ...defaultProps,
88
+ links: ['https://example.com/query'],
89
+ isList: true,
90
+ };
91
+
92
+ const { container } = render(<QueryInfo {...propsWithLinks} />);
93
+
94
+ const link = container.querySelector('a[href="https://example.com/query"]');
95
+ expect(link).toBeInTheDocument();
96
+ expect(link).toHaveTextContent('query-123');
97
+ });
98
+
99
+ it('renders list view with query ID as text when no links', () => {
100
+ const propsNoLinks = {
101
+ ...defaultProps,
102
+ links: [],
103
+ isList: true,
104
+ };
105
+
106
+ const { getByText } = render(<QueryInfo {...propsNoLinks} />);
107
+
108
+ expect(getByText('query-123')).toBeInTheDocument();
109
+ });
110
+
111
+ it('displays errors with syntax highlighter in list view', () => {
112
+ const propsWithErrors = {
113
+ ...defaultProps,
114
+ errors: ['Syntax error on line 5', 'Connection timeout'],
115
+ isList: true,
116
+ };
117
+
118
+ const { getByText } = render(<QueryInfo {...propsWithErrors} />);
119
+
120
+ expect(getByText('Logs')).toBeInTheDocument();
121
+ });
122
+
123
+ it('displays finished timestamp in list view', () => {
124
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
125
+
126
+ expect(getByText('Finished')).toBeInTheDocument();
127
+ expect(getByText('2024-01-01 10:15:00')).toBeInTheDocument();
128
+ });
129
+
130
+ it('displays output table and row count in list view', () => {
131
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
132
+
133
+ expect(getByText('Output Table:')).toBeInTheDocument();
134
+ expect(getByText('output.table')).toBeInTheDocument();
135
+ expect(getByText('Rows:')).toBeInTheDocument();
136
+ expect(getByText('1000')).toBeInTheDocument();
137
+ });
138
+
139
+ it('displays multiple links in list view', () => {
140
+ const propsWithLinks = {
141
+ ...defaultProps,
142
+ links: ['https://link1.com', 'https://link2.com', 'https://link3.com'],
143
+ isList: true,
144
+ };
145
+
146
+ const { getByText } = render(<QueryInfo {...propsWithLinks} />);
147
+
148
+ expect(getByText('https://link1.com')).toBeInTheDocument();
149
+ expect(getByText('https://link2.com')).toBeInTheDocument();
150
+ expect(getByText('https://link3.com')).toBeInTheDocument();
151
+ });
152
+
153
+ it('renders empty logs section when no errors in list view', () => {
154
+ const { getByText } = render(<QueryInfo {...defaultProps} isList={true} />);
155
+
156
+ expect(getByText('Logs')).toBeInTheDocument();
157
+ });
158
+
159
+ it('displays engine name and version correctly', () => {
160
+ const { container } = render(<QueryInfo {...defaultProps} />);
161
+
162
+ const badges = container.querySelectorAll('.badge');
163
+ const engineBadge = Array.from(badges).find(b =>
164
+ b.textContent.includes('spark'),
165
+ );
166
+ expect(engineBadge).toBeTruthy();
167
+ expect(engineBadge.textContent).toContain('3.2.0');
168
+ });
169
+
170
+ it('handles undefined optional props', () => {
171
+ const minimalProps = {
172
+ id: 'query-456',
173
+ state: 'running',
174
+ engine_name: 'trino',
175
+ engine_version: '1.0',
176
+ };
177
+
178
+ const { getByText } = render(<QueryInfo {...minimalProps} />);
179
+
180
+ expect(getByText('query-456')).toBeInTheDocument();
181
+ expect(getByText('running')).toBeInTheDocument();
182
+ });
183
+ });