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,150 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import { ServiceAccountsSection } from '../ServiceAccountsSection';
4
+
5
+ describe('ServiceAccountsSection', () => {
6
+ const mockOnCreate = jest.fn();
7
+ const mockOnDelete = jest.fn();
8
+
9
+ const mockAccounts = [
10
+ {
11
+ id: 1,
12
+ name: 'my-pipeline',
13
+ client_id: 'abc-123-xyz',
14
+ created_at: '2024-12-01T00:00:00Z',
15
+ },
16
+ {
17
+ id: 2,
18
+ name: 'etl-job',
19
+ client_id: 'def-456-uvw',
20
+ created_at: '2024-12-02T00:00:00Z',
21
+ },
22
+ ];
23
+
24
+ beforeEach(() => {
25
+ jest.clearAllMocks();
26
+ });
27
+
28
+ it('renders empty state when no accounts', () => {
29
+ render(
30
+ <ServiceAccountsSection
31
+ accounts={[]}
32
+ onCreate={mockOnCreate}
33
+ onDelete={mockOnDelete}
34
+ />,
35
+ );
36
+
37
+ expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
38
+ });
39
+
40
+ it('renders accounts list', () => {
41
+ render(
42
+ <ServiceAccountsSection
43
+ accounts={mockAccounts}
44
+ onCreate={mockOnCreate}
45
+ onDelete={mockOnDelete}
46
+ />,
47
+ );
48
+
49
+ expect(screen.getByText('my-pipeline')).toBeInTheDocument();
50
+ expect(screen.getByText('etl-job')).toBeInTheDocument();
51
+ expect(screen.getByText('abc-123-xyz')).toBeInTheDocument();
52
+ expect(screen.getByText('def-456-uvw')).toBeInTheDocument();
53
+ });
54
+
55
+ it('renders section title and create button', () => {
56
+ render(
57
+ <ServiceAccountsSection
58
+ accounts={[]}
59
+ onCreate={mockOnCreate}
60
+ onDelete={mockOnDelete}
61
+ />,
62
+ );
63
+
64
+ expect(screen.getByText('Service Accounts')).toBeInTheDocument();
65
+ expect(screen.getByText('+ Create')).toBeInTheDocument();
66
+ });
67
+
68
+ it('opens create modal when create button is clicked', () => {
69
+ render(
70
+ <ServiceAccountsSection
71
+ accounts={[]}
72
+ onCreate={mockOnCreate}
73
+ onDelete={mockOnDelete}
74
+ />,
75
+ );
76
+
77
+ fireEvent.click(screen.getByText('+ Create'));
78
+
79
+ expect(screen.getByText('Create Service Account')).toBeInTheDocument();
80
+ });
81
+
82
+ it('calls onDelete when delete is confirmed', async () => {
83
+ window.confirm = jest.fn().mockReturnValue(true);
84
+ mockOnDelete.mockResolvedValue();
85
+
86
+ render(
87
+ <ServiceAccountsSection
88
+ accounts={mockAccounts}
89
+ onCreate={mockOnCreate}
90
+ onDelete={mockOnDelete}
91
+ />,
92
+ );
93
+
94
+ const deleteButtons = screen.getAllByTitle('Delete service account');
95
+ fireEvent.click(deleteButtons[0]);
96
+
97
+ expect(window.confirm).toHaveBeenCalledWith(
98
+ 'Delete service account "my-pipeline"?\n\nThis will revoke all access for this account and cannot be undone.',
99
+ );
100
+
101
+ await waitFor(() => {
102
+ expect(mockOnDelete).toHaveBeenCalledWith('abc-123-xyz');
103
+ });
104
+ });
105
+
106
+ it('does not call onDelete when delete is cancelled', () => {
107
+ window.confirm = jest.fn().mockReturnValue(false);
108
+
109
+ render(
110
+ <ServiceAccountsSection
111
+ accounts={mockAccounts}
112
+ onCreate={mockOnCreate}
113
+ onDelete={mockOnDelete}
114
+ />,
115
+ );
116
+
117
+ const deleteButtons = screen.getAllByTitle('Delete service account');
118
+ fireEvent.click(deleteButtons[0]);
119
+
120
+ expect(mockOnDelete).not.toHaveBeenCalled();
121
+ });
122
+
123
+ it('renders description text', () => {
124
+ render(
125
+ <ServiceAccountsSection
126
+ accounts={[]}
127
+ onCreate={mockOnCreate}
128
+ onDelete={mockOnDelete}
129
+ />,
130
+ );
131
+
132
+ expect(
133
+ screen.getByText(/Service accounts allow programmatic access/i),
134
+ ).toBeInTheDocument();
135
+ });
136
+
137
+ it('shows table headers when accounts exist', () => {
138
+ render(
139
+ <ServiceAccountsSection
140
+ accounts={mockAccounts}
141
+ onCreate={mockOnCreate}
142
+ onDelete={mockOnDelete}
143
+ />,
144
+ );
145
+
146
+ expect(screen.getByText('Name')).toBeInTheDocument();
147
+ expect(screen.getByText('Client ID')).toBeInTheDocument();
148
+ expect(screen.getByText('Created')).toBeInTheDocument();
149
+ });
150
+ });
@@ -0,0 +1,184 @@
1
+ import React from 'react';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { SettingsPage } from '../index';
4
+ import DJClientContext from '../../../providers/djclient';
5
+
6
+ describe('SettingsPage', () => {
7
+ const mockDjClient = {
8
+ whoami: jest.fn(),
9
+ getNotificationPreferences: jest.fn(),
10
+ getNodesByNames: jest.fn(),
11
+ listServiceAccounts: jest.fn(),
12
+ subscribeToNotifications: jest.fn(),
13
+ unsubscribeFromNotifications: jest.fn(),
14
+ createServiceAccount: jest.fn(),
15
+ deleteServiceAccount: jest.fn(),
16
+ };
17
+
18
+ const renderWithContext = () => {
19
+ return render(
20
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
21
+ <SettingsPage />
22
+ </DJClientContext.Provider>,
23
+ );
24
+ };
25
+
26
+ beforeEach(() => {
27
+ jest.clearAllMocks();
28
+
29
+ // Default mock implementations
30
+ mockDjClient.whoami.mockResolvedValue({
31
+ username: 'testuser',
32
+ email: 'test@example.com',
33
+ name: 'Test User',
34
+ });
35
+ mockDjClient.getNotificationPreferences.mockResolvedValue([]);
36
+ mockDjClient.getNodesByNames.mockResolvedValue([]);
37
+ mockDjClient.listServiceAccounts.mockResolvedValue([]);
38
+ });
39
+
40
+ it('shows loading state initially', () => {
41
+ // Make whoami hang
42
+ mockDjClient.whoami.mockImplementation(() => new Promise(() => {}));
43
+
44
+ renderWithContext();
45
+
46
+ // Should show loading icon (or some loading indicator)
47
+ expect(document.querySelector('.settings-page')).toBeInTheDocument();
48
+ });
49
+
50
+ it('renders all sections after loading', async () => {
51
+ renderWithContext();
52
+
53
+ await waitFor(() => {
54
+ expect(screen.getByText('Settings')).toBeInTheDocument();
55
+ });
56
+
57
+ expect(screen.getByText('Profile')).toBeInTheDocument();
58
+ expect(screen.getByText('Notification Subscriptions')).toBeInTheDocument();
59
+ expect(screen.getByText('Service Accounts')).toBeInTheDocument();
60
+ });
61
+
62
+ it('fetches and displays user profile', async () => {
63
+ mockDjClient.whoami.mockResolvedValue({
64
+ username: 'alice',
65
+ email: 'alice@example.com',
66
+ name: 'Alice Smith',
67
+ });
68
+
69
+ renderWithContext();
70
+
71
+ await waitFor(() => {
72
+ expect(screen.getByText('alice')).toBeInTheDocument();
73
+ });
74
+
75
+ expect(screen.getByText('alice@example.com')).toBeInTheDocument();
76
+ expect(screen.getByText('AS')).toBeInTheDocument(); // initials
77
+ });
78
+
79
+ it('fetches and displays subscriptions', async () => {
80
+ mockDjClient.getNotificationPreferences.mockResolvedValue([
81
+ {
82
+ entity_name: 'default.my_metric',
83
+ entity_type: 'node',
84
+ activity_types: ['update'],
85
+ },
86
+ ]);
87
+
88
+ mockDjClient.getNodesByNames.mockResolvedValue([
89
+ {
90
+ name: 'default.my_metric',
91
+ type: 'METRIC',
92
+ current: {
93
+ displayName: 'My Metric',
94
+ status: 'VALID',
95
+ mode: 'PUBLISHED',
96
+ },
97
+ },
98
+ ]);
99
+
100
+ renderWithContext();
101
+
102
+ await waitFor(() => {
103
+ expect(screen.getByText('default.my_metric')).toBeInTheDocument();
104
+ });
105
+ });
106
+
107
+ it('fetches and displays service accounts', async () => {
108
+ mockDjClient.listServiceAccounts.mockResolvedValue([
109
+ {
110
+ id: 1,
111
+ name: 'my-pipeline',
112
+ client_id: 'abc-123',
113
+ created_at: '2024-12-01T00:00:00Z',
114
+ },
115
+ ]);
116
+
117
+ renderWithContext();
118
+
119
+ await waitFor(() => {
120
+ expect(screen.getByText('my-pipeline')).toBeInTheDocument();
121
+ });
122
+
123
+ expect(screen.getByText('abc-123')).toBeInTheDocument();
124
+ });
125
+
126
+ it('handles service accounts API error gracefully', async () => {
127
+ const consoleSpy = jest.spyOn(console, 'log').mockImplementation();
128
+ mockDjClient.listServiceAccounts.mockRejectedValue(
129
+ new Error('Not available'),
130
+ );
131
+
132
+ renderWithContext();
133
+
134
+ await waitFor(() => {
135
+ expect(screen.getByText('Settings')).toBeInTheDocument();
136
+ });
137
+
138
+ // Page should still render without service accounts
139
+ expect(screen.getByText(/No service accounts yet/i)).toBeInTheDocument();
140
+
141
+ consoleSpy.mockRestore();
142
+ });
143
+
144
+ it('handles fetch error gracefully', async () => {
145
+ const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
146
+ mockDjClient.whoami.mockRejectedValue(new Error('Network error'));
147
+
148
+ renderWithContext();
149
+
150
+ await waitFor(() => {
151
+ // Page should render even after error
152
+ expect(document.querySelector('.settings-page')).toBeInTheDocument();
153
+ });
154
+
155
+ consoleSpy.mockRestore();
156
+ });
157
+
158
+ it('enriches subscriptions with node info from GraphQL', async () => {
159
+ mockDjClient.getNotificationPreferences.mockResolvedValue([
160
+ {
161
+ entity_name: 'default.orders',
162
+ entity_type: 'node',
163
+ activity_types: ['update'],
164
+ },
165
+ ]);
166
+
167
+ mockDjClient.getNodesByNames.mockResolvedValue([
168
+ {
169
+ name: 'default.orders',
170
+ type: 'SOURCE',
171
+ current: {
172
+ displayName: 'Orders Table',
173
+ status: 'VALID',
174
+ },
175
+ },
176
+ ]);
177
+
178
+ renderWithContext();
179
+
180
+ await waitFor(() => {
181
+ expect(screen.getByText('SOURCE')).toBeInTheDocument();
182
+ });
183
+ });
184
+ });
@@ -0,0 +1,148 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+ import LoadingIcon from '../../icons/LoadingIcon';
4
+ import ProfileSection from './ProfileSection';
5
+ import NotificationSubscriptionsSection from './NotificationSubscriptionsSection';
6
+ import ServiceAccountsSection from './ServiceAccountsSection';
7
+ import '../../../styles/settings.css';
8
+
9
+ /**
10
+ * Main Settings page that orchestrates all settings sections.
11
+ */
12
+ export function SettingsPage() {
13
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
14
+ const [currentUser, setCurrentUser] = useState(null);
15
+ const [subscriptions, setSubscriptions] = useState([]);
16
+ const [serviceAccounts, setServiceAccounts] = useState([]);
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ async function fetchData() {
21
+ try {
22
+ // Fetch user profile
23
+ const user = await djClient.whoami();
24
+ setCurrentUser(user);
25
+
26
+ // Fetch notification subscriptions
27
+ const prefs = await djClient.getNotificationPreferences();
28
+
29
+ // Fetch node details for subscriptions via GraphQL
30
+ const nodeNames = (prefs || [])
31
+ .filter(p => p.entity_type === 'node')
32
+ .map(p => p.entity_name);
33
+
34
+ let nodeInfoMap = {};
35
+ if (nodeNames.length > 0) {
36
+ const nodes = await djClient.getNodesByNames(nodeNames);
37
+ nodeInfoMap = Object.fromEntries(
38
+ nodes.map(n => [
39
+ n.name,
40
+ {
41
+ node_type: n.type?.toLowerCase(),
42
+ display_name: n.current?.displayName,
43
+ status: n.current?.status?.toLowerCase(),
44
+ mode: n.current?.mode?.toLowerCase(),
45
+ },
46
+ ]),
47
+ );
48
+ }
49
+
50
+ // Merge node info into subscriptions
51
+ const enrichedPrefs = (prefs || []).map(pref => ({
52
+ ...pref,
53
+ ...(nodeInfoMap[pref.entity_name] || {}),
54
+ }));
55
+ setSubscriptions(enrichedPrefs);
56
+
57
+ // Fetch service accounts
58
+ try {
59
+ const accounts = await djClient.listServiceAccounts();
60
+ setServiceAccounts(accounts || []);
61
+ } catch (err) {
62
+ // Service accounts may not be available, ignore error
63
+ console.log('Service accounts not available:', err);
64
+ }
65
+ } catch (error) {
66
+ console.error('Error fetching settings data:', error);
67
+ } finally {
68
+ setLoading(false);
69
+ }
70
+ }
71
+ fetchData();
72
+ }, [djClient]);
73
+
74
+ // Subscription handlers
75
+ const handleUpdateSubscription = async (sub, activityTypes) => {
76
+ await djClient.subscribeToNotifications({
77
+ entity_type: sub.entity_type,
78
+ entity_name: sub.entity_name,
79
+ activity_types: activityTypes,
80
+ alert_types: sub.alert_types || ['web'],
81
+ });
82
+
83
+ // Update local state
84
+ setSubscriptions(
85
+ subscriptions.map(s =>
86
+ s.entity_name === sub.entity_name
87
+ ? { ...s, activity_types: activityTypes }
88
+ : s,
89
+ ),
90
+ );
91
+ };
92
+
93
+ const handleUnsubscribe = async sub => {
94
+ await djClient.unsubscribeFromNotifications({
95
+ entity_type: sub.entity_type,
96
+ entity_name: sub.entity_name,
97
+ });
98
+ setSubscriptions(
99
+ subscriptions.filter(s => s.entity_name !== sub.entity_name),
100
+ );
101
+ };
102
+
103
+ // Service account handlers
104
+ const handleCreateServiceAccount = async name => {
105
+ const result = await djClient.createServiceAccount(name);
106
+ if (result.client_id) {
107
+ setServiceAccounts([...serviceAccounts, result]);
108
+ }
109
+ return result;
110
+ };
111
+
112
+ const handleDeleteServiceAccount = async clientId => {
113
+ await djClient.deleteServiceAccount(clientId);
114
+ setServiceAccounts(serviceAccounts.filter(a => a.client_id !== clientId));
115
+ };
116
+
117
+ if (loading) {
118
+ return (
119
+ <div className="settings-page">
120
+ <div className="settings-container">
121
+ <LoadingIcon />
122
+ </div>
123
+ </div>
124
+ );
125
+ }
126
+
127
+ return (
128
+ <div className="settings-page">
129
+ <div className="settings-container">
130
+ <h1 className="settings-title">Settings</h1>
131
+
132
+ <ProfileSection user={currentUser} />
133
+
134
+ <NotificationSubscriptionsSection
135
+ subscriptions={subscriptions}
136
+ onUpdate={handleUpdateSubscription}
137
+ onUnsubscribe={handleUnsubscribe}
138
+ />
139
+
140
+ <ServiceAccountsSection
141
+ accounts={serviceAccounts}
142
+ onCreate={handleCreateServiceAccount}
143
+ onDelete={handleDeleteServiceAccount}
144
+ />
145
+ </div>
146
+ </div>
147
+ );
148
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Asynchronously loads the component for the Node page
3
+ */
4
+
5
+ import * as React from 'react';
6
+ import { lazyLoad } from '../../../utils/loadable';
7
+
8
+ export const TagPage = () => {
9
+ return lazyLoad(
10
+ () => import('./index'),
11
+ module => module.TagPage,
12
+ {
13
+ fallback: <div></div>,
14
+ },
15
+ )();
16
+ };
@@ -0,0 +1,70 @@
1
+ import React from 'react';
2
+ import { screen, waitFor } from '@testing-library/react';
3
+ import fetchMock from 'jest-fetch-mock';
4
+ import { render } from '../../../../setupTests';
5
+ import { MemoryRouter, Route, Routes } from 'react-router-dom';
6
+ import DJClientContext from '../../../providers/djclient';
7
+ import { TagPage } from '../index';
8
+
9
+ describe('<TagPage />', () => {
10
+ const initializeMockDJClient = () => {
11
+ return {
12
+ DataJunctionAPI: {
13
+ getTag: jest.fn(),
14
+ listNodesForTag: jest.fn(),
15
+ },
16
+ };
17
+ };
18
+
19
+ const mockDjClient = initializeMockDJClient();
20
+
21
+ beforeEach(() => {
22
+ fetchMock.resetMocks();
23
+ jest.clearAllMocks();
24
+ window.scrollTo = jest.fn();
25
+
26
+ mockDjClient.DataJunctionAPI.getTag.mockReturnValue({
27
+ name: 'domains.com',
28
+ tag_type: 'domains',
29
+ description: 'Top-level domain .com',
30
+ });
31
+ mockDjClient.DataJunctionAPI.listNodesForTag.mockReturnValue([
32
+ {
33
+ name: 'random.node_a',
34
+ type: 'metric',
35
+ display_name: 'Node A',
36
+ },
37
+ ]);
38
+ });
39
+
40
+ const renderTagsPage = element => {
41
+ return render(
42
+ <MemoryRouter initialEntries={['/tags/:name']}>
43
+ <Routes>
44
+ <Route path="tags/:name" element={element} />
45
+ </Routes>
46
+ </MemoryRouter>,
47
+ );
48
+ };
49
+
50
+ const testElement = djClient => {
51
+ return (
52
+ <DJClientContext.Provider value={djClient}>
53
+ <TagPage />
54
+ </DJClientContext.Provider>
55
+ );
56
+ };
57
+
58
+ it('renders the tag page correctly', async () => {
59
+ const element = testElement(mockDjClient);
60
+ renderTagsPage(element);
61
+ await waitFor(() => {
62
+ expect(mockDjClient.DataJunctionAPI.getTag).toHaveBeenCalledTimes(1);
63
+ expect(
64
+ mockDjClient.DataJunctionAPI.listNodesForTag,
65
+ ).toHaveBeenCalledTimes(1);
66
+ });
67
+ expect(screen.getByText('Nodes')).toBeInTheDocument();
68
+ expect(screen.getByText('Node A')).toBeInTheDocument();
69
+ }, 60000);
70
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * For a given tag, displays nodes tagged with it
3
+ */
4
+ import NamespaceHeader from '../../components/NamespaceHeader';
5
+ import React, { useContext, useEffect, useState } from 'react';
6
+ import 'styles/node-creation.scss';
7
+ import DJClientContext from '../../providers/djclient';
8
+ import { useParams } from 'react-router-dom';
9
+
10
+ export function TagPage() {
11
+ const [nodes, setNodes] = useState([]);
12
+ const [tag, setTag] = useState([]);
13
+
14
+ const { name } = useParams();
15
+
16
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
17
+ useEffect(() => {
18
+ const fetchData = async () => {
19
+ const data = await djClient.listNodesForTag(name);
20
+ const tagData = await djClient.getTag(name);
21
+ setNodes(data);
22
+ setTag(tagData);
23
+ };
24
+ fetchData().catch(console.error);
25
+ }, [djClient, name]);
26
+
27
+ return (
28
+ <div className="mid">
29
+ <NamespaceHeader namespace="" />
30
+ <div className="card">
31
+ <div className="card-header">
32
+ <h3
33
+ className="card-title align-items-start flex-column"
34
+ style={{ display: 'inline-block' }}
35
+ >
36
+ Tag
37
+ </h3>
38
+ <div>
39
+ <div style={{ marginBottom: '1.5rem' }}>
40
+ <h6 className="mb-0 w-100">Display Name</h6>
41
+ <p className="mb-0 opacity-75">{tag.display_name}</p>
42
+ </div>
43
+ <div style={{ marginBottom: '1.5rem' }}>
44
+ <h6 className="mb-0 w-100">Name</h6>
45
+ <p className="mb-0 opacity-75">{name}</p>
46
+ </div>
47
+ <div style={{ marginBottom: '1.5rem' }}>
48
+ <h6 className="mb-0 w-100">Tag Type</h6>
49
+ <p className="mb-0 opacity-75">{tag.tag_type}</p>
50
+ </div>
51
+ <div style={{ marginBottom: '1.5rem' }}>
52
+ <h6 className="mb-0 w-100">Description</h6>
53
+ <p className="mb-0 opacity-75">{tag.description}</p>
54
+ </div>
55
+ <div style={{ marginBottom: '1.5rem' }}>
56
+ <h6 className="mb-0 w-100">Nodes</h6>
57
+ <div className={`list-group-item`}>
58
+ {nodes?.map(node => (
59
+ <div
60
+ className="button-3 cube-element"
61
+ key={node.name}
62
+ role="cell"
63
+ aria-label="CubeElement"
64
+ aria-hidden="false"
65
+ >
66
+ <a href={`/nodes/${node.name}`}>{node.display_name}</a>
67
+ <span className={`badge node_type__${node.type}`}>
68
+ {node.type}
69
+ </span>
70
+ </div>
71
+ ))}
72
+ </div>
73
+ </div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+ );
79
+ }