datajunction-ui 0.0.59 → 0.0.62

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.
@@ -9,6 +9,7 @@ export function CreateBranchModal({
9
9
  onCreate,
10
10
  namespace,
11
11
  gitBranch,
12
+ isGitRoot,
12
13
  }) {
13
14
  const [branchName, setBranchName] = useState('');
14
15
  const [creating, setCreating] = useState(false);
@@ -16,10 +17,22 @@ export function CreateBranchModal({
16
17
  const [result, setResult] = useState(null);
17
18
 
18
19
  // Convert branch name to expected namespace suffix for preview
20
+ // If creating from git root (e.g., "demo.metrics"), use full namespace as prefix: "demo.metrics.rr"
21
+ // If creating from branch namespace (e.g., "demo.main"), use parent prefix: "demo.rr"
19
22
  const previewNamespace = branchName
20
- ? `${namespace.split('.').slice(0, -1).join('.') || namespace}.${branchName
21
- .replace(/-/g, '_')
22
- .replace(/\//g, '_')}`
23
+ ? (() => {
24
+ const branchSuffix = branchName.replace(/-/g, '_').replace(/\//g, '_');
25
+ if (isGitRoot) {
26
+ // Git root: use full namespace as prefix
27
+ return `${namespace}.${branchSuffix}`;
28
+ } else {
29
+ // Branch namespace: remove last segment to get parent prefix
30
+ const parts = namespace.split('.');
31
+ const prefix =
32
+ parts.length > 1 ? parts.slice(0, -1).join('.') : namespace;
33
+ return `${prefix}.${branchSuffix}`;
34
+ }
35
+ })()
23
36
  : '';
24
37
 
25
38
  const handleSubmit = async e => {
@@ -2,6 +2,11 @@ import React, { useState, useEffect } from 'react';
2
2
 
3
3
  /**
4
4
  * Modal for configuring git settings for a namespace.
5
+ * Supports two modes:
6
+ * - Git Root: Configure repository and path only (no branch, no git_only flag)
7
+ * - Branch Namespace: Auto-calculates parent from namespace name, user sets branch and git_only
8
+ * (e.g., "demo.main" automatically has parent "demo")
9
+ * git_only determines if the branch is read-only (deployed from git) or editable (UI changes allowed)
5
10
  */
6
11
  export function GitSettingsModal({
7
12
  isOpen,
@@ -11,34 +16,55 @@ export function GitSettingsModal({
11
16
  currentConfig,
12
17
  namespace,
13
18
  }) {
19
+ const [mode, setMode] = useState('root'); // 'root' or 'branch'
14
20
  const [repoPath, setRepoPath] = useState('');
15
21
  const [branch, setBranch] = useState('');
16
22
  const [path, setPath] = useState('');
23
+ const [defaultBranch, setDefaultBranch] = useState('main');
17
24
  const [gitOnly, setGitOnly] = useState(true);
18
25
  const [saving, setSaving] = useState(false);
19
26
  const [removing, setRemoving] = useState(false);
20
27
  const [error, setError] = useState(null);
21
28
  const [success, setSuccess] = useState(false);
22
29
  const [wasRemoved, setWasRemoved] = useState(false);
30
+ const [parentConfig, setParentConfig] = useState(null);
31
+
32
+ // Auto-calculate parent namespace from current namespace
33
+ // e.g., "demo.main" -> "demo", "demo.metrics.feature1" -> "demo.metrics"
34
+ const parentNamespace = namespace?.includes('.')
35
+ ? namespace.substring(0, namespace.lastIndexOf('.'))
36
+ : '';
23
37
 
24
38
  useEffect(() => {
25
39
  if (currentConfig) {
40
+ // Determine mode based on whether parent_namespace is set
41
+ const isBranchMode = !!currentConfig.parent_namespace;
42
+ setMode(isBranchMode ? 'branch' : 'root');
43
+
26
44
  setRepoPath(currentConfig.github_repo_path || '');
27
45
  setBranch(currentConfig.git_branch || '');
28
46
  setPath(currentConfig.git_path || 'nodes/');
29
- // If git is already configured (has repo path), use the existing git_only value
30
- // Otherwise, default to read-only (true) for new git configurations
31
- const hasExistingGitConfig = !!currentConfig.github_repo_path;
32
- setGitOnly(hasExistingGitConfig ? currentConfig.git_only : true);
47
+ setDefaultBranch(currentConfig.default_branch || 'main');
48
+
49
+ // git_only is only relevant for branch namespaces
50
+ // Default to true (read-only) for new branch configs, use existing value if set
51
+ if (isBranchMode) {
52
+ setGitOnly(
53
+ currentConfig.git_only !== undefined ? currentConfig.git_only : true,
54
+ );
55
+ }
33
56
  } else {
34
- // New configuration - default to read-only and nodes/ path
57
+ // New configuration - default to git root mode and nodes/ path
58
+ setMode('root');
35
59
  setPath('nodes/');
60
+ // Default git_only to true for when user switches to branch mode
36
61
  setGitOnly(true);
37
62
  }
38
63
  // Don't reset success here - it gets reset when modal closes
39
64
  // Otherwise the success banner disappears when currentConfig updates after save
40
65
  setError(null);
41
66
  setWasRemoved(false);
67
+ setParentConfig(null);
42
68
  }, [currentConfig]);
43
69
 
44
70
  const handleSubmit = async e => {
@@ -46,15 +72,44 @@ export function GitSettingsModal({
46
72
  setError(null);
47
73
  setSuccess(false);
48
74
  setWasRemoved(false);
75
+
76
+ // Client-side validation
77
+ if (mode === 'branch') {
78
+ if (!parentNamespace) {
79
+ setError(
80
+ 'Cannot configure as branch namespace: namespace has no parent (no dots in name)',
81
+ );
82
+ return;
83
+ }
84
+ if (!branch.trim()) {
85
+ setError('Git branch is required for branch mode');
86
+ return;
87
+ }
88
+ } else {
89
+ // Git root mode
90
+ if (!repoPath.trim()) {
91
+ setError('Repository is required');
92
+ return;
93
+ }
94
+ }
95
+
49
96
  setSaving(true);
50
97
 
51
98
  try {
52
- const config = {
53
- github_repo_path: repoPath.trim() || null,
54
- git_branch: branch.trim() || null,
55
- git_path: path.trim() || null,
56
- git_only: gitOnly,
57
- };
99
+ const config =
100
+ mode === 'branch'
101
+ ? {
102
+ // Branch mode: only send branch, parent, and git_only
103
+ git_branch: branch.trim() || null,
104
+ parent_namespace: parentNamespace || null,
105
+ git_only: gitOnly,
106
+ }
107
+ : {
108
+ // Git root mode: only send repo, path, and default_branch (no git_branch, no git_only)
109
+ github_repo_path: repoPath.trim() || null,
110
+ git_path: path.trim() || null,
111
+ default_branch: defaultBranch.trim() || null,
112
+ };
58
113
 
59
114
  const result = await onSave(config);
60
115
  if (result?._error) {
@@ -71,6 +126,19 @@ export function GitSettingsModal({
71
126
  }
72
127
  };
73
128
 
129
+ // Fetch parent config when parent namespace changes (only if modal is open)
130
+ useEffect(() => {
131
+ if (isOpen && mode === 'branch' && parentNamespace) {
132
+ // Fetch parent's git config to show inherited values
133
+ fetch(`/api/namespaces/${parentNamespace}/git`)
134
+ .then(res => (res.ok ? res.json() : null))
135
+ .then(data => setParentConfig(data))
136
+ .catch(() => setParentConfig(null));
137
+ } else {
138
+ setParentConfig(null);
139
+ }
140
+ }, [isOpen, mode, parentNamespace]);
141
+
74
142
  const handleRemove = async () => {
75
143
  if (
76
144
  !window.confirm(
@@ -163,110 +231,306 @@ export function GitSettingsModal({
163
231
  </div>
164
232
  )}
165
233
 
166
- <div className="form-group">
167
- <label htmlFor="git-repo-path">Repository</label>
168
- <input
169
- id="git-repo-path"
170
- type="text"
171
- placeholder="owner/repo"
172
- value={repoPath}
173
- onChange={e => setRepoPath(e.target.value)}
174
- disabled={saving}
175
- />
176
- <span className="form-hint">
177
- GitHub repository path (e.g., "myorg/dj-definitions")
178
- </span>
179
- </div>
180
-
181
- <div className="form-group">
182
- <label htmlFor="git-branch">Branch</label>
183
- <input
184
- id="git-branch"
185
- type="text"
186
- placeholder="main"
187
- value={branch}
188
- onChange={e => setBranch(e.target.value)}
189
- disabled={saving}
190
- />
191
- <span className="form-hint">
192
- Git branch for this namespace (e.g., "main" or "production")
193
- </span>
194
- </div>
195
-
196
- <div className="form-group">
197
- <label htmlFor="git-path">Path</label>
198
- <input
199
- id="git-path"
200
- type="text"
201
- placeholder="nodes/"
202
- value={path}
203
- onChange={e => setPath(e.target.value)}
204
- disabled={saving}
205
- required
206
- />
207
- <span className="form-hint">
208
- Subdirectory within the repo for node YAML files
209
- </span>
210
- </div>
211
-
212
- <div
213
- style={{
214
- marginTop: '16px',
215
- padding: '12px',
216
- backgroundColor: gitOnly ? '#fef3c7' : '#f0fdf4',
217
- borderRadius: '6px',
218
- border: `1px solid ${gitOnly ? '#fcd34d' : '#86efac'}`,
219
- }}
220
- >
221
- <label
234
+ {/* Mode Toggle */}
235
+ <div style={{ marginBottom: '20px' }}>
236
+ <div
222
237
  style={{
223
238
  display: 'flex',
224
- alignItems: 'flex-start',
225
- gap: '10px',
226
- cursor: 'pointer',
227
- margin: 0,
228
- textTransform: 'none',
229
- letterSpacing: 'normal',
230
- fontSize: '14px',
231
- fontWeight: 'normal',
239
+ gap: '12px',
240
+ padding: '4px',
241
+ backgroundColor: '#f1f5f9',
242
+ borderRadius: '8px',
232
243
  }}
233
244
  >
234
- <input
235
- type="checkbox"
236
- checked={gitOnly}
237
- onChange={e => setGitOnly(e.target.checked)}
245
+ <button
246
+ type="button"
247
+ onClick={() => setMode('root')}
248
+ disabled={saving}
249
+ style={{
250
+ flex: 1,
251
+ padding: '10px 16px',
252
+ fontSize: '13px',
253
+ fontWeight: 500,
254
+ border: 'none',
255
+ borderRadius: '6px',
256
+ backgroundColor:
257
+ mode === 'root' ? '#ffffff' : 'transparent',
258
+ color: mode === 'root' ? '#0f172a' : '#64748b',
259
+ cursor: saving ? 'not-allowed' : 'pointer',
260
+ boxShadow:
261
+ mode === 'root' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
262
+ transition: 'all 0.15s',
263
+ }}
264
+ >
265
+ Git Root
266
+ </button>
267
+ <button
268
+ type="button"
269
+ onClick={() => setMode('branch')}
238
270
  disabled={saving}
239
- style={{ marginTop: '3px' }}
240
- />
241
- <span>
242
- <span
271
+ style={{
272
+ flex: 1,
273
+ padding: '10px 16px',
274
+ fontSize: '13px',
275
+ fontWeight: 500,
276
+ border: 'none',
277
+ borderRadius: '6px',
278
+ backgroundColor:
279
+ mode === 'branch' ? '#ffffff' : 'transparent',
280
+ color: mode === 'branch' ? '#0f172a' : '#64748b',
281
+ cursor: saving ? 'not-allowed' : 'pointer',
282
+ boxShadow:
283
+ mode === 'branch' ? '0 1px 3px rgba(0,0,0,0.1)' : 'none',
284
+ transition: 'all 0.15s',
285
+ }}
286
+ >
287
+ Branch Namespace
288
+ </button>
289
+ </div>
290
+ <span
291
+ style={{
292
+ display: 'block',
293
+ marginTop: '8px',
294
+ fontSize: '12px',
295
+ color: '#64748b',
296
+ }}
297
+ >
298
+ {mode === 'root'
299
+ ? 'Configure repository for this namespace (recommended for most users)'
300
+ : `Link to parent "${
301
+ parentNamespace || '(none)'
302
+ }" and inherit repository configuration`}
303
+ </span>
304
+ </div>
305
+
306
+ {/* Conditional Fields based on Mode */}
307
+ {mode === 'root' ? (
308
+ <>
309
+ {/* Git Root Mode - Repository and Path only */}
310
+ <div className="form-group">
311
+ <label htmlFor="git-repo-path">Repository *</label>
312
+ <input
313
+ id="git-repo-path"
314
+ type="text"
315
+ placeholder="owner/repo"
316
+ value={repoPath}
317
+ onChange={e => setRepoPath(e.target.value)}
318
+ disabled={saving}
319
+ required
320
+ />
321
+ <span className="form-hint">
322
+ GitHub repository path (e.g., "myorg/dj-definitions")
323
+ </span>
324
+ </div>
325
+
326
+ <div className="form-group">
327
+ <label htmlFor="git-path">Path</label>
328
+ <input
329
+ id="git-path"
330
+ type="text"
331
+ placeholder="nodes/"
332
+ value={path}
333
+ onChange={e => setPath(e.target.value)}
334
+ disabled={saving}
335
+ />
336
+ <span className="form-hint">
337
+ Subdirectory within the repo for node YAML files
338
+ </span>
339
+ </div>
340
+
341
+ <div className="form-group">
342
+ <label htmlFor="default-branch">Default Branch</label>
343
+ <input
344
+ id="default-branch"
345
+ type="text"
346
+ placeholder="main"
347
+ value={defaultBranch}
348
+ onChange={e => setDefaultBranch(e.target.value)}
349
+ disabled={saving}
350
+ />
351
+ <span className="form-hint">
352
+ Default branch to use when creating new branches (e.g.,
353
+ "main")
354
+ </span>
355
+ </div>
356
+ </>
357
+ ) : (
358
+ <>
359
+ {/* Branch Mode - Show parent and branch field */}
360
+ {!parentNamespace ? (
361
+ <div
243
362
  style={{
244
- fontWeight: 600,
245
- color: gitOnly ? '#92400e' : '#166534',
246
- textTransform: 'none',
363
+ marginBottom: '16px',
364
+ padding: '12px',
365
+ backgroundColor: '#fef2f2',
366
+ borderRadius: '6px',
367
+ border: '1px solid #fecaca',
368
+ color: '#dc2626',
369
+ fontSize: '13px',
247
370
  }}
248
371
  >
249
- {gitOnly
250
- ? 'Read-only (Git is source of truth)'
251
- : 'Editable (UI edits allowed)'}
372
+ Cannot configure as branch namespace: namespace "{namespace}
373
+ " has no parent. Branch namespaces must have a dot in their
374
+ name (e.g., "demo.main").
375
+ </div>
376
+ ) : (
377
+ <>
378
+ {/* Display parent namespace as read-only info */}
379
+ <div
380
+ style={{
381
+ marginBottom: '16px',
382
+ padding: '12px',
383
+ backgroundColor: '#f8fafc',
384
+ borderRadius: '6px',
385
+ border: '1px solid #e2e8f0',
386
+ }}
387
+ >
388
+ <div
389
+ style={{
390
+ fontSize: '12px',
391
+ fontWeight: 600,
392
+ color: '#475569',
393
+ marginBottom: '8px',
394
+ }}
395
+ >
396
+ Parent Namespace
397
+ </div>
398
+ <div
399
+ style={{
400
+ fontSize: '14px',
401
+ fontWeight: 500,
402
+ color: '#0f172a',
403
+ fontFamily: 'monospace',
404
+ }}
405
+ >
406
+ {parentNamespace}
407
+ </div>
408
+ <div
409
+ style={{
410
+ fontSize: '12px',
411
+ color: '#64748b',
412
+ marginTop: '6px',
413
+ }}
414
+ >
415
+ Repository configuration will be inherited from this
416
+ parent
417
+ </div>
418
+ </div>
419
+
420
+ {/* Show inherited config from parent */}
421
+ {parentConfig && (
422
+ <div
423
+ style={{
424
+ marginBottom: '16px',
425
+ padding: '12px',
426
+ backgroundColor: '#f0f9ff',
427
+ borderRadius: '6px',
428
+ border: '1px solid #bae6fd',
429
+ }}
430
+ >
431
+ <div
432
+ style={{
433
+ fontSize: '12px',
434
+ fontWeight: 600,
435
+ color: '#0369a1',
436
+ marginBottom: '8px',
437
+ }}
438
+ >
439
+ Inherited Configuration
440
+ </div>
441
+ <div style={{ fontSize: '12px', color: '#64748b' }}>
442
+ <div style={{ marginBottom: '4px' }}>
443
+ <strong>Repository:</strong>{' '}
444
+ {parentConfig.github_repo_path ||
445
+ '(not configured)'}
446
+ </div>
447
+ <div>
448
+ <strong>Path:</strong>{' '}
449
+ {parentConfig.git_path || '(root)'}
450
+ </div>
451
+ </div>
452
+ </div>
453
+ )}
454
+ </>
455
+ )}
456
+
457
+ <div className="form-group">
458
+ <label htmlFor="git-branch">Branch *</label>
459
+ <input
460
+ id="git-branch"
461
+ type="text"
462
+ placeholder="feature-x, dev, etc."
463
+ value={branch}
464
+ onChange={e => setBranch(e.target.value)}
465
+ disabled={saving}
466
+ required
467
+ />
468
+ <span className="form-hint">
469
+ Git branch name for this namespace
252
470
  </span>
253
- <span
471
+ </div>
472
+
473
+ {/* Git-only checkbox - only for branch namespaces */}
474
+ <div
475
+ style={{
476
+ marginTop: '16px',
477
+ padding: '12px',
478
+ backgroundColor: gitOnly ? '#fef3c7' : '#f0fdf4',
479
+ borderRadius: '6px',
480
+ border: `1px solid ${gitOnly ? '#fcd34d' : '#86efac'}`,
481
+ }}
482
+ >
483
+ <label
254
484
  style={{
255
- display: 'block',
256
- marginTop: '4px',
257
- fontSize: '12px',
258
- color: '#64748b',
259
- fontWeight: 'normal',
485
+ display: 'flex',
486
+ alignItems: 'flex-start',
487
+ gap: '10px',
488
+ cursor: 'pointer',
489
+ margin: 0,
260
490
  textTransform: 'none',
491
+ letterSpacing: 'normal',
492
+ fontSize: '14px',
493
+ fontWeight: 'normal',
261
494
  }}
262
495
  >
263
- {gitOnly
264
- ? 'Changes must be made via git and deployed through CI/CD. UI editing is disabled.'
265
- : 'Users can edit nodes in the UI. Changes can be synced to git.'}
266
- </span>
267
- </span>
268
- </label>
269
- </div>
496
+ <input
497
+ type="checkbox"
498
+ checked={gitOnly}
499
+ onChange={e => setGitOnly(e.target.checked)}
500
+ disabled={saving}
501
+ style={{ marginTop: '3px' }}
502
+ />
503
+ <span>
504
+ <span
505
+ style={{
506
+ fontWeight: 600,
507
+ color: gitOnly ? '#92400e' : '#166534',
508
+ textTransform: 'none',
509
+ }}
510
+ >
511
+ {gitOnly
512
+ ? 'Read-only (Git is source of truth)'
513
+ : 'Editable (UI edits allowed)'}
514
+ </span>
515
+ <span
516
+ style={{
517
+ display: 'block',
518
+ marginTop: '4px',
519
+ fontSize: '12px',
520
+ color: '#64748b',
521
+ fontWeight: 'normal',
522
+ textTransform: 'none',
523
+ }}
524
+ >
525
+ {gitOnly
526
+ ? 'Changes must be made via git and deployed through CI/CD. UI editing is disabled.'
527
+ : 'Users can edit nodes in the UI. Changes can be synced to git.'}
528
+ </span>
529
+ </span>
530
+ </label>
531
+ </div>
532
+ </>
533
+ )}
270
534
 
271
535
  {success && (
272
536
  <div