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,248 @@
1
+ import React from 'react';
2
+ import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3
+ import UserMenu from '../UserMenu';
4
+ import DJClientContext from '../../providers/djclient';
5
+ import { UserProvider } from '../../providers/UserProvider';
6
+
7
+ describe('<UserMenu />', () => {
8
+ const createMockDjClient = (overrides = {}) => ({
9
+ whoami: jest.fn().mockResolvedValue({
10
+ id: 1,
11
+ username: 'testuser',
12
+ email: 'test@example.com',
13
+ }),
14
+ logout: jest.fn().mockResolvedValue({}),
15
+ ...overrides,
16
+ });
17
+
18
+ const renderWithContext = (mockDjClient: any, props = {}) => {
19
+ return render(
20
+ <DJClientContext.Provider value={{ DataJunctionAPI: mockDjClient }}>
21
+ <UserProvider>
22
+ <UserMenu {...props} />
23
+ </UserProvider>
24
+ </DJClientContext.Provider>,
25
+ );
26
+ };
27
+
28
+ // Mock window.location.reload
29
+ const originalLocation = window.location;
30
+
31
+ beforeEach(() => {
32
+ jest.clearAllMocks();
33
+ delete (window as any).location;
34
+ (window as any).location = { ...originalLocation, reload: jest.fn() };
35
+ });
36
+
37
+ afterEach(() => {
38
+ (window as any).location = originalLocation;
39
+ });
40
+
41
+ it('renders the avatar button', async () => {
42
+ const mockDjClient = createMockDjClient();
43
+ renderWithContext(mockDjClient);
44
+
45
+ const button = screen.getByRole('button');
46
+ expect(button).toBeInTheDocument();
47
+ expect(button).toHaveClass('avatar-button');
48
+ });
49
+
50
+ it('shows "?" before user is loaded', () => {
51
+ const mockDjClient = createMockDjClient({
52
+ whoami: jest.fn().mockImplementation(
53
+ () => new Promise(() => {}), // Never resolves
54
+ ),
55
+ });
56
+ renderWithContext(mockDjClient);
57
+
58
+ const button = screen.getByRole('button');
59
+ expect(button).toHaveTextContent('?');
60
+ });
61
+
62
+ it('displays initials from username (first two letters uppercase)', async () => {
63
+ const mockDjClient = createMockDjClient({
64
+ whoami: jest.fn().mockResolvedValue({
65
+ id: 1,
66
+ username: 'johndoe',
67
+ email: 'john@example.com',
68
+ }),
69
+ });
70
+ renderWithContext(mockDjClient);
71
+
72
+ await waitFor(() => {
73
+ expect(mockDjClient.whoami).toHaveBeenCalled();
74
+ });
75
+
76
+ const button = await screen.findByText('JO');
77
+ expect(button).toBeInTheDocument();
78
+ });
79
+
80
+ it('displays initials from name when available', async () => {
81
+ const mockDjClient = createMockDjClient({
82
+ whoami: jest.fn().mockResolvedValue({
83
+ id: 1,
84
+ username: 'johndoe',
85
+ email: 'john@example.com',
86
+ name: 'John Doe',
87
+ }),
88
+ });
89
+ renderWithContext(mockDjClient);
90
+
91
+ await waitFor(() => {
92
+ expect(mockDjClient.whoami).toHaveBeenCalled();
93
+ });
94
+
95
+ const button = await screen.findByText('JD');
96
+ expect(button).toBeInTheDocument();
97
+ });
98
+
99
+ it('opens dropdown when avatar is clicked', async () => {
100
+ const mockDjClient = createMockDjClient();
101
+ renderWithContext(mockDjClient);
102
+
103
+ await waitFor(() => {
104
+ expect(mockDjClient.whoami).toHaveBeenCalled();
105
+ });
106
+
107
+ const button = screen.getByRole('button');
108
+ fireEvent.click(button);
109
+
110
+ expect(screen.getByText('testuser')).toBeInTheDocument();
111
+ });
112
+
113
+ it('shows Settings and Logout links in dropdown', async () => {
114
+ const mockDjClient = createMockDjClient();
115
+ renderWithContext(mockDjClient);
116
+
117
+ await waitFor(() => {
118
+ expect(mockDjClient.whoami).toHaveBeenCalled();
119
+ });
120
+
121
+ const button = screen.getByRole('button');
122
+ fireEvent.click(button);
123
+
124
+ const settingsLink = screen.getByText('Settings');
125
+ expect(settingsLink).toHaveAttribute('href', '/settings');
126
+
127
+ const logoutLink = screen.getByText('Logout');
128
+ expect(logoutLink).toHaveAttribute('href', '/');
129
+ });
130
+
131
+ it('calls logout and reloads page when Logout is clicked', async () => {
132
+ const mockDjClient = createMockDjClient();
133
+ renderWithContext(mockDjClient);
134
+
135
+ await waitFor(() => {
136
+ expect(mockDjClient.whoami).toHaveBeenCalled();
137
+ });
138
+
139
+ const button = screen.getByRole('button');
140
+ fireEvent.click(button);
141
+
142
+ const logoutLink = screen.getByText('Logout');
143
+ fireEvent.click(logoutLink);
144
+
145
+ expect(mockDjClient.logout).toHaveBeenCalled();
146
+ });
147
+
148
+ it('calls onDropdownToggle when dropdown is opened', async () => {
149
+ const mockDjClient = createMockDjClient();
150
+ const onDropdownToggle = jest.fn();
151
+ renderWithContext(mockDjClient, { onDropdownToggle });
152
+
153
+ await waitFor(() => {
154
+ expect(mockDjClient.whoami).toHaveBeenCalled();
155
+ });
156
+
157
+ const button = screen.getByRole('button');
158
+ fireEvent.click(button);
159
+
160
+ expect(onDropdownToggle).toHaveBeenCalledWith(true);
161
+ });
162
+
163
+ it('closes dropdown when forceClose becomes true', async () => {
164
+ const mockDjClient = createMockDjClient();
165
+
166
+ const { rerender } = render(
167
+ <DJClientContext.Provider
168
+ value={{ DataJunctionAPI: mockDjClient as any }}
169
+ >
170
+ <UserProvider>
171
+ <UserMenu forceClose={false} />
172
+ </UserProvider>
173
+ </DJClientContext.Provider>,
174
+ );
175
+
176
+ await waitFor(() => {
177
+ expect(mockDjClient.whoami).toHaveBeenCalled();
178
+ });
179
+
180
+ // Open the dropdown
181
+ const button = screen.getByRole('button');
182
+ fireEvent.click(button);
183
+
184
+ // Verify dropdown is open
185
+ expect(screen.getByText('testuser')).toBeInTheDocument();
186
+
187
+ // Rerender with forceClose=true
188
+ rerender(
189
+ <DJClientContext.Provider
190
+ value={{ DataJunctionAPI: mockDjClient as any }}
191
+ >
192
+ <UserProvider>
193
+ <UserMenu forceClose={true} />
194
+ </UserProvider>
195
+ </DJClientContext.Provider>,
196
+ );
197
+
198
+ // Dropdown should be closed
199
+ expect(screen.queryByText('Settings')).not.toBeInTheDocument();
200
+ });
201
+
202
+ it('closes dropdown when clicking outside', async () => {
203
+ const mockDjClient = createMockDjClient();
204
+ const onDropdownToggle = jest.fn();
205
+ renderWithContext(mockDjClient, { onDropdownToggle });
206
+
207
+ await waitFor(() => {
208
+ expect(mockDjClient.whoami).toHaveBeenCalled();
209
+ });
210
+
211
+ // Open the dropdown
212
+ const button = screen.getByRole('button');
213
+ fireEvent.click(button);
214
+
215
+ // Verify dropdown is open
216
+ expect(screen.getByText('testuser')).toBeInTheDocument();
217
+
218
+ // Click outside
219
+ fireEvent.click(document.body);
220
+
221
+ // Dropdown should be closed
222
+ expect(screen.queryByText('Settings')).not.toBeInTheDocument();
223
+
224
+ // onDropdownToggle should be called with false
225
+ expect(onDropdownToggle).toHaveBeenCalledWith(false);
226
+ });
227
+
228
+ it('toggles dropdown closed when clicking avatar again', async () => {
229
+ const mockDjClient = createMockDjClient();
230
+ const onDropdownToggle = jest.fn();
231
+ renderWithContext(mockDjClient, { onDropdownToggle });
232
+
233
+ await waitFor(() => {
234
+ expect(mockDjClient.whoami).toHaveBeenCalled();
235
+ });
236
+
237
+ const button = screen.getByRole('button');
238
+
239
+ // Open
240
+ fireEvent.click(button);
241
+ expect(onDropdownToggle).toHaveBeenCalledWith(true);
242
+ expect(screen.getByText('testuser')).toBeInTheDocument();
243
+
244
+ // Close by clicking again
245
+ fireEvent.click(button);
246
+ expect(onDropdownToggle).toHaveBeenCalledWith(false);
247
+ });
248
+ });
@@ -14,11 +14,16 @@ exports[`<ListGroupItem /> should render and match the snapshot 1`] = `
14
14
  Name
15
15
  </h6>
16
16
  <p
17
+ aria-hidden="false"
18
+ aria-label="Name"
17
19
  className="mb-0 opacity-75"
20
+ role="dialog"
18
21
  >
19
- <span>
20
- Something
21
- </span>
22
+ <Markdown>
23
+ <span>
24
+ Something
25
+ </span>
26
+ </Markdown>
22
27
  </p>
23
28
  </div>
24
29
  </div>
@@ -8,25 +8,9 @@ exports[`<NamespaceHeader /> should render and match the snapshot 1`] = `
8
8
  className="breadcrumb-item"
9
9
  >
10
10
  <a
11
- href="/namespaces/"
11
+ href="/"
12
12
  >
13
- <svg
14
- className="bi bi-house-door-fill"
15
- fill="currentColor"
16
- height="16"
17
- style={
18
- Object {
19
- "paddingBottom": "0.2rem",
20
- }
21
- }
22
- viewBox="0 0 16 16"
23
- width="16"
24
- xmlns="http://www.w3.org/2000/svg"
25
- >
26
- <path
27
- d="M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z"
28
- />
29
- </svg>
13
+ <HorizontalHierarchyIcon />
30
14
  </a>
31
15
  </li>
32
16
  <li
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { DJNodeDimensions } from './DJNodeDimensions';
3
+ import { DJNodeColumns } from './DJNodeColumns';
4
+
5
+ export default function Collapse({ collapsed, text, data }) {
6
+ const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
7
+
8
+ const limit = 5;
9
+ return (
10
+ <>
11
+ <div className="collapse">
12
+ {data.type === 'metric' ? (
13
+ <button
14
+ className="collapse-button"
15
+ onClick={() => setIsCollapsed(!isCollapsed)}
16
+ >
17
+ {isCollapsed ? '\u25B6 Show' : '\u25BC Hide'} {text}
18
+ </button>
19
+ ) : (
20
+ ''
21
+ )}
22
+ <div
23
+ className={`collapse-content ${
24
+ isCollapsed && data.type === 'metric' ? 'collapsed' : 'expanded'
25
+ }`}
26
+ aria-expanded={isCollapsed}
27
+ >
28
+ {data.type !== 'metric'
29
+ ? isCollapsed
30
+ ? DJNodeColumns({ data: data, limit: limit })
31
+ : DJNodeColumns({ data: data, limit: 100 })
32
+ : DJNodeDimensions(data)}
33
+ </div>
34
+ {data.type !== 'metric' && data.column_names.length > limit ? (
35
+ <button
36
+ className="collapse-button"
37
+ onClick={() => setIsCollapsed(!isCollapsed)}
38
+ >
39
+ {isCollapsed ? '\u25B6 More' : '\u25BC Less'} {text}
40
+ </button>
41
+ ) : (
42
+ ''
43
+ )}
44
+ </div>
45
+ </>
46
+ );
47
+ }
@@ -1,81 +1,58 @@
1
- import React, { memo } from 'react';
1
+ import { memo } from 'react';
2
2
  import { Handle, Position } from 'reactflow';
3
-
4
- function renderBasedOnDJNodeType(param) {
5
- switch (param) {
6
- case 'source':
7
- return { backgroundColor: '#7EB46150', color: '#7EB461' };
8
- case 'transform':
9
- return { backgroundColor: '#6DAAA750', color: '#6DAAA7' };
10
- case 'dimension':
11
- return { backgroundColor: '#CF7D2950', color: '#CF7D29' };
12
- case 'metric':
13
- return { backgroundColor: '#A27E8650', color: '#A27E86' };
14
- case 'cube':
15
- return { backgroundColor: '#C2180750', color: '#C21807' };
16
- default:
17
- return {};
18
- }
19
- }
3
+ import Collapse from './Collapse';
20
4
 
21
5
  function capitalize(string) {
22
6
  return string.charAt(0).toUpperCase() + string.slice(1);
23
7
  }
24
8
 
25
- const Collapse = ({ collapsed, text, children }) => {
26
- const [isCollapsed, setIsCollapsed] = React.useState(collapsed);
27
-
28
- return (
29
- <>
30
- <div className="collapse">
31
- <button
32
- className="collapse-button"
33
- onClick={() => setIsCollapsed(!isCollapsed)}
34
- >
35
- {isCollapsed ? '\u25B6 Show' : '\u25BC Hide'} {text}
36
- </button>
37
- <div
38
- className={`collapse-content ${
39
- isCollapsed ? 'collapsed' : 'expanded'
40
- }`}
41
- aria-expanded={isCollapsed}
42
- >
43
- {children}
44
- </div>
45
- </div>
46
- </>
47
- );
48
- };
49
-
50
9
  export function DJNode({ id, data }) {
51
- const columnsRenderer = data =>
52
- data.column_names.map(col => (
53
- <tr>
54
- <td>
55
- {data.primary_key.includes(col.name) ? (
56
- <b>{col.name} (PK)</b>
57
- ) : (
58
- <>{col.name}</>
59
- )}
60
- </td>
61
- <td style={{ textAlign: 'right' }}>{col.type}</td>
62
- </tr>
63
- ));
64
- // const dimensionsRenderer = data =>
65
- // data.dimensions.map(dim => (
66
- // <tr>
67
- // <td>{dim}</td>
68
- // </tr>
69
- // ));
10
+ const handleWrapperStyle = {
11
+ display: 'flex',
12
+ position: 'absolute',
13
+ height: '100%',
14
+ flexDirection: 'column',
15
+ top: '50%',
16
+ justifyContent: 'space-between',
17
+ };
18
+ const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
70
19
 
20
+ const handleStyle = {
21
+ width: '12px',
22
+ height: '12px',
23
+ borderRadius: '12px',
24
+ background: 'transparent',
25
+ border: '4px solid transparent',
26
+ cursor: 'pointer',
27
+ position: 'absolute',
28
+ top: '0px',
29
+ left: 0,
30
+ };
31
+ const handleStyleLeft = percentage => {
32
+ return {
33
+ ...handleStyle,
34
+ ...{
35
+ transform: 'translate(-' + percentage + '%, -50%)',
36
+ },
37
+ };
38
+ };
39
+ const highlightNodeClass =
40
+ data.is_current === true ? ' dj-node_highlight' : '';
71
41
  return (
72
42
  <>
73
- <Handle
74
- type="target"
75
- position={Position.Left}
76
- style={{ backgroundColor: '#ccc' }}
77
- />
78
- <div className="dj-node__full" style={renderBasedOnDJNodeType(data.type)}>
43
+ <div
44
+ className={'dj-node__full node_type__' + data.type + highlightNodeClass}
45
+ key={data.name}
46
+ style={{ width: '450px' }}
47
+ >
48
+ <div style={handleWrapperStyle}>
49
+ <Handle
50
+ type="target"
51
+ id={data.name}
52
+ position={Position.Left}
53
+ style={handleStyleLeft(100)}
54
+ />
55
+ </div>
79
56
  <div className="dj-node__header">
80
57
  <div className="serif">
81
58
  {data.name
@@ -85,25 +62,26 @@ export function DJNode({ id, data }) {
85
62
  </div>
86
63
  </div>
87
64
  <div className="dj-node__body">
88
- <b>{capitalize(data.type)}</b>:{' '}
89
- {data.type === 'source' ? data.table : data.display_name}
65
+ <b>{capitalize(data.type)}</b>
66
+ <br />{' '}
67
+ <a href={`/nodes/${data.name}`}>
68
+ {data.type === 'source' ? data.table : data.display_name}
69
+ </a>
90
70
  <Collapse
91
- collapsed={true}
71
+ collapsed={data.is_current && data.type != 'metric' ? false : true}
92
72
  text={data.type !== 'metric' ? 'columns' : 'dimensions'}
93
- >
94
- <div className="dj-node__metadata">
95
- {
96
- data.type !== 'metric' ? columnsRenderer(data) : '' // dimensionsRenderer(data)
97
- }
98
- </div>
99
- </Collapse>
73
+ data={data}
74
+ />
75
+ </div>
76
+ <div style={handleWrapperStyleRight}>
77
+ <Handle
78
+ type="source"
79
+ id={data.name}
80
+ position={Position.Right}
81
+ style={handleStyleLeft(90)}
82
+ />
100
83
  </div>
101
84
  </div>
102
- <Handle
103
- type="source"
104
- position={Position.Right}
105
- style={{ backgroundColor: '#ccc' }}
106
- />
107
85
  </>
108
86
  );
109
87
  }
@@ -0,0 +1,75 @@
1
+ import { Handle } from 'reactflow';
2
+ import React from 'react';
3
+
4
+ export function DJNodeColumns({ data, limit }) {
5
+ const handleWrapperStyle = {
6
+ display: 'flex',
7
+ position: 'absolute',
8
+ height: '100%',
9
+ flexDirection: 'column',
10
+ top: '50%',
11
+ justifyContent: 'space-between',
12
+ };
13
+ const handleWrapperStyleRight = { ...handleWrapperStyle, ...{ right: 0 } };
14
+
15
+ const handleStyle = {
16
+ width: '12px',
17
+ height: '12px',
18
+ borderRadius: '12px',
19
+ background: 'transparent',
20
+ border: '4px solid transparent',
21
+ cursor: 'pointer',
22
+ position: 'absolute',
23
+ top: '0px',
24
+ left: 0,
25
+ };
26
+ const handleStyleLeft = percentage => {
27
+ return {
28
+ ...handleStyle,
29
+ ...{
30
+ transform: 'translate(-' + percentage + '%, -50%)',
31
+ },
32
+ };
33
+ };
34
+ return data.column_names.slice(0, limit).map(col => (
35
+ <div
36
+ className={
37
+ 'custom-node-subheader node_type__' +
38
+ data.type +
39
+ (col.order <= 0 ? ' custom-node-emphasis' : '')
40
+ }
41
+ key={`${data.name}.${col.name}`}
42
+ >
43
+ <div style={handleWrapperStyle}>
44
+ <Handle
45
+ type="target"
46
+ position="left"
47
+ id={data.name + '.' + col.name}
48
+ style={handleStyleLeft(100)}
49
+ />
50
+ </div>
51
+ <div
52
+ className="custom-node-port"
53
+ id={data.name + '.' + col.name}
54
+ key={'i-' + data.name + '.' + col.name}
55
+ >
56
+ {data.primary_key.includes(col.name) ? (
57
+ <b>{col.name} (PK)</b>
58
+ ) : (
59
+ <>{col.name}</>
60
+ )}
61
+ <span style={{ marginLeft: '0.25rem' }} className={'badge'}>
62
+ {col.type}
63
+ </span>
64
+ </div>
65
+ <div style={handleWrapperStyleRight}>
66
+ <Handle
67
+ type="source"
68
+ position="right"
69
+ id={data.name + '.' + col.name}
70
+ style={handleStyle}
71
+ />
72
+ </div>
73
+ </div>
74
+ ));
75
+ }
@@ -0,0 +1,75 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../../providers/djclient';
3
+
4
+ export function DJNodeDimensions(data) {
5
+ const [dimensions, setDimensions] = useState([]);
6
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
7
+ useEffect(() => {
8
+ if (data.type === 'metric') {
9
+ async function getDimensions() {
10
+ try {
11
+ const metricData = await djClient.metric(data.name);
12
+ setDimensions(metricData.dimensions);
13
+ } catch (err) {
14
+ console.log(err);
15
+ }
16
+ }
17
+ getDimensions();
18
+ }
19
+ }, [data, djClient]);
20
+ const dimensionsToObject = dimensions => {
21
+ return dimensions.map(dim => {
22
+ const [attribute, ...nodeName] = dim.name.split('.').reverse();
23
+ return {
24
+ dimension: nodeName.reverse().join('.'),
25
+ path: dim.path,
26
+ column: attribute,
27
+ };
28
+ });
29
+ };
30
+ const groupedDimensions = dims =>
31
+ dims.reduce((acc, current) => {
32
+ const dimKey = current.dimension + ' via ' + current.path.slice(-1);
33
+ acc[dimKey] = acc[dimKey] || {
34
+ dimension: current.dimension,
35
+ path: current.path.slice(-1),
36
+ columns: [],
37
+ };
38
+ acc[dimKey].columns.push(current.column);
39
+ return acc;
40
+ }, {});
41
+ const dimensionsRenderer = grouped =>
42
+ Object.entries(grouped).map(([dimKey, dimValue]) => {
43
+ if (Array.isArray(dimValue.columns)) {
44
+ const attributes = dimValue.columns.map(col => {
45
+ return (
46
+ <span className={'badge white_badge'} key={`attr-${col}`}>
47
+ {col}
48
+ </span>
49
+ );
50
+ });
51
+ return (
52
+ <div
53
+ className={'custom-node-subheader node_type__' + data.type}
54
+ key={`dim-${dimValue.path}-${dimValue.dimension}`}
55
+ >
56
+ <div className="custom-node-port">
57
+ <a href={`/nodes/${dimValue.dimension}`}>{dimValue.dimension}</a>{' '}
58
+ <div className={'badge node_type__metric text-black'}>
59
+ {dimValue.path}
60
+ </div>
61
+ </div>
62
+ <div className={'dimension_attributes'}>{attributes}</div>
63
+ </div>
64
+ );
65
+ }
66
+ return <></>;
67
+ });
68
+ return (
69
+ <>
70
+ {dimensions.length <= 0
71
+ ? ''
72
+ : dimensionsRenderer(groupedDimensions(dimensionsToObject(dimensions)))}
73
+ </>
74
+ );
75
+ }