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,229 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../providers/djclient';
3
+ import { useCurrentUser } from '../providers/UserProvider';
4
+ import NotificationIcon from '../icons/NotificationIcon';
5
+ import SettingsIcon from '../icons/SettingsIcon';
6
+ import LoadingIcon from '../icons/LoadingIcon';
7
+ import { formatRelativeTime } from '../utils/date';
8
+
9
+ interface HistoryEntry {
10
+ id: number;
11
+ entity_type: string;
12
+ entity_name: string;
13
+ node: string;
14
+ activity_type: string;
15
+ user: string;
16
+ created_at: string;
17
+ details?: {
18
+ version?: string;
19
+ [key: string]: any;
20
+ };
21
+ }
22
+
23
+ interface EnrichedHistoryEntry extends HistoryEntry {
24
+ node_type?: string;
25
+ display_name?: string;
26
+ }
27
+
28
+ interface NodeInfo {
29
+ name: string;
30
+ type: string;
31
+ current?: {
32
+ displayName?: string;
33
+ };
34
+ }
35
+
36
+ // Calculate unread count based on last_viewed_notifications_at
37
+ const calculateUnreadCount = (
38
+ notifs: HistoryEntry[],
39
+ lastViewed: string | null | undefined,
40
+ ): number => {
41
+ if (!lastViewed) return notifs.length;
42
+ const lastViewedDate = new Date(lastViewed);
43
+ return notifs.filter(n => new Date(n.created_at) > lastViewedDate).length;
44
+ };
45
+
46
+ // Enrich history entries with node info from GraphQL
47
+ const enrichWithNodeInfo = (
48
+ entries: HistoryEntry[],
49
+ nodes: NodeInfo[],
50
+ ): EnrichedHistoryEntry[] => {
51
+ const nodeMap = new Map(nodes.map(n => [n.name, n]));
52
+ return entries.map(entry => {
53
+ const node = nodeMap.get(entry.entity_name);
54
+ return {
55
+ ...entry,
56
+ node_type: node?.type,
57
+ display_name: node?.current?.displayName,
58
+ };
59
+ });
60
+ };
61
+
62
+ interface NotificationBellProps {
63
+ onDropdownToggle?: (isOpen: boolean) => void;
64
+ forceClose?: boolean;
65
+ }
66
+
67
+ export default function NotificationBell({
68
+ onDropdownToggle,
69
+ forceClose,
70
+ }: NotificationBellProps) {
71
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
72
+ const { currentUser, loading: userLoading } = useCurrentUser();
73
+ const [showDropdown, setShowDropdown] = useState(false);
74
+
75
+ // Close when forceClose becomes true
76
+ useEffect(() => {
77
+ if (forceClose && showDropdown) {
78
+ setShowDropdown(false);
79
+ }
80
+ }, [forceClose, showDropdown]);
81
+ const [notifications, setNotifications] = useState<EnrichedHistoryEntry[]>(
82
+ [],
83
+ );
84
+ const [loading, setLoading] = useState(false);
85
+ const [unreadCount, setUnreadCount] = useState(0);
86
+
87
+ // Fetch notifications when user data is available
88
+ useEffect(() => {
89
+ if (userLoading) return;
90
+
91
+ async function fetchNotifications() {
92
+ setLoading(true);
93
+ try {
94
+ const history: HistoryEntry[] =
95
+ (await djClient.getSubscribedHistory(5)) || [];
96
+
97
+ // Get unique entity names and fetch their info via GraphQL
98
+ // (some may not be nodes, but GraphQL will just not return them)
99
+ const nodeNames = Array.from(new Set(history.map(h => h.entity_name)));
100
+ const nodes: NodeInfo[] = nodeNames.length
101
+ ? await djClient.getNodesByNames(nodeNames)
102
+ : [];
103
+
104
+ const enriched = enrichWithNodeInfo(history, nodes);
105
+ setNotifications(enriched);
106
+ setUnreadCount(
107
+ calculateUnreadCount(
108
+ history,
109
+ currentUser?.last_viewed_notifications_at,
110
+ ),
111
+ );
112
+ } catch (error) {
113
+ console.error('Error fetching notifications:', error);
114
+ } finally {
115
+ setLoading(false);
116
+ }
117
+ }
118
+ fetchNotifications();
119
+ }, [djClient, currentUser, userLoading]);
120
+
121
+ // Close dropdown when clicking outside
122
+ useEffect(() => {
123
+ const handleClickOutside = (event: MouseEvent) => {
124
+ const target = event.target as HTMLElement;
125
+ if (!target.closest('.notification-bell-dropdown')) {
126
+ setShowDropdown(false);
127
+ onDropdownToggle?.(false);
128
+ }
129
+ };
130
+ document.addEventListener('click', handleClickOutside);
131
+ return () => document.removeEventListener('click', handleClickOutside);
132
+ }, [onDropdownToggle]);
133
+
134
+ const handleToggle = (e: React.MouseEvent) => {
135
+ e.stopPropagation();
136
+ const willOpen = !showDropdown;
137
+
138
+ // Mark as read when opening
139
+ if (willOpen && unreadCount > 0) {
140
+ djClient.markNotificationsRead();
141
+ setUnreadCount(0);
142
+ }
143
+
144
+ setShowDropdown(willOpen);
145
+ onDropdownToggle?.(willOpen);
146
+ };
147
+
148
+ return (
149
+ <div className="nav-dropdown notification-bell-dropdown">
150
+ <button className="nav-icon-button" onClick={handleToggle}>
151
+ <NotificationIcon />
152
+ {unreadCount > 0 && (
153
+ <span className="notification-badge">{unreadCount}</span>
154
+ )}
155
+ </button>
156
+ {showDropdown && (
157
+ <div className="nav-dropdown-menu notifications-menu">
158
+ <div className="dropdown-header">
159
+ <span className="header-left">
160
+ <NotificationIcon /> Updates
161
+ </span>
162
+ <a
163
+ href="/settings#notifications"
164
+ className="header-settings"
165
+ title="Manage subscriptions"
166
+ >
167
+ <SettingsIcon />
168
+ </a>
169
+ </div>
170
+ <div className="notifications-list">
171
+ {loading ? (
172
+ <div className="dropdown-item">
173
+ <LoadingIcon centered={false} />
174
+ </div>
175
+ ) : notifications.length === 0 ? (
176
+ <div className="dropdown-item text-muted">
177
+ No updates on watched nodes
178
+ </div>
179
+ ) : (
180
+ notifications.slice(0, 5).map(entry => {
181
+ const version = entry.details?.version;
182
+ const href = version
183
+ ? `/nodes/${entry.entity_name}/revisions/${version}`
184
+ : `/nodes/${entry.entity_name}/history`;
185
+ return (
186
+ <a key={entry.id} className="notification-item" href={href}>
187
+ <span className="notification-node">
188
+ <span className="notification-title">
189
+ {entry.display_name || entry.entity_name}
190
+ {version && (
191
+ <span className="badge version">{version}</span>
192
+ )}
193
+ </span>
194
+ {entry.display_name && (
195
+ <span className="notification-entity">
196
+ {entry.entity_name}
197
+ </span>
198
+ )}
199
+ </span>
200
+ <span className="notification-meta">
201
+ {entry.node_type && (
202
+ <span
203
+ className={`node_type__${entry.node_type} badge node_type`}
204
+ >
205
+ {entry.node_type.toUpperCase()}
206
+ </span>
207
+ )}
208
+ {entry.activity_type}d by{' '}
209
+ <span style={{ color: '#333' }}>{entry.user}</span> ·{' '}
210
+ {formatRelativeTime(entry.created_at)}
211
+ </span>
212
+ </a>
213
+ );
214
+ })
215
+ )}
216
+ </div>
217
+ {notifications.length > 0 && (
218
+ <>
219
+ <hr className="dropdown-divider" />
220
+ <a className="dropdown-item view-all" href="/notifications">
221
+ View all
222
+ </a>
223
+ </>
224
+ )}
225
+ </div>
226
+ )}
227
+ </div>
228
+ );
229
+ }
@@ -0,0 +1,172 @@
1
+ import { Light as SyntaxHighlighter } from 'react-syntax-highlighter';
2
+ import { solarizedDark } from 'react-syntax-highlighter/src/styles/hljs';
3
+ import React from 'react';
4
+
5
+ export default function QueryInfo({
6
+ id,
7
+ state,
8
+ engine_name,
9
+ engine_version,
10
+ errors,
11
+ links,
12
+ output_table,
13
+ scheduled,
14
+ started,
15
+ finished,
16
+ numRows,
17
+ isList = false,
18
+ }) {
19
+ return isList === false ? (
20
+ <div className="table-responsive">
21
+ <table className="card-inner-table table">
22
+ <thead className="fs-7 fw-bold text-gray-400 border-bottom-0">
23
+ <tr>
24
+ <th>Query ID</th>
25
+ <th>Engine</th>
26
+ <th>State</th>
27
+ <th>Scheduled</th>
28
+ <th>Started</th>
29
+ <th>Errors</th>
30
+ <th>Links</th>
31
+ <th>Output Table</th>
32
+ <th>Number of Rows</th>
33
+ </tr>
34
+ </thead>
35
+ <tbody>
36
+ <tr>
37
+ <td>
38
+ <span className="rounded-pill badge bg-secondary-soft">{id}</span>
39
+ </td>
40
+ <td>
41
+ <span className="rounded-pill badge bg-secondary-soft">
42
+ {engine_name}
43
+ {' - '}
44
+ {engine_version}
45
+ </span>
46
+ </td>
47
+ <td>{state}</td>
48
+ <td>{scheduled}</td>
49
+ <td>{started}</td>
50
+ <td>
51
+ {errors?.length ? (
52
+ errors.map((e, idx) => (
53
+ <p key={`error-${idx}`}>
54
+ <span className="rounded-pill badge bg-secondary-error">
55
+ {e}
56
+ </span>
57
+ </p>
58
+ ))
59
+ ) : (
60
+ <></>
61
+ )}
62
+ </td>
63
+ <td>
64
+ {links?.length ? (
65
+ links.map((link, idx) => (
66
+ <p key={idx}>
67
+ <a href={link} target="_blank" rel="noreferrer">
68
+ {link}
69
+ </a>
70
+ </p>
71
+ ))
72
+ ) : (
73
+ <></>
74
+ )}
75
+ </td>
76
+ <td>{output_table}</td>
77
+ <td>{numRows}</td>
78
+ </tr>
79
+ </tbody>
80
+ </table>
81
+ </div>
82
+ ) : (
83
+ <div className="rightbottom">
84
+ <ul style={{ padding: '20px' }}>
85
+ <li className={'query-info'}>
86
+ <label>Query ID</label>{' '}
87
+ <span className="tag_value rounded-pill badge">
88
+ {links?.length ? (
89
+ <a
90
+ href={links[links.length - 1]}
91
+ target={'_blank'}
92
+ rel="noreferrer"
93
+ >
94
+ {id}
95
+ </a>
96
+ ) : (
97
+ id
98
+ )}
99
+ </span>
100
+ </li>
101
+ <li className={'query-info'}>
102
+ <label>State</label>
103
+ <span className="tag_value rounded-pill badge">{state}</span>
104
+ </li>
105
+ <li className={'query-info'}>
106
+ <label>Engine</label>{' '}
107
+ <span className="tag_value rounded-pill badge">
108
+ {engine_name}
109
+ {' - '}
110
+ {engine_version}
111
+ </span>
112
+ </li>
113
+ <li className={'query-info'}>
114
+ <label>Scheduled</label> {scheduled}
115
+ </li>
116
+ <li className={'query-info'}>
117
+ <label>Started</label> {started}
118
+ </li>
119
+ <li className={'query-info'}>
120
+ <label>Finished</label> {finished}
121
+ </li>
122
+ <li className={'query-info'}>
123
+ <label>Logs</label>{' '}
124
+ {errors?.length ? (
125
+ errors.map(error => (
126
+ <div
127
+ style={{
128
+ height: '800px',
129
+ width: '80%',
130
+ overflow: 'scroll',
131
+ borderRadius: '0',
132
+ border: '1px solid #ccc',
133
+ }}
134
+ className="queryrunner-query"
135
+ >
136
+ <SyntaxHighlighter
137
+ language="javascript"
138
+ style={solarizedDark}
139
+ wrapLines={true}
140
+ >
141
+ {error}
142
+ </SyntaxHighlighter>
143
+ </div>
144
+ ))
145
+ ) : (
146
+ <></>
147
+ )}
148
+ </li>
149
+ <li className={'query-info'}>
150
+ <label>Links:</label>{' '}
151
+ {links?.length ? (
152
+ links.map((link, idx) => (
153
+ <p key={idx}>
154
+ <a href={link} target="_blank" rel="noreferrer">
155
+ {link}
156
+ </a>
157
+ </p>
158
+ ))
159
+ ) : (
160
+ <></>
161
+ )}
162
+ </li>
163
+ <li className={'query-info'}>
164
+ <label>Output Table:</label> {output_table}
165
+ </li>
166
+ <li className={'query-info'}>
167
+ <label>Rows:</label> {numRows}
168
+ </li>
169
+ </ul>
170
+ </div>
171
+ );
172
+ }
@@ -0,0 +1,94 @@
1
+ import { useState, useEffect, useContext } from 'react';
2
+ import DJClientContext from '../providers/djclient';
3
+ import Fuse from 'fuse.js';
4
+
5
+ import './search.css';
6
+
7
+ export default function Search() {
8
+ const [fuse, setFuse] = useState();
9
+ const [searchValue, setSearchValue] = useState('');
10
+ const [searchResults, setSearchResults] = useState([]);
11
+
12
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
13
+
14
+ const truncate = str => {
15
+ if (str === null) {
16
+ return '';
17
+ }
18
+ return str.length > 100 ? str.substring(0, 90) + '...' : str;
19
+ };
20
+
21
+ useEffect(() => {
22
+ const fetchNodes = async () => {
23
+ try {
24
+ const [data, tags] = await Promise.all([
25
+ djClient.nodeDetails(),
26
+ djClient.listTags(),
27
+ ]);
28
+ const allEntities = data.concat(
29
+ (tags || []).map(tag => {
30
+ tag.type = 'tag';
31
+ return tag;
32
+ }),
33
+ );
34
+ const fuse = new Fuse(allEntities || [], {
35
+ keys: [
36
+ 'name', // will be assigned a `weight` of 1
37
+ { name: 'description', weight: 2 },
38
+ { name: 'display_name', weight: 3 },
39
+ { name: 'type', weight: 4 },
40
+ { name: 'tag_type', weight: 5 },
41
+ ],
42
+ });
43
+ setFuse(fuse);
44
+ } catch (error) {
45
+ console.error('Error fetching nodes or tags:', error);
46
+ }
47
+ };
48
+ fetchNodes();
49
+ }, []);
50
+
51
+ const handleChange = e => {
52
+ setSearchValue(e.target.value);
53
+ if (fuse) {
54
+ setSearchResults(fuse.search(e.target.value).map(result => result.item));
55
+ }
56
+ };
57
+
58
+ return (
59
+ <div>
60
+ <form
61
+ className="search-box"
62
+ onSubmit={e => {
63
+ e.preventDefault();
64
+ }}
65
+ >
66
+ <input
67
+ type="text"
68
+ placeholder="Search"
69
+ name="search"
70
+ value={searchValue}
71
+ onChange={handleChange}
72
+ />
73
+ </form>
74
+ <div className="search-results">
75
+ {searchResults.slice(0, 20).map(item => {
76
+ const itemUrl =
77
+ item.type !== 'tag' ? `/nodes/${item.name}` : `/tags/${item.name}`;
78
+ return (
79
+ <a href={itemUrl}>
80
+ <div key={item.name} className="search-result-item">
81
+ <span className={`node_type__${item.type} badge node_type`}>
82
+ {item.type}
83
+ </span>
84
+ {item.display_name} (<b>{item.name}</b>){' '}
85
+ {item.description ? '- ' : ' '}
86
+ {truncate(item.description || '')}
87
+ </div>
88
+ </a>
89
+ );
90
+ })}
91
+ </div>
92
+ </div>
93
+ );
94
+ }
@@ -7,7 +7,14 @@ export default class Tab extends Component {
7
7
  <div className={selectedTab === id ? 'col active' : 'col'}>
8
8
  <div className="header-tabs nav-overflow nav nav-tabs">
9
9
  <div className="nav-item">
10
- <button id={id} className="nav-link" tabIndex="0" onClick={onClick}>
10
+ <button
11
+ id={id}
12
+ className="nav-link"
13
+ tabIndex="0"
14
+ onClick={onClick}
15
+ aria-label={this.props.name}
16
+ aria-hidden="false"
17
+ >
11
18
  {this.props.name}
12
19
  </button>
13
20
  </div>
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+
3
+ const ToggleSwitch = ({ checked, onChange, toggleName }) => (
4
+ <>
5
+ <input
6
+ id="show-compiled-sql-toggle"
7
+ role="checkbox"
8
+ aria-label="ToggleSwitch"
9
+ aria-hidden="false"
10
+ type="checkbox"
11
+ className="checkbox"
12
+ checked={checked}
13
+ onChange={e => onChange(e.target.checked)}
14
+ />
15
+ <label htmlFor="show-compiled-sql-toggle" className="switch"></label>{' '}
16
+ {toggleName}
17
+ </>
18
+ );
19
+
20
+ export default ToggleSwitch;
@@ -0,0 +1,92 @@
1
+ import { useContext, useEffect, useState } from 'react';
2
+ import DJClientContext from '../providers/djclient';
3
+ import { useCurrentUser } from '../providers/UserProvider';
4
+
5
+ interface User {
6
+ id: number;
7
+ username: string;
8
+ email: string;
9
+ name?: string;
10
+ }
11
+
12
+ // Extract initials from user's name or username
13
+ const getInitials = (user: User | null): string => {
14
+ if (!user) return '?';
15
+ if (user.name) {
16
+ return user.name
17
+ .split(' ')
18
+ .map(n => n[0])
19
+ .join('')
20
+ .toUpperCase()
21
+ .slice(0, 2);
22
+ }
23
+ return user.username.slice(0, 2).toUpperCase();
24
+ };
25
+
26
+ interface UserMenuProps {
27
+ onDropdownToggle?: (isOpen: boolean) => void;
28
+ forceClose?: boolean;
29
+ }
30
+
31
+ export default function UserMenu({
32
+ onDropdownToggle,
33
+ forceClose,
34
+ }: UserMenuProps) {
35
+ const djClient = useContext(DJClientContext).DataJunctionAPI;
36
+ const { currentUser } = useCurrentUser();
37
+ const [showDropdown, setShowDropdown] = useState(false);
38
+
39
+ // Close when forceClose becomes true
40
+ useEffect(() => {
41
+ if (forceClose && showDropdown) {
42
+ setShowDropdown(false);
43
+ }
44
+ }, [forceClose, showDropdown]);
45
+
46
+ // Close dropdown when clicking outside
47
+ useEffect(() => {
48
+ const handleClickOutside = (event: MouseEvent) => {
49
+ const target = event.target as HTMLElement;
50
+ if (!target.closest('.user-menu-dropdown')) {
51
+ setShowDropdown(false);
52
+ onDropdownToggle?.(false);
53
+ }
54
+ };
55
+ document.addEventListener('click', handleClickOutside);
56
+ return () => document.removeEventListener('click', handleClickOutside);
57
+ }, [onDropdownToggle]);
58
+
59
+ const handleToggle = (e: React.MouseEvent) => {
60
+ e.stopPropagation();
61
+ const willOpen = !showDropdown;
62
+ setShowDropdown(willOpen);
63
+ onDropdownToggle?.(willOpen);
64
+ };
65
+
66
+ const handleLogout = async () => {
67
+ await djClient.logout();
68
+ window.location.reload();
69
+ };
70
+
71
+ return (
72
+ <div className="nav-dropdown user-menu-dropdown">
73
+ <button className="avatar-button" onClick={handleToggle}>
74
+ {getInitials(currentUser as User | null)}
75
+ </button>
76
+ {showDropdown && (
77
+ <div className="nav-dropdown-menu">
78
+ <div className="dropdown-header">
79
+ {currentUser?.username || 'User'}
80
+ </div>
81
+ <hr className="dropdown-divider" />
82
+ <a className="dropdown-item" href="/settings">
83
+ Settings
84
+ </a>
85
+ <a className="dropdown-item" href="/" onClick={handleLogout}>
86
+ Logout
87
+ </a>
88
+ </div>
89
+ )}
90
+ </div>
91
+ );
92
+ }