datajunction-ui 0.0.55 → 0.0.57

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 (30) hide show
  1. package/package.json +1 -1
  2. package/src/app/components/NamespaceHeader.jsx +423 -12
  3. package/src/app/components/NodeListActions.jsx +10 -7
  4. package/src/app/components/__tests__/GitModals.test.jsx +1293 -0
  5. package/src/app/components/__tests__/NamespaceHeader.test.jsx +905 -16
  6. package/src/app/components/__tests__/NodeListActions.test.jsx +5 -3
  7. package/src/app/components/__tests__/__snapshots__/NamespaceHeader.test.jsx.snap +41 -0
  8. package/src/app/components/git/CreateBranchModal.jsx +229 -0
  9. package/src/app/components/git/CreatePRModal.jsx +270 -0
  10. package/src/app/components/git/DeleteBranchModal.jsx +173 -0
  11. package/src/app/components/git/GitSettingsModal.jsx +375 -0
  12. package/src/app/components/git/SyncToGitModal.jsx +219 -0
  13. package/src/app/components/git/__tests__/GitSettingsModal.test.jsx +301 -0
  14. package/src/app/components/git/index.js +5 -0
  15. package/src/app/icons/DeleteIcon.jsx +3 -3
  16. package/src/app/icons/EditIcon.jsx +3 -3
  17. package/src/app/icons/EyeIcon.jsx +3 -4
  18. package/src/app/icons/JupyterExportIcon.jsx +3 -7
  19. package/src/app/icons/PythonIcon.jsx +3 -3
  20. package/src/app/pages/AddEditNodePage/index.jsx +8 -5
  21. package/src/app/pages/NamespacePage/index.jsx +34 -21
  22. package/src/app/pages/NodePage/ClientCodePopover.jsx +3 -7
  23. package/src/app/pages/NodePage/NodeInfoTab.jsx +10 -3
  24. package/src/app/pages/NodePage/NotebookDownload.jsx +4 -10
  25. package/src/app/pages/NodePage/WatchNodeButton.jsx +7 -12
  26. package/src/app/pages/NodePage/index.jsx +42 -13
  27. package/src/app/services/DJService.js +218 -1
  28. package/src/styles/index.css +3 -0
  29. package/src/styles/node-creation.scss +22 -0
  30. package/src/styles/settings.css +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "datajunction-ui",
3
- "version": "0.0.55",
3
+ "version": "0.0.57",
4
4
  "description": "DataJunction UI",
5
5
  "module": "src/index.tsx",
6
6
  "repository": {
@@ -1,21 +1,48 @@
1
1
  import { useContext, useEffect, useState, useRef } from 'react';
2
2
  import DJClientContext from '../providers/djclient';
3
+ import {
4
+ GitSettingsModal,
5
+ CreateBranchModal,
6
+ SyncToGitModal,
7
+ CreatePRModal,
8
+ DeleteBranchModal,
9
+ } from './git';
3
10
 
4
- export default function NamespaceHeader({ namespace, children }) {
11
+ export default function NamespaceHeader({
12
+ namespace,
13
+ children,
14
+ onGitConfigLoaded,
15
+ }) {
5
16
  const djClient = useContext(DJClientContext).DataJunctionAPI;
6
17
  const [sources, setSources] = useState(null);
7
18
  const [recentDeployments, setRecentDeployments] = useState([]);
8
19
  const [deploymentsDropdownOpen, setDeploymentsDropdownOpen] = useState(false);
9
20
  const dropdownRef = useRef(null);
10
21
 
22
+ // Git config state
23
+ const [gitConfig, setGitConfig] = useState(null);
24
+ const [gitConfigLoading, setGitConfigLoading] = useState(true);
25
+ const [parentGitConfig, setParentGitConfig] = useState(null);
26
+ const [existingPR, setExistingPR] = useState(null);
27
+
28
+ // Modal states
29
+ const [showGitSettings, setShowGitSettings] = useState(false);
30
+ const [showCreateBranch, setShowCreateBranch] = useState(false);
31
+ const [showSyncToGit, setShowSyncToGit] = useState(false);
32
+ const [showCreatePR, setShowCreatePR] = useState(false);
33
+ const [showDeleteBranch, setShowDeleteBranch] = useState(false);
34
+
11
35
  useEffect(() => {
12
- const fetchSources = async () => {
36
+ // Reset loading state when namespace changes
37
+ setGitConfigLoading(true);
38
+
39
+ const fetchData = async () => {
13
40
  if (namespace) {
41
+ // Fetch deployment sources
14
42
  try {
15
43
  const data = await djClient.namespaceSources(namespace);
16
44
  setSources(data);
17
45
 
18
- // Fetch recent deployments for this namespace
19
46
  try {
20
47
  const deployments = await djClient.listDeployments(namespace, 5);
21
48
  setRecentDeployments(deployments || []);
@@ -26,9 +53,47 @@ export default function NamespaceHeader({ namespace, children }) {
26
53
  } catch (e) {
27
54
  // Silently fail - badge just won't show
28
55
  }
56
+
57
+ // Fetch git config
58
+ try {
59
+ const config = await djClient.getNamespaceGitConfig(namespace);
60
+ setGitConfig(config);
61
+ if (onGitConfigLoaded) {
62
+ onGitConfigLoaded(config);
63
+ }
64
+
65
+ // If this is a branch namespace, fetch parent's git config and check for existing PR
66
+ if (config?.parent_namespace) {
67
+ try {
68
+ const parentConfig = await djClient.getNamespaceGitConfig(
69
+ config.parent_namespace,
70
+ );
71
+ setParentGitConfig(parentConfig);
72
+ } catch (e) {
73
+ console.error('Failed to fetch parent git config:', e);
74
+ }
75
+
76
+ // Check for existing PR
77
+ try {
78
+ const pr = await djClient.getPullRequest(namespace);
79
+ setExistingPR(pr);
80
+ } catch (e) {
81
+ // No PR or error - that's fine
82
+ setExistingPR(null);
83
+ }
84
+ }
85
+ } catch (e) {
86
+ // Git config not available
87
+ setGitConfig(null);
88
+ if (onGitConfigLoaded) {
89
+ onGitConfigLoaded(null);
90
+ }
91
+ } finally {
92
+ setGitConfigLoading(false);
93
+ }
29
94
  }
30
95
  };
31
- fetchSources();
96
+ fetchData();
32
97
  }, [djClient, namespace]);
33
98
 
34
99
  // Close dropdown when clicking outside
@@ -44,6 +109,80 @@ export default function NamespaceHeader({ namespace, children }) {
44
109
 
45
110
  const namespaceParts = namespace ? namespace.split('.') : [];
46
111
 
112
+ const hasGitConfig = gitConfig?.github_repo_path && gitConfig?.git_branch;
113
+ const isBranchNamespace = !!gitConfig?.parent_namespace;
114
+
115
+ // Handlers for git operations
116
+ const handleSaveGitConfig = async config => {
117
+ const result = await djClient.updateNamespaceGitConfig(namespace, config);
118
+ if (!result?._error) {
119
+ setGitConfig(result);
120
+ }
121
+ return result;
122
+ };
123
+ const handleRemoveGitConfig = async () => {
124
+ const result = await djClient.deleteNamespaceGitConfig(namespace);
125
+ if (!result?._error) {
126
+ setGitConfig(null);
127
+ }
128
+ };
129
+
130
+ const handleCreateBranch = async branchName => {
131
+ return await djClient.createBranch(namespace, branchName);
132
+ };
133
+
134
+ const handleSyncToGit = async commitMessage => {
135
+ return await djClient.syncNamespaceToGit(namespace, commitMessage);
136
+ };
137
+
138
+ const handleCreatePR = async (title, body, onProgress) => {
139
+ // First sync changes to git using PR title as commit message
140
+ if (onProgress) onProgress('syncing');
141
+ const syncResult = await djClient.syncNamespaceToGit(namespace, title);
142
+ if (syncResult?._error) {
143
+ return syncResult;
144
+ }
145
+
146
+ // Then create the PR
147
+ if (onProgress) onProgress('creating');
148
+ const result = await djClient.createPullRequest(namespace, title, body);
149
+ if (result && !result._error) {
150
+ setExistingPR(result);
151
+ }
152
+ return result;
153
+ };
154
+
155
+ const handleDeleteBranch = async deleteGitBranch => {
156
+ return await djClient.deleteBranch(
157
+ gitConfig.parent_namespace,
158
+ namespace,
159
+ deleteGitBranch,
160
+ );
161
+ };
162
+
163
+ // Button style helpers
164
+ const buttonStyle = {
165
+ height: '28px',
166
+ padding: '0 10px',
167
+ fontSize: '12px',
168
+ border: '1px solid #e2e8f0',
169
+ borderRadius: '4px',
170
+ backgroundColor: '#ffffff',
171
+ color: '#475569',
172
+ cursor: 'pointer',
173
+ display: 'flex',
174
+ alignItems: 'center',
175
+ gap: '4px',
176
+ whiteSpace: 'nowrap',
177
+ };
178
+
179
+ const primaryButtonStyle = {
180
+ ...buttonStyle,
181
+ backgroundColor: '#3b82f6',
182
+ borderColor: '#3b82f6',
183
+ color: '#ffffff',
184
+ };
185
+
47
186
  return (
48
187
  <div
49
188
  style={{
@@ -123,7 +262,82 @@ export default function NamespaceHeader({ namespace, children }) {
123
262
  </span>
124
263
  )}
125
264
 
126
- {/* Deployment badge + dropdown */}
265
+ {/* Branch indicator */}
266
+ {isBranchNamespace && (
267
+ <span
268
+ style={{
269
+ display: 'flex',
270
+ alignItems: 'center',
271
+ gap: '4px',
272
+ padding: '2px 8px',
273
+ backgroundColor: '#dbeafe',
274
+ borderRadius: '12px',
275
+ fontSize: '11px',
276
+ color: '#1e40af',
277
+ marginLeft: '4px',
278
+ }}
279
+ >
280
+ <svg
281
+ xmlns="http://www.w3.org/2000/svg"
282
+ width="12"
283
+ height="12"
284
+ viewBox="0 0 24 24"
285
+ fill="none"
286
+ stroke="currentColor"
287
+ strokeWidth="2"
288
+ strokeLinecap="round"
289
+ strokeLinejoin="round"
290
+ >
291
+ <line x1="6" y1="3" x2="6" y2="15" />
292
+ <circle cx="18" cy="6" r="3" />
293
+ <circle cx="6" cy="18" r="3" />
294
+ <path d="M18 9a9 9 0 0 1-9 9" />
295
+ </svg>
296
+ Branch of{' '}
297
+ <a
298
+ href={`/namespaces/${gitConfig.parent_namespace}`}
299
+ style={{ color: '#1e40af', textDecoration: 'underline' }}
300
+ >
301
+ {gitConfig.parent_namespace}
302
+ </a>
303
+ </span>
304
+ )}
305
+
306
+ {/* Git-only (read-only) indicator */}
307
+ {gitConfig?.git_only && (
308
+ <span
309
+ style={{
310
+ display: 'flex',
311
+ alignItems: 'center',
312
+ gap: '4px',
313
+ padding: '2px 8px',
314
+ backgroundColor: '#fef3c7',
315
+ borderRadius: '12px',
316
+ fontSize: '11px',
317
+ color: '#92400e',
318
+ marginLeft: '4px',
319
+ }}
320
+ title="This namespace is git-only. Changes must be made via git and deployed."
321
+ >
322
+ <svg
323
+ xmlns="http://www.w3.org/2000/svg"
324
+ width="12"
325
+ height="12"
326
+ viewBox="0 0 24 24"
327
+ fill="none"
328
+ stroke="currentColor"
329
+ strokeWidth="2"
330
+ strokeLinecap="round"
331
+ strokeLinejoin="round"
332
+ >
333
+ <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
334
+ <path d="M7 11V7a5 5 0 0 1 10 0v4" />
335
+ </svg>
336
+ Read-only
337
+ </span>
338
+ )}
339
+
340
+ {/* Deployment badge + dropdown (existing functionality) */}
127
341
  {sources && sources.total_deployments > 0 && (
128
342
  <div
129
343
  style={{ position: 'relative', marginLeft: '8px' }}
@@ -166,7 +380,7 @@ export default function NamespaceHeader({ namespace, children }) {
166
380
  <circle cx="6" cy="18" r="3"></circle>
167
381
  <path d="M18 9a9 9 0 0 1-9 9"></path>
168
382
  </svg>
169
- Git Managed
383
+ Deployed from Git
170
384
  </>
171
385
  ) : (
172
386
  <>
@@ -437,12 +651,209 @@ export default function NamespaceHeader({ namespace, children }) {
437
651
  )}
438
652
  </div>
439
653
 
440
- {/* Right side actions passed as children */}
441
- {children && (
442
- <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
443
- {children}
444
- </div>
445
- )}
654
+ {/* Right side: git actions + children */}
655
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
656
+ {/* Git controls for non-branch namespaces */}
657
+ {namespace && !isBranchNamespace && !gitConfigLoading && (
658
+ <>
659
+ <button
660
+ style={buttonStyle}
661
+ onClick={() => setShowGitSettings(true)}
662
+ title="Git Settings"
663
+ >
664
+ <svg
665
+ xmlns="http://www.w3.org/2000/svg"
666
+ width="14"
667
+ height="14"
668
+ viewBox="0 0 24 24"
669
+ fill="none"
670
+ stroke="currentColor"
671
+ strokeWidth="2"
672
+ strokeLinecap="round"
673
+ strokeLinejoin="round"
674
+ >
675
+ <circle cx="12" cy="12" r="3" />
676
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" />
677
+ </svg>
678
+ Git Settings
679
+ </button>
680
+ {hasGitConfig ? (
681
+ <button
682
+ style={primaryButtonStyle}
683
+ onClick={() => setShowCreateBranch(true)}
684
+ >
685
+ <svg
686
+ xmlns="http://www.w3.org/2000/svg"
687
+ width="14"
688
+ height="14"
689
+ viewBox="0 0 24 24"
690
+ fill="none"
691
+ stroke="currentColor"
692
+ strokeWidth="2"
693
+ strokeLinecap="round"
694
+ strokeLinejoin="round"
695
+ >
696
+ <line x1="12" y1="5" x2="12" y2="19" />
697
+ <line x1="5" y1="12" x2="19" y2="12" />
698
+ </svg>
699
+ New Branch
700
+ </button>
701
+ ) : (
702
+ <></>
703
+ )}
704
+ </>
705
+ )}
706
+
707
+ {/* Git controls for branch namespaces */}
708
+ {isBranchNamespace && hasGitConfig && (
709
+ <>
710
+ <button style={buttonStyle} onClick={() => setShowSyncToGit(true)}>
711
+ <svg
712
+ xmlns="http://www.w3.org/2000/svg"
713
+ width="14"
714
+ height="14"
715
+ viewBox="0 0 24 24"
716
+ fill="none"
717
+ stroke="currentColor"
718
+ strokeWidth="2"
719
+ strokeLinecap="round"
720
+ strokeLinejoin="round"
721
+ >
722
+ <path d="M21 12a9 9 0 0 1-9 9m9-9a9 9 0 0 0-9-9m9 9H3m9 9a9 9 0 0 1-9-9m9 9c1.66 0 3-4.03 3-9s-1.34-9-3-9m0 18c-1.66 0-3-4.03-3-9s1.34-9 3-9m-9 9a9 9 0 0 1 9-9" />
723
+ </svg>
724
+ Sync to Git
725
+ </button>
726
+ {existingPR ? (
727
+ <a
728
+ href={existingPR.pr_url}
729
+ target="_blank"
730
+ rel="noopener noreferrer"
731
+ style={{
732
+ ...primaryButtonStyle,
733
+ textDecoration: 'none',
734
+ backgroundColor: '#16a34a',
735
+ borderColor: '#16a34a',
736
+ }}
737
+ >
738
+ <svg
739
+ xmlns="http://www.w3.org/2000/svg"
740
+ width="14"
741
+ height="14"
742
+ viewBox="0 0 24 24"
743
+ fill="none"
744
+ stroke="currentColor"
745
+ strokeWidth="2"
746
+ strokeLinecap="round"
747
+ strokeLinejoin="round"
748
+ >
749
+ <circle cx="18" cy="18" r="3" />
750
+ <circle cx="6" cy="6" r="3" />
751
+ <path d="M13 6h3a2 2 0 0 1 2 2v7" />
752
+ <line x1="6" y1="9" x2="6" y2="21" />
753
+ </svg>
754
+ View PR #{existingPR.pr_number}
755
+ </a>
756
+ ) : (
757
+ <button
758
+ style={primaryButtonStyle}
759
+ onClick={() => setShowCreatePR(true)}
760
+ >
761
+ <svg
762
+ xmlns="http://www.w3.org/2000/svg"
763
+ width="14"
764
+ height="14"
765
+ viewBox="0 0 24 24"
766
+ fill="none"
767
+ stroke="currentColor"
768
+ strokeWidth="2"
769
+ strokeLinecap="round"
770
+ strokeLinejoin="round"
771
+ >
772
+ <circle cx="18" cy="18" r="3" />
773
+ <circle cx="6" cy="6" r="3" />
774
+ <path d="M13 6h3a2 2 0 0 1 2 2v7" />
775
+ <line x1="6" y1="9" x2="6" y2="21" />
776
+ </svg>
777
+ Create PR
778
+ </button>
779
+ )}
780
+ <button
781
+ style={{
782
+ ...buttonStyle,
783
+ color: '#dc2626',
784
+ borderColor: '#fecaca',
785
+ }}
786
+ onClick={() => setShowDeleteBranch(true)}
787
+ title="Delete Branch"
788
+ >
789
+ <svg
790
+ xmlns="http://www.w3.org/2000/svg"
791
+ width="14"
792
+ height="14"
793
+ viewBox="0 0 24 24"
794
+ fill="none"
795
+ stroke="currentColor"
796
+ strokeWidth="2"
797
+ strokeLinecap="round"
798
+ strokeLinejoin="round"
799
+ >
800
+ <path d="M3 6h18" />
801
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
802
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
803
+ </svg>
804
+ </button>
805
+ </>
806
+ )}
807
+
808
+ {/* Additional actions passed as children */}
809
+ {children}
810
+ </div>
811
+
812
+ {/* Modals */}
813
+ <GitSettingsModal
814
+ isOpen={showGitSettings}
815
+ onClose={() => setShowGitSettings(false)}
816
+ onSave={handleSaveGitConfig}
817
+ onRemove={handleRemoveGitConfig}
818
+ currentConfig={gitConfig}
819
+ namespace={namespace}
820
+ />
821
+
822
+ <CreateBranchModal
823
+ isOpen={showCreateBranch}
824
+ onClose={() => setShowCreateBranch(false)}
825
+ onCreate={handleCreateBranch}
826
+ namespace={namespace}
827
+ gitBranch={gitConfig?.git_branch}
828
+ />
829
+
830
+ <SyncToGitModal
831
+ isOpen={showSyncToGit}
832
+ onClose={() => setShowSyncToGit(false)}
833
+ onSync={handleSyncToGit}
834
+ namespace={namespace}
835
+ gitBranch={gitConfig?.git_branch}
836
+ repoPath={gitConfig?.github_repo_path}
837
+ />
838
+
839
+ <CreatePRModal
840
+ isOpen={showCreatePR}
841
+ onClose={() => setShowCreatePR(false)}
842
+ onCreate={handleCreatePR}
843
+ namespace={namespace}
844
+ gitBranch={gitConfig?.git_branch}
845
+ parentBranch={parentGitConfig?.git_branch}
846
+ repoPath={gitConfig?.github_repo_path}
847
+ />
848
+
849
+ <DeleteBranchModal
850
+ isOpen={showDeleteBranch}
851
+ onClose={() => setShowDeleteBranch(false)}
852
+ onDelete={handleDeleteBranch}
853
+ namespace={namespace}
854
+ gitBranch={gitConfig?.git_branch}
855
+ parentNamespace={gitConfig?.parent_namespace}
856
+ />
446
857
  </div>
447
858
  );
448
859
  }
@@ -6,9 +6,8 @@ import { Form, Formik } from 'formik';
6
6
  import { useContext } from 'react';
7
7
  import { displayMessageAfterSubmit } from '../../utils/form';
8
8
 
9
- export default function NodeListActions({ nodeName }) {
10
- const [editButton, setEditButton] = React.useState(<EditIcon />);
11
- const [deleteButton, setDeleteButton] = React.useState(<DeleteIcon />);
9
+ export default function NodeListActions({ nodeName, iconSize = 20 }) {
10
+ const [deleted, setDeleted] = React.useState(false);
12
11
 
13
12
  const djClient = useContext(DJClientContext).DataJunctionAPI;
14
13
  const deleteNode = async (values, { setStatus }) => {
@@ -22,8 +21,8 @@ export default function NodeListActions({ nodeName }) {
22
21
  setStatus({
23
22
  success: <>Successfully deleted node {values.nodeName}</>,
24
23
  });
25
- setEditButton(''); // hide the Edit button
26
- setDeleteButton(''); // hide the Delete button
24
+ // Delay hiding component so success message is visible briefly
25
+ setTimeout(() => setDeleted(true), 1500);
27
26
  } else {
28
27
  setStatus({
29
28
  failure: `${json.message}`,
@@ -35,10 +34,14 @@ export default function NodeListActions({ nodeName }) {
35
34
  nodeName: nodeName,
36
35
  };
37
36
 
37
+ if (deleted) {
38
+ return null;
39
+ }
40
+
38
41
  return (
39
42
  <div>
40
43
  <a href={`/nodes/${nodeName}/edit`} style={{ marginLeft: '0.5rem' }}>
41
- {editButton}
44
+ <EditIcon size={iconSize} />
42
45
  </a>
43
46
  <Formik initialValues={initialValues} onSubmit={deleteNode}>
44
47
  {function Render({ status, setFieldValue }) {
@@ -56,7 +59,7 @@ export default function NodeListActions({ nodeName }) {
56
59
  cursor: 'pointer',
57
60
  }}
58
61
  >
59
- {deleteButton}
62
+ <DeleteIcon size={iconSize} />
60
63
  </button>
61
64
  </>
62
65
  }