|
| 1 | +import React, { useState, useEffect } from "react"; |
| 2 | +import api from "../../utils/api"; |
| 3 | +import toast from "react-hot-toast"; |
| 4 | +import { Database, CheckCircle, Server, Copy } from "lucide-react"; |
| 5 | +import ConfirmationModal from "../../pages/ConfirmationModal"; |
| 6 | +import { SettingsCard } from "./formPrimitives"; |
| 7 | +import { inputStyle } from "../../utils/styles"; |
| 8 | + |
| 9 | +export default function DatabaseConfigForm({ project, projectId, onProjectUpdate, role }) { |
| 10 | + const isViewer = role === 'viewer'; |
| 11 | + const [dbUri, setDbUri] = useState(""); |
| 12 | + const [loading, setLoading] = useState(false); |
| 13 | + const isConfigured = project?.resources?.db?.isExternal || false; |
| 14 | + const [showForm, setShowForm] = useState(!isConfigured); |
| 15 | + const [showRemoveModal, setShowRemoveModal] = useState(false); |
| 16 | + const [serverIp, setServerIp] = useState(null); |
| 17 | + |
| 18 | + useEffect(() => { |
| 19 | + let isMounted = true; |
| 20 | + Promise.resolve().then(() => setShowForm(!(project?.resources?.db?.isExternal || false))); |
| 21 | + const fetchIp = async () => { |
| 22 | + try { |
| 23 | + const res = await api.get(`/api/server-ip`); |
| 24 | + if (isMounted) setServerIp(res.data.ip); |
| 25 | + } |
| 26 | + catch (e) { console.error("Failed to fetch server IP", e); } |
| 27 | + }; |
| 28 | + fetchIp(); |
| 29 | + return () => { isMounted = false; }; |
| 30 | + }, [project]); |
| 31 | + |
| 32 | + const copyIp = async () => { |
| 33 | + if (serverIp && navigator?.clipboard) { |
| 34 | + try { |
| 35 | + await navigator.clipboard.writeText(serverIp); |
| 36 | + toast.success("Server IP copied!"); |
| 37 | + } catch { |
| 38 | + toast.error("Failed to copy server IP"); |
| 39 | + } |
| 40 | + } |
| 41 | + }; |
| 42 | + |
| 43 | + const handleUpdate = async () => { |
| 44 | + if (!dbUri) return toast.error("Database URI is required"); |
| 45 | + setLoading(true); |
| 46 | + try { |
| 47 | + await api.patch(`/api/projects/${projectId}/byod-config`, { dbUri }); |
| 48 | + toast.success("Database configuration updated!"); |
| 49 | + setShowForm(false); |
| 50 | + setDbUri(""); |
| 51 | + } catch (err) { |
| 52 | + const errorMsg = err.response?.data?.message || err.response?.data?.error || "Failed to update DB config"; |
| 53 | + if (errorMsg.includes("whitelist Server IP")) { |
| 54 | + toast.error(<div><b>Access Denied!</b><br />{errorMsg}</div>, { duration: 6000 }); |
| 55 | + } else { |
| 56 | + toast.error(errorMsg); |
| 57 | + } |
| 58 | + } finally { |
| 59 | + setLoading(false); |
| 60 | + } |
| 61 | + }; |
| 62 | + |
| 63 | + const executeRemove = async () => { |
| 64 | + try { |
| 65 | + await api.delete(`/api/projects/${projectId}/byod-config/db`, { data: { projectId } }); |
| 66 | + toast.success("External database configuration removed!"); |
| 67 | + onProjectUpdate(prev => ({ ...prev, resources: { ...prev.resources, db: { ...prev.resources.db, isExternal: false } } })); |
| 68 | + setShowForm(true); |
| 69 | + } catch (err) { |
| 70 | + toast.error(err.response?.data?.message || err.response?.data?.error || "Failed to remove DB config"); |
| 71 | + } finally { |
| 72 | + setLoading(false); |
| 73 | + } |
| 74 | + }; |
| 75 | + |
| 76 | + return ( |
| 77 | + <SettingsCard title="Database (MongoDB)" icon={Database} iconColor="var(--color-primary)" accentColor="var(--color-primary)"> |
| 78 | + {showRemoveModal && ( |
| 79 | + <ConfirmationModal |
| 80 | + open={showRemoveModal} |
| 81 | + title="Remove Database Config" |
| 82 | + message="Remove the external database configuration? This will switch back to the internal database." |
| 83 | + onConfirm={() => { executeRemove(); setShowRemoveModal(false); }} |
| 84 | + onCancel={() => setShowRemoveModal(false)} |
| 85 | + /> |
| 86 | + )} |
| 87 | + <p style={{ fontSize: '0.75rem', color: 'var(--color-text-muted)', marginBottom: '0.75rem' }}>Connect your own MongoDB cluster for full data ownership.</p> |
| 88 | + |
| 89 | + {isConfigured && !showForm ? ( |
| 90 | + <div style={{ background: 'rgba(16,185,129,0.08)', padding: '10px 12px', borderRadius: '6px', border: '1px solid rgba(16,185,129,0.2)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> |
| 91 | + <div style={{ display: 'flex', alignItems: 'center', gap: '7px', color: '#10B981', fontWeight: 600, fontSize: '0.78rem' }}> |
| 92 | + <CheckCircle size={13} /> Connected to external MongoDB |
| 93 | + </div> |
| 94 | + {!isViewer && ( |
| 95 | + <div style={{ display: 'flex', gap: '6px' }}> |
| 96 | + <button className="btn btn-secondary" onClick={() => setShowForm(true)} style={{ height: '26px', fontSize: '0.7rem', padding: '0 10px' }}>Update URI</button> |
| 97 | + <button className="btn btn-danger" onClick={() => setShowRemoveModal(true)} disabled={loading} style={{ height: '26px', fontSize: '0.7rem', padding: '0 10px' }}> |
| 98 | + {loading ? "Removing..." : "Remove"} |
| 99 | + </button> |
| 100 | + </div> |
| 101 | + )} |
| 102 | + </div> |
| 103 | + ) : ( |
| 104 | + <div> |
| 105 | + <div className="form-group" style={{ marginBottom: '10px' }}> |
| 106 | + <label style={{ display: 'block', marginBottom: '5px', fontSize: '0.75rem', color: 'var(--color-text-muted)' }}>MongoDB Connection URI</label> |
| 107 | + <input |
| 108 | + type="password" |
| 109 | + className="input-field" |
| 110 | + placeholder="mongodb+srv://user:pass@cluster.mongodb.net/..." |
| 111 | + value={dbUri} |
| 112 | + onChange={(e) => setDbUri(e.target.value)} |
| 113 | + style={{ ...inputStyle, fontFamily: 'monospace' }} |
| 114 | + disabled={isViewer} |
| 115 | + /> |
| 116 | + </div> |
| 117 | + <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', paddingTop: '10px', borderTop: '1px solid var(--color-border)' }}> |
| 118 | + <div style={{ display: 'flex', alignItems: 'center', gap: '6px', fontSize: '0.7rem', color: 'var(--color-text-muted)' }}> |
| 119 | + <Server size={11} /> |
| 120 | + <span>Public IP: <code style={{ background: 'rgba(255,255,255,0.08)', padding: '1px 5px', borderRadius: '3px', fontSize: '0.68rem' }}>{serverIp || "..."}</code></span> |
| 121 | + {serverIp && ( |
| 122 | + <button onClick={copyIp} aria-label="Copy server IP" style={{ border: 'none', background: 'transparent', cursor: 'pointer', color: 'var(--color-primary)', padding: 0 }}> |
| 123 | + <Copy size={11} /> |
| 124 | + </button> |
| 125 | + )} |
| 126 | + </div> |
| 127 | + {!isViewer && ( |
| 128 | + <div style={{ display: 'flex', gap: '8px' }}> |
| 129 | + {isConfigured && <button className="btn btn-secondary" onClick={() => setShowForm(false)} style={{ height: '28px', fontSize: '0.72rem', padding: '0 10px' }}>Cancel</button>} |
| 130 | + <button onClick={handleUpdate} className="btn btn-primary" disabled={loading} style={{ height: '28px', fontSize: '0.72rem', padding: '0 12px' }}> |
| 131 | + {loading ? "Connecting..." : "Connect Database"} |
| 132 | + </button> |
| 133 | + </div> |
| 134 | + )} |
| 135 | + </div> |
| 136 | + </div> |
| 137 | + )} |
| 138 | + </SettingsCard> |
| 139 | + ); |
| 140 | +} |
0 commit comments