feat: add TableList component with styles and functionality for displaying database tables
feat: implement NewConnectionDialog component for creating and editing database connections with form validation chore: generate TypeScript definitions and JavaScript bindings for app functions chore: add models for configuration, connection requests, and database entities
This commit is contained in:
154
frontend/src/components/common/NewConnectionDialog.css
Normal file
154
frontend/src/components/common/NewConnectionDialog.css
Normal file
@@ -0,0 +1,154 @@
|
||||
/* NewConnectionDialog Styles */
|
||||
|
||||
.dialog-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.dialog-box {
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
width: 480px;
|
||||
max-width: 95vw;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-5) var(--space-6);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dialog-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
font-size: var(--text-base);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.dialog-form {
|
||||
padding: var(--space-5) var(--space-6);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
/* Type selector */
|
||||
.type-selector {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.type-btn {
|
||||
flex: 1;
|
||||
padding: var(--space-3) var(--space-2);
|
||||
font-size: var(--text-sm);
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: all var(--transition-fast) var(--ease-in-out);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.type-btn:hover {
|
||||
color: var(--text-primary);
|
||||
border-color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.type-btn.active {
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
background: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
/* Form groups */
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.form-group.flex-1 {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-group.flex-3 {
|
||||
flex: 3;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.form-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
font-family: var(--font-sans);
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-md);
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast) var(--ease-in-out);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Error */
|
||||
.form-error {
|
||||
padding: var(--space-3);
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border: 1px solid rgba(239, 68, 68, 0.4);
|
||||
border-radius: var(--radius-md);
|
||||
font-size: var(--text-sm);
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-3);
|
||||
padding-top: var(--space-2);
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
210
frontend/src/components/common/NewConnectionDialog.tsx
Normal file
210
frontend/src/components/common/NewConnectionDialog.tsx
Normal file
@@ -0,0 +1,210 @@
|
||||
/**
|
||||
* NewConnectionDialog Component
|
||||
* Modal form for creating a new database connection.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import './NewConnectionDialog.css';
|
||||
|
||||
export interface NewConnectionFormData {
|
||||
name: string;
|
||||
type: 'mysql' | 'postgres' | 'sqlite';
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
database: string;
|
||||
ssl_mode: string;
|
||||
timeout: number;
|
||||
}
|
||||
|
||||
interface NewConnectionDialogProps {
|
||||
onConfirm: (data: NewConnectionFormData) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
initialData?: Partial<NewConnectionFormData> & { id?: string };
|
||||
mode?: 'create' | 'edit';
|
||||
}
|
||||
|
||||
const DEFAULT_PORTS: Record<string, number> = {
|
||||
mysql: 3306,
|
||||
postgres: 5432,
|
||||
sqlite: 0,
|
||||
};
|
||||
|
||||
const NewConnectionDialog: React.FC<NewConnectionDialogProps> = ({ onConfirm, onCancel, initialData, mode = 'create' }) => {
|
||||
const isEditMode = mode === 'edit';
|
||||
const [form, setForm] = useState<NewConnectionFormData>({
|
||||
name: initialData?.name ?? '',
|
||||
type: initialData?.type ?? 'postgres',
|
||||
host: initialData?.host ?? '',
|
||||
port: initialData?.port ?? 5432,
|
||||
username: initialData?.username ?? '',
|
||||
password: '',
|
||||
database: initialData?.database ?? '',
|
||||
ssl_mode: initialData?.ssl_mode ?? 'disable',
|
||||
timeout: initialData?.timeout ?? 30,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleTypeChange = (type: NewConnectionFormData['type']) => {
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
type,
|
||||
port: DEFAULT_PORTS[type] || 0,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof NewConnectionFormData, value: string | number) => {
|
||||
setForm((prev) => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
setLoading(true);
|
||||
try {
|
||||
await onConfirm(form);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const isSQLite = form.type === 'sqlite';
|
||||
|
||||
return (
|
||||
<div className="dialog-overlay" onClick={onCancel}>
|
||||
<div className="dialog-box" onClick={(e) => e.stopPropagation()}>
|
||||
<div className="dialog-header">
|
||||
<h2 className="dialog-title">{isEditMode ? 'Edit Connection' : 'New Connection'}</h2>
|
||||
<button className="btn-icon dialog-close" onClick={onCancel} aria-label="Close">✕</button>
|
||||
</div>
|
||||
|
||||
<form className="dialog-form" onSubmit={handleSubmit}>
|
||||
{/* Connection Type */}
|
||||
<div className="form-group">
|
||||
<label className="form-label">Database Type</label>
|
||||
<div className="type-selector">
|
||||
{(['postgres', 'mysql', 'sqlite'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
type="button"
|
||||
className={`type-btn ${form.type === t ? 'active' : ''}`}
|
||||
onClick={() => handleTypeChange(t)}
|
||||
disabled={isEditMode}
|
||||
>
|
||||
{t === 'postgres' ? '🐘 PostgreSQL' : t === 'mysql' ? '🗄️ MySQL' : '◪ SQLite'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Name */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="conn-name">Connection Name <span className="required">*</span></label>
|
||||
<input
|
||||
id="conn-name"
|
||||
className="form-input"
|
||||
type="text"
|
||||
placeholder="e.g. My Postgres DB"
|
||||
value={form.name}
|
||||
onChange={(e) => handleChange('name', e.target.value)}
|
||||
required
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
{!isSQLite && (
|
||||
<>
|
||||
{/* Host & Port */}
|
||||
<div className="form-row">
|
||||
<div className="form-group flex-3">
|
||||
<label className="form-label" htmlFor="conn-host">Host <span className="required">*</span></label>
|
||||
<input
|
||||
id="conn-host"
|
||||
className="form-input"
|
||||
type="text"
|
||||
placeholder="127.0.0.1"
|
||||
value={form.host}
|
||||
onChange={(e) => handleChange('host', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group flex-1">
|
||||
<label className="form-label" htmlFor="conn-port">Port <span className="required">*</span></label>
|
||||
<input
|
||||
id="conn-port"
|
||||
className="form-input"
|
||||
type="number"
|
||||
min={1}
|
||||
max={65535}
|
||||
value={form.port}
|
||||
onChange={(e) => handleChange('port', parseInt(e.target.value, 10) || 0)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Username & Password */}
|
||||
<div className="form-row">
|
||||
<div className="form-group flex-1">
|
||||
<label className="form-label" htmlFor="conn-user">Username</label>
|
||||
<input
|
||||
id="conn-user"
|
||||
className="form-input"
|
||||
type="text"
|
||||
placeholder="postgres"
|
||||
value={form.username}
|
||||
onChange={(e) => handleChange('username', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="form-group flex-1">
|
||||
<label className="form-label" htmlFor="conn-pass">Password</label>
|
||||
<input
|
||||
id="conn-pass"
|
||||
className="form-input"
|
||||
type="password"
|
||||
placeholder={isEditMode ? 'Leave blank to keep current' : '••••••••'}
|
||||
value={form.password}
|
||||
onChange={(e) => handleChange('password', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Database / File */}
|
||||
<div className="form-group">
|
||||
<label className="form-label" htmlFor="conn-db">
|
||||
{isSQLite ? 'File Path' : 'Database'} <span className="required">*</span>
|
||||
</label>
|
||||
<input
|
||||
id="conn-db"
|
||||
className="form-input"
|
||||
type="text"
|
||||
placeholder={isSQLite ? '/path/to/db.sqlite' : 'postgres'}
|
||||
value={form.database}
|
||||
onChange={(e) => handleChange('database', e.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{error && <div className="form-error">{error}</div>}
|
||||
|
||||
<div className="dialog-actions">
|
||||
<button type="button" className="btn btn-secondary" onClick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
</button>
|
||||
<button type="submit" className="btn btn-primary" disabled={loading}>
|
||||
{loading ? (isEditMode ? 'Saving…' : 'Connecting…') : (isEditMode ? 'Save Changes' : 'Create Connection')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default NewConnectionDialog;
|
||||
Reference in New Issue
Block a user