Datavision / frontend /src /components /ui /ConnectionSetupModal.tsx
DataVision CI/CD Bot
release: clean production build for HuggingFace Space
1e2158c
Raw
History Blame Contribute Delete
18.1 kB
import React, { useState } from 'react';
import { motion } from 'framer-motion';
import { X, Database, Server, User, Copy, Check, Zap, Terminal, Loader2 } from 'lucide-react';
import { useToast } from '@/contexts/ToastContext';
import { useUserStore } from '@/store/userStore';
import { api } from '@/services/api';
import { getAuthHeadersSync, getUserIdSync } from '@/utils/userId';
interface Props {
source: string; // 'PostgreSQL' | 'Snowflake' | 'Kafka'
onClose: () => void;
onConnect: (connectionId: string) => void;
}
// Generate the Python script dynamically based on user inputs and connector type
const generateScript = (source: string, pushUrl: string, host: string, dbName: string, tableName: string, username: string, schema: string = 'PUBLIC'): string => {
const sourceLower = source.toLowerCase();
if (sourceLower === 'postgresql') {
return `import json, psycopg2, requests, time
from psycopg2.extras import RealDictCursor
# Your unique DataVision Cloud Push URL (do NOT share this)
URL = "${pushUrl}"
# Connect to your local PostgreSQL database
conn = psycopg2.connect(
dbname="${dbName}",
user="${username}",
password="YOUR_PASSWORD", # <-- Enter your password here
host="${host}",
port="5432"
)
conn.autocommit = True
cursor = conn.cursor(cursor_factory=RealDictCursor)
print("Connected to ${dbName}! Streaming '${tableName}' to DataVision Cloud...")
previous_count = 0
while True:
try:
# First run imports the existing table; later polls send the table again.
# DataVision de-duplicates identical records before writing its live CSV.
cursor.execute("SELECT * FROM ${tableName};")
records = json.loads(json.dumps([dict(row) for row in cursor.fetchall()], default=str))
total_rows = len(records)
rows_per_sec = max(0, total_rows - previous_count) if previous_count else 0
previous_count = total_rows
res = requests.post(URL, json={
"total_rows": total_rows,
"rows_per_sec": rows_per_sec,
"data": records,
"cpu_usage": 0.0,
"error_rate": 0.0,
"status": "Streaming ${tableName} to Cloud"
})
print(f"Sent: {total_rows} rows -> {res.json()}")
except Exception as e:
print("Error:", e)
time.sleep(5)
`;
}
if (sourceLower === 'snowflake') {
return `import json, snowflake.connector, requests, time
# Your unique DataVision Cloud Push URL (do NOT share this)
URL = "${pushUrl}"
# Connect to your Snowflake warehouse
conn = snowflake.connector.connect(
user="${username}",
password="YOUR_PASSWORD", # <-- Enter your password here
account="${host}", # e.g. xy12345.us-east-1
warehouse="COMPUTE_WH",
database="${dbName}",
schema="${schema || 'PUBLIC'}"
)
cursor = conn.cursor()
print("Connected to Snowflake! Streaming '${tableName}' from schema '${schema || 'PUBLIC'}' to DataVision Cloud...")
previous_count = 0
while True:
try:
cursor.execute("SELECT * FROM ${schema ? `${schema}.${tableName}` : tableName}")
columns = [item[0] for item in cursor.description]
records = json.loads(json.dumps([dict(zip(columns, row)) for row in cursor.fetchall()], default=str))
total_rows = len(records)
rows_per_sec = max(0, total_rows - previous_count) if previous_count else 0
previous_count = total_rows
res = requests.post(URL, json={
"total_rows": total_rows,
"rows_per_sec": rows_per_sec,
"data": records,
"cpu_usage": 0.0,
"error_rate": 0.0,
"status": "Streaming ${tableName} from Snowflake"
})
print(f"Sent: {total_rows} rows -> {res.json()}")
except Exception as e:
print("Error:", e)
time.sleep(5)
`;
}
// Kafka
return `from confluent_kafka import Consumer
import json, requests
# Your unique DataVision Cloud Push URL (do NOT share this)
URL = "${pushUrl}"
c = Consumer({
"bootstrap.servers": "${host}",
"group.id": "datavision-push-group",
"auto.offset.reset": "latest"
})
c.subscribe(["${tableName}"]) # Kafka Topic Name
print("Listening to Kafka topic '${tableName}' and streaming to DataVision Cloud...")
sent_messages = 0
while True:
msg = c.poll(1.0)
if msg is None:
continue
if msg.error():
print("Consumer error:", msg.error())
continue
# Send the actual event. DataVision stores it in the live CSV/Data Hub.
try:
event = json.loads(msg.value().decode("utf-8"))
if not isinstance(event, dict):
event = {"value": event}
except Exception:
event = {"value": msg.value().decode("utf-8", errors="replace")}
try:
res = requests.post(URL, json={
"data": [event],
"rows_per_sec": 1,
"cpu_usage": 0.0,
"error_rate": 0.0,
"status": "Receiving Kafka messages"
}, timeout=15)
res.raise_for_status()
sent_messages += 1 # Count only records DataVision accepted.
print(f"Pushed message #{sent_messages} -> {res.json()}")
except requests.RequestException as exc:
print(f"Push failed; event was not counted: {exc}")
`;
};
export const ConnectionSetupModal: React.FC<Props> = ({ source, onClose, onConnect }) => {
const { isDark } = useUserStore();
const toast = useToast();
// Step 1: collect details. Step 2: show generated script. Step 3: launch dashboard.
const [step, setStep] = useState<1 | 2 | 3>(1);
const [isGenerating, setIsGenerating] = useState(false);
const [copied, setCopied] = useState(false);
// Form fields
const [host, setHost] = useState('');
const [databaseName, setDatabaseName] = useState('');
const [targetTable, setTargetTable] = useState('');
const [username, setUsername] = useState('');
const [schema, setSchema] = useState('PUBLIC');
// Generated values
const [connectionId, setConnectionId] = useState('');
const [pushUrl, setPushUrl] = useState('');
const sourceIcon = source === 'PostgreSQL' ? '🐘' : source === 'Snowflake' ? '❄️' : '⚑';
const sourceColor = source === 'PostgreSQL' ? 'indigo' : source === 'Snowflake' ? 'blue' : 'yellow';
const placeholders: Record<string, { host: string; db: string; table: string; user: string; schema?: string }> = {
PostgreSQL: { host: 'localhost', db: 'streaming_db', table: 'weather_data', user: 'postgres' },
Snowflake: { host: 'xy12345.us-east-1', db: 'PRODUCTION', table: 'SALES_DATA', user: 'admin', schema: 'PUBLIC' },
Kafka: { host: 'localhost:9092', db: '(not needed for Kafka)', table: 'my_topic', user: '(optional)' },
};
const ph = placeholders[source] || placeholders.PostgreSQL;
const handleGenerate = async () => {
if (!host || !targetTable) {
toast.error('Host and Table/Topic are required.');
return;
}
setIsGenerating(true);
try {
const response = await fetch('/api/v1/connections', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...getAuthHeadersSync() },
body: JSON.stringify({
source_type: `api_push_${source.toLowerCase()}`,
host,
database_name: databaseName || (source === 'Snowflake' ? 'PRODUCTION' : 'push'),
target_table: targetTable,
credentials: 'none'
})
});
const data = await response.json();
const connId = data.connection_id;
setConnectionId(connId);
// Persist guest connections in localStorage
if (data.is_guest && data.connection) {
const stored = JSON.parse(localStorage.getItem('guest_live_connections') || '[]');
stored.unshift({ ...data.connection, source_type: `api_push_${source.toLowerCase()}` });
localStorage.setItem('guest_live_connections', JSON.stringify(stored));
}
const url = `${window.location.protocol}//${window.location.host}/api/v1/push/${connId}`;
setPushUrl(url);
setStep(2);
} catch (err: any) {
toast.error('Failed to generate connection: ' + (err?.message || 'Unknown error'));
} finally {
setIsGenerating(false);
}
};
const handleCopy = () => {
const script = generateScript(source, pushUrl, host, databaseName, targetTable, username, schema);
navigator.clipboard.writeText(script);
setCopied(true);
toast.success('Script copied to clipboard!');
setTimeout(() => setCopied(false), 2000);
};
const handleLaunchDashboard = () => {
setStep(3);
setTimeout(() => {
onConnect(connectionId);
}, 1200);
};
const inputClasses = `w-full border rounded-lg px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-${sourceColor}-500/50 focus:border-${sourceColor}-500 transition-all ${isDark ? 'bg-black/40 border-gray-700 text-white placeholder-gray-500' : 'bg-white border-gray-300 text-gray-900 placeholder-gray-400'}`;
return (
<div className={`fixed inset-0 z-[60] flex items-center justify-center p-4 backdrop-blur-md ${isDark ? 'bg-black/70' : 'bg-white/40'}`}>
<motion.div
initial={{ opacity: 0, scale: 0.95, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
className={`w-full ${step === 2 ? 'max-w-2xl' : 'max-w-md'} border rounded-2xl overflow-hidden shadow-2xl flex flex-col transition-all ${isDark ? 'bg-[#0a0a0a] border-gray-800' : 'bg-white border-gray-200'}`}
>
{/* Header */}
<div className={`p-4 border-b flex items-center justify-between ${isDark ? 'bg-gradient-to-r from-[#111] to-[#0d0d0d] border-gray-800' : 'bg-gray-50 border-gray-200'}`}>
<div className="flex items-center gap-3">
<div className="text-2xl">{sourceIcon}</div>
<div>
<h2 className={`font-bold ${isDark ? 'text-white' : 'text-gray-900'}`}>
{step === 1 ? `Connect ${source}` : step === 2 ? 'Your Streaming Client' : 'Launching...'}
</h2>
<p className={`text-xs ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
{step === 1 ? 'Enter your database details below' : step === 2 ? 'Copy this script and run it on your machine' : 'Opening live dashboard...'}
</p>
</div>
</div>
<button onClick={onClose} className={`p-2 rounded-lg transition-colors ${isDark ? 'hover:bg-white/10 text-gray-400 hover:text-white' : 'hover:bg-gray-200 text-gray-500 hover:text-gray-900'}`}>
<X className="w-5 h-5" />
</button>
</div>
{/* Body */}
<div className="p-6">
{/* ─── STEP 1: Collect Details ─── */}
{step === 1 && (
<div className="space-y-4">
<div className="space-y-1.5">
<label className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
<Server className="w-4 h-4" /> {source === 'Snowflake' ? 'Account Identifier' : 'Host'}
</label>
<input type="text" value={host} onChange={e => setHost(e.target.value)} placeholder={`e.g. ${ph.host}`} className={inputClasses} />
</div>
{source !== 'Kafka' && (
<div className="space-y-1.5">
<label className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
<Database className="w-4 h-4" /> Database Name
</label>
<input type="text" value={databaseName} onChange={e => setDatabaseName(e.target.value)} placeholder={`e.g. ${ph.db}`} className={inputClasses} />
</div>
)}
{source === 'Snowflake' && (
<div className="space-y-1.5">
<label className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
<Database className="w-4 h-4" /> Schema Name
</label>
<input type="text" value={schema} onChange={e => setSchema(e.target.value)} placeholder="e.g. PUBLIC" className={inputClasses} />
</div>
)}
<div className="space-y-1.5">
<label className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
<Database className="w-4 h-4" /> {source === 'Kafka' ? 'Topic Name' : 'Table Name'}
</label>
<input type="text" value={targetTable} onChange={e => setTargetTable(e.target.value)} placeholder={`e.g. ${ph.table}`} className={inputClasses} />
</div>
<div className="space-y-1.5">
<label className={`text-sm font-medium flex items-center gap-2 ${isDark ? 'text-gray-300' : 'text-gray-700'}`}>
<User className="w-4 h-4" /> Username
</label>
<input type="text" value={username} onChange={e => setUsername(e.target.value)} placeholder={`e.g. ${ph.user}`} className={inputClasses} />
</div>
<div className={`p-3 rounded-lg text-xs ${isDark ? 'bg-yellow-500/10 text-yellow-300 border border-yellow-500/20' : 'bg-yellow-50 text-yellow-700 border border-yellow-200'}`}>
<strong>How it works:</strong> We generate a Python script with your details pre-filled. You run it on your machine β€” it reads your database locally and securely streams data to DataVision Cloud. Your credentials never leave your machine.
</div>
<button
onClick={handleGenerate}
disabled={isGenerating}
className={`w-full mt-2 bg-${sourceColor}-600 hover:bg-${sourceColor}-700 text-white font-semibold py-3 rounded-xl transition-all flex items-center justify-center gap-2 disabled:opacity-70`}
style={{ backgroundColor: source === 'PostgreSQL' ? '#4f46e5' : source === 'Snowflake' ? '#3b82f6' : '#eab308' }}
>
{isGenerating ? (
<><Loader2 className="w-5 h-5 animate-spin" /> Generating Secure Push URL...</>
) : (
<><Terminal className="w-5 h-5" /> Generate Streaming Client</>
)}
</button>
</div>
)}
{/* ─── STEP 2: Show Generated Script ─── */}
{step === 2 && (
<div className="space-y-4">
<div className={`flex items-center justify-between p-3 rounded-lg ${isDark ? 'bg-green-500/10 border border-green-500/20' : 'bg-green-50 border border-green-200'}`}>
<div className="flex items-center gap-2 text-green-400 text-sm font-semibold">
<Check className="w-4 h-4" /> Push URL generated successfully!
</div>
<button
onClick={handleCopy}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-bold transition-all ${copied ? 'bg-green-500 text-white' : isDark ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-gray-200 text-gray-800 hover:bg-gray-300'}`}
>
{copied ? <><Check className="w-3.5 h-3.5" /> Copied!</> : <><Copy className="w-3.5 h-3.5" /> Copy Script</>}
</button>
</div>
<div className={`rounded-xl border overflow-hidden ${isDark ? 'border-gray-800' : 'border-gray-200'}`}>
<div className={`px-4 py-2 text-xs font-mono flex items-center gap-2 ${isDark ? 'bg-[#1a1a1a] text-gray-400 border-b border-gray-800' : 'bg-gray-100 text-gray-600 border-b border-gray-200'}`}>
<Terminal className="w-3.5 h-3.5" /> datavision_{source.toLowerCase()}_push.py
</div>
<pre className={`text-xs p-4 overflow-x-auto whitespace-pre-wrap max-h-[340px] overflow-y-auto font-mono leading-relaxed ${isDark ? 'bg-[#0d0d0d] text-green-300' : 'bg-gray-50 text-gray-800'}`}>
{generateScript(source, pushUrl, host, databaseName, targetTable, username)}
</pre>
</div>
<div className={`text-xs space-y-1 ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
<p><strong>Step 1:</strong> Copy the script above.</p>
<p><strong>Step 2:</strong> Replace <code className="text-yellow-400">YOUR_PASSWORD</code> with your actual password.</p>
<p><strong>Step 3:</strong> Run <code className="text-blue-400">python datavision_{source.toLowerCase()}_push.py</code> on your machine.</p>
<p><strong>Step 4:</strong> Click the button below to open your Live Dashboard.</p>
</div>
<button
onClick={handleLaunchDashboard}
className="w-full bg-green-600 hover:bg-green-700 text-white font-semibold py-3 rounded-xl transition-all flex items-center justify-center gap-2"
>
<Zap className="w-5 h-5" /> Launch Live Dashboard
</button>
</div>
)}
{/* ─── STEP 3: Launching animation ─── */}
{step === 3 && (
<div className="flex flex-col items-center justify-center py-10 space-y-4">
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
className="w-20 h-20 bg-green-500/20 rounded-full flex items-center justify-center"
>
<Zap className="w-10 h-10 text-green-500" />
</motion.div>
<h3 className={`text-xl font-bold text-center ${isDark ? 'text-white' : 'text-gray-900'}`}>Launching Live Dashboard</h3>
<p className={`text-center text-sm ${isDark ? 'text-gray-400' : 'text-gray-500'}`}>
Waiting for data from your {source} streaming client...
</p>
</div>
)}
</div>
</motion.div>
</div>
);
};