datajunction-ui 0.0.55 → 0.0.56

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