datajunction-ui
Version:
DataJunction UI
291 lines (273 loc) • 9.15 kB
JSX
import React, { useState, useEffect } from 'react';
import { detectShape, buildGitConfigPayload } from './gitShape';
import SplitFilter from '../SplitFilter';
/**
* Modal for configuring git settings for a namespace.
* Intent-based: user picks "Edited in DataJunction" or "Git repo",
* then (if git) picks "Tracks a branch" (flat) or "Feature branches + PRs" (root).
* No git_only checkbox — git-backed namespaces are always read-only here.
*/
export function GitSettingsModal({
isOpen,
onClose,
onSave,
onRemove,
currentConfig,
namespace,
nodeCount = 0,
}) {
const [source, setSource] = useState('dj'); // 'dj' | 'git'
const [model, setModel] = useState('flat'); // 'flat' | 'root'
const [repository, setRepository] = useState('');
const [branch, setBranch] = useState('main');
const [defaultBranch, setDefaultBranch] = useState('main');
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const [success, setSuccess] = useState(false);
useEffect(() => {
if (!isOpen) return;
const shape = detectShape(currentConfig || {});
setSource(shape === 'not-git' ? 'dj' : 'git');
setModel(shape === 'root' ? 'root' : 'flat');
setRepository(currentConfig?.github_repo_path || '');
setBranch(currentConfig?.git_branch || 'main');
setDefaultBranch(currentConfig?.default_branch || 'main');
setError(null);
}, [currentConfig, isOpen]);
const handleSubmit = async e => {
e.preventDefault();
setError(null);
setSuccess(false);
if (source === 'git' && !repository.trim()) {
setError('Repository is required');
return;
}
setSaving(true);
try {
const payload = buildGitConfigPayload({
source,
model,
repository,
branch,
defaultBranch,
});
const result =
payload === null ? await onRemove() : await onSave(payload);
if (result?._error) setError(result.message);
else setSuccess(true);
} catch (err) {
setError(err.message || 'Failed to save git settings');
} finally {
setSaving(false);
}
};
const handleClose = () => {
setError(null);
setSuccess(false);
onClose();
};
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={handleClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3>Git Configuration</h3>
<button
className="btn-close-modal"
onClick={handleClose}
title="Close"
>
×
</button>
</div>
<form onSubmit={handleSubmit}>
<div
className="modal-body"
style={{
fontFamily:
'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif',
textTransform: 'none',
}}
>
<p
style={{
color: '#64748b',
fontSize: '13px',
marginBottom: '16px',
}}
>
Configure git integration for <strong>{namespace}</strong>.
</p>
{error && (
<div
style={{
padding: '12px',
backgroundColor: '#fef2f2',
border: '1px solid #fecaca',
borderRadius: '6px',
color: '#dc2626',
fontSize: '13px',
marginBottom: '16px',
}}
>
{error}
</div>
)}
{/* Q1: Source of definitions */}
<div
style={{
display: 'flex',
textTransform: 'none',
marginBottom: '16px',
}}
>
<SplitFilter
label="Source of definitions"
value={source}
onChange={v => v && setSource(v)}
options={[
{ value: 'dj', label: 'Edited in DataJunction' },
{ value: 'git', label: 'Git repo' },
]}
/>
</div>
{source === 'git' && (
<>
{detectShape(currentConfig || {}) === 'not-git' &&
nodeCount > 0 && (
<div className="git-repo-wins-warning" role="alert">
⚠️ {nodeCount} nodes here aren't in the repo and will be
replaced by the repo's contents on the next deploy. Git
becomes the source of truth for this namespace.
</div>
)}
{/* Q2: Git model */}
<div
style={{
display: 'flex',
textTransform: 'none',
marginBottom: '16px',
}}
>
<SplitFilter
label="Git model"
value={model}
onChange={v => v && setModel(v)}
options={[
{ value: 'flat', label: 'Tracks a branch' },
{ value: 'root', label: 'Feature branches + PRs' },
]}
/>
</div>
<div className="form-group">
<label htmlFor="git-repo-path">Repository</label>
<input
id="git-repo-path"
type="text"
placeholder="owner/repo"
value={repository}
onChange={e => setRepository(e.target.value)}
disabled={saving}
/>
</div>
{model === 'flat' ? (
<div className="form-group">
<label htmlFor="git-branch">Branch</label>
<input
id="git-branch"
type="text"
placeholder="main"
value={branch}
onChange={e => setBranch(e.target.value)}
disabled={saving}
/>
</div>
) : (
<div className="form-group">
<label htmlFor="git-default-branch">Default branch</label>
<input
id="git-default-branch"
type="text"
placeholder="main"
value={defaultBranch}
onChange={e => setDefaultBranch(e.target.value)}
disabled={saving}
/>
</div>
)}
<p className="git-readonly-note">
🔒 Git-backed namespaces are read-only here; edit via git +
CI.
</p>
</>
)}
{success && (
<div
style={{
marginTop: '16px',
padding: '12px',
backgroundColor: '#f0fdf4',
border: '1px solid #86efac',
borderRadius: '6px',
color: '#166534',
fontSize: '13px',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
Git configuration saved successfully!
</div>
)}
</div>
<div
className="modal-actions"
style={{ display: 'flex', justifyContent: 'flex-end', gap: '8px' }}
>
<button
type="button"
className="btn-secondary"
onClick={handleClose}
disabled={saving}
>
{success ? 'Close' : 'Cancel'}
</button>
{!success && (
<button
type="submit"
className="btn-primary"
disabled={saving}
style={
saving
? {
opacity: 0.7,
cursor: 'wait',
backgroundColor: '#9ca3af',
}
: {}
}
>
{saving ? 'Saving...' : 'Save'}
</button>
)}
</div>
</form>
</div>
</div>
);
}
export default GitSettingsModal;