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:
loveuer
2026-04-06 21:45:28 +08:00
parent 9874561410
commit 347ecd0f1b
22 changed files with 2475 additions and 315 deletions

View 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);
}

View 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;