mirror of
https://github.com/R0m1k3/CollectFlow.git
synced 2026-10-12 09:46:56 +02:00
feat: implement UI-driven database configuration
This commit is contained in:
1 parent
e54e679bd4
commit
0e4c276816
4 files changed
+248
-17
No files matched your search
@@ -5,6 +5,9 @@ import { Save, Eye, EyeOff, CheckCircle, AlertCircle, Loader2, RefreshCw, Sun, M
|
||||
import { useTheme } from "next-themes";
|
||||
import { useGridStore } from "@/features/grid/store/use-grid-store";
|
||||
import { useScoreSettingsStore } from "@/features/score/store/use-score-settings-store";
|
||||
import { useDbSettingsStore } from "@/features/settings/store/use-db-settings-store";
|
||||
import { testDatabaseConnection, saveDatabaseSettings, getSavedDatabaseConfig } from "@/features/settings/actions";
|
||||
import { useEffect } from "react";
|
||||
|
||||
interface OpenRouterModel { id: string; name: string; free: boolean; }
|
||||
|
||||
@@ -50,11 +53,45 @@ export default function SettingsPage() {
|
||||
const [showKey, setShowKey] = useState(false);
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [dbStatus, setDbStatus] = useState<"idle" | "testing" | "ok" | "error">("idle");
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saved">("idle");
|
||||
const [dbError, setDbError] = useState<string | null>(null);
|
||||
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved">("idle");
|
||||
const [models, setModels] = useState<OpenRouterModel[]>([]);
|
||||
const [selectedModel, setSelectedModel] = useState("google/gemini-flash-1.5");
|
||||
const [modelsStatus, setModelsStatus] = useState<"idle" | "loading" | "ok" | "error">("idle");
|
||||
|
||||
const {
|
||||
host, setHost,
|
||||
port, setPort,
|
||||
database, setDatabase,
|
||||
user, setUser,
|
||||
password, setPassword,
|
||||
ssl, setSsl,
|
||||
getDatabaseUrl
|
||||
} = useDbSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
// Optionnel : On pourrait charger la config serveur au montage si on veut écraser le localStorage
|
||||
// Pour l'instant on laisse le localStorage faire son travail, mais on offre une fonction de reset
|
||||
}, []);
|
||||
|
||||
const reloadFromServer = async () => {
|
||||
const config = await getSavedDatabaseConfig();
|
||||
if (config && config.url) {
|
||||
// Parser l'URL pour remettre dans le store
|
||||
try {
|
||||
const url = new URL(config.url.replace("postgres://", "http://")); // URL parser helper
|
||||
setHost(url.hostname);
|
||||
setPort(url.port || "5432");
|
||||
setDatabase(url.pathname.slice(1).split("?")[0]);
|
||||
setUser(url.username);
|
||||
setPassword(decodeURIComponent(url.password));
|
||||
setSsl(config.url.includes("sslmode=require"));
|
||||
} catch (e) {
|
||||
console.error("Failed to parse saved URL", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchModels = useCallback(async (key: string) => {
|
||||
if (!key.trim()) return;
|
||||
setModelsStatus("loading");
|
||||
@@ -69,10 +106,30 @@ export default function SettingsPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const testConnection = async () => {
|
||||
const testDb = async () => {
|
||||
setDbStatus("testing");
|
||||
await new Promise((r) => setTimeout(r, 1200));
|
||||
setDbStatus("error");
|
||||
setDbError(null);
|
||||
const url = getDatabaseUrl();
|
||||
const res = await testDatabaseConnection(url);
|
||||
if (res.success) {
|
||||
setDbStatus("ok");
|
||||
} else {
|
||||
setDbStatus("error");
|
||||
setDbError(res.error || "Erreur de connexion inconnue");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setSaveStatus("saving");
|
||||
const url = getDatabaseUrl();
|
||||
const res = await saveDatabaseSettings(url);
|
||||
if (res.success) {
|
||||
setSaveStatus("saved");
|
||||
setTimeout(() => setSaveStatus("idle"), 2500);
|
||||
} else {
|
||||
setSaveStatus("idle");
|
||||
alert("Erreur lors de la sauvegarde : " + res.error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -85,38 +142,90 @@ export default function SettingsPage() {
|
||||
{/* PostgreSQL */}
|
||||
<Section title="PostgreSQL" subtitle="Connexion à votre instance Docker ou distante">
|
||||
<Field label="Hôte / Nom du container" hint="Ex: localhost, 192.168.1.10, ou nom du service Docker (ex: postgres, db)">
|
||||
<input type="text" placeholder="localhost" className="apple-input font-mono" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="localhost"
|
||||
value={host}
|
||||
onChange={(e) => setHost(e.target.value)}
|
||||
className="apple-input font-mono"
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Port">
|
||||
<input type="text" placeholder="5432" defaultValue="5432" className="apple-input font-mono" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="5432"
|
||||
value={port}
|
||||
onChange={(e) => setPort(e.target.value)}
|
||||
className="apple-input font-mono"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Base de données">
|
||||
<input type="text" placeholder="collectflow" className="apple-input font-mono" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="collectflow"
|
||||
value={database}
|
||||
onChange={(e) => setDatabase(e.target.value)}
|
||||
className="apple-input font-mono"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Utilisateur">
|
||||
<input type="text" placeholder="postgres" className="apple-input font-mono" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="postgres"
|
||||
value={user}
|
||||
onChange={(e) => setUser(e.target.value)}
|
||||
className="apple-input font-mono"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Mot de passe">
|
||||
<input type="password" placeholder="••••••••" className="apple-input font-mono" />
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password || ""}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="apple-input font-mono"
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<input type="checkbox" id="ssl" className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer" />
|
||||
<input
|
||||
type="checkbox"
|
||||
id="ssl"
|
||||
checked={ssl}
|
||||
onChange={(e) => setSsl(e.target.checked)}
|
||||
className="w-3.5 h-3.5 rounded accent-teal-500 cursor-pointer"
|
||||
/>
|
||||
<label htmlFor="ssl" className="text-[12px] cursor-pointer" style={{ color: "var(--text-secondary)" }}>Connexion SSL</label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={testConnection} disabled={dbStatus === "testing"} className="apple-btn-secondary">
|
||||
<button onClick={testDb} disabled={dbStatus === "testing"} className="apple-btn-secondary">
|
||||
{dbStatus === "testing" && <Loader2 className="w-3.5 h-3.5 animate-spin" />}
|
||||
{dbStatus === "ok" && <CheckCircle className="w-3.5 h-3.5 text-teal-500" />}
|
||||
{dbStatus === "error" && <AlertCircle className="w-3.5 h-3.5 text-red-400" />}
|
||||
Tester la connexion
|
||||
</button>
|
||||
{dbStatus === "error" && (
|
||||
<span className="text-[12px]" style={{ color: "var(--text-secondary)" }}>Connexion refusée — vérifiez les paramètres</span>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-[12px] text-red-400 font-medium">Connexion refusée</span>
|
||||
{dbError && <span className="text-[11px]" style={{ color: "var(--text-muted)" }}>{dbError}</span>}
|
||||
</div>
|
||||
)}
|
||||
{dbStatus === "ok" && (
|
||||
<span className="text-[12px] text-teal-500 font-medium">Connexion réussie !</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={reloadFromServer}
|
||||
className="flex items-center gap-2 text-[12px] font-medium transition-colors hover:text-emerald-500"
|
||||
style={{ color: "var(--text-secondary)" }}
|
||||
>
|
||||
<RotateCcw className="w-3.5 h-3.5" />
|
||||
Recharger la configuration enregistrée sur le serveur
|
||||
</button>
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
@@ -281,12 +390,13 @@ export default function SettingsPage() {
|
||||
{/* Save */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => { setSaveStatus("saved"); setTimeout(() => setSaveStatus("idle"), 2500); }}
|
||||
onClick={handleSave}
|
||||
disabled={saveStatus === "saving"}
|
||||
className="apple-btn-primary"
|
||||
style={{ background: "var(--accent)" }}
|
||||
>
|
||||
{saveStatus === "saved" ? <CheckCircle className="w-4 h-4" /> : <Save className="w-4 h-4" />}
|
||||
{saveStatus === "saved" ? "Sauvegardé !" : "Sauvegarder"}
|
||||
{saveStatus === "saving" ? <Loader2 className="w-4 h-4 animate-spin" /> : saveStatus === "saved" ? <CheckCircle className="w-4 h-4" /> : <Save className="w-4 h-4" />}
|
||||
{saveStatus === "saving" ? "Enregistrement..." : saveStatus === "saved" ? "Sauvegardé !" : "Sauvegarder la configuration"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+18
-2
@@ -1,10 +1,26 @@
|
||||
import { drizzle } from "drizzle-orm/pg-proxy"; // Or node-postgres if using directly
|
||||
import { drizzle as nodeDrizzle } from "drizzle-orm/node-postgres";
|
||||
import { Pool } from "pg";
|
||||
import * as schema from "./schema";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
let connectionString = process.env.DATABASE_URL;
|
||||
|
||||
// On tente de lire la config sauvegardée via l'UI
|
||||
try {
|
||||
const CONFIG_PATH = path.join(process.cwd(), ".db-config.json");
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8"));
|
||||
if (config.url) {
|
||||
connectionString = config.url;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Error reading database config file:", err);
|
||||
}
|
||||
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
connectionString,
|
||||
});
|
||||
|
||||
export const db = nodeDrizzle(pool, { schema });
|
||||
@@ -0,0 +1,52 @@
|
||||
"use server";
|
||||
|
||||
import { Pool } from "pg";
|
||||
import fs from "fs/promises";
|
||||
import path from "path";
|
||||
|
||||
const CONFIG_FILE = path.join(process.cwd(), ".db-config.json");
|
||||
|
||||
export interface DbConfig {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export async function testDatabaseConnection(url: string) {
|
||||
console.log("Testing connection to:", url.replace(/:([^@]+)@/, ":****@"));
|
||||
const pool = new Pool({
|
||||
connectionString: url,
|
||||
connectionTimeoutMillis: 5000,
|
||||
});
|
||||
|
||||
try {
|
||||
const client = await pool.connect();
|
||||
await client.query("SELECT 1");
|
||||
client.release();
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error("Database connection test failed:", error.message);
|
||||
return { success: false, error: error.message };
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveDatabaseSettings(url: string) {
|
||||
try {
|
||||
const config: DbConfig = { url };
|
||||
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
|
||||
console.log("Database configuration saved to:", CONFIG_FILE);
|
||||
return { success: true };
|
||||
} catch (error: any) {
|
||||
console.error("Failed to save database configuration:", error.message);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
export async function getSavedDatabaseConfig(): Promise<DbConfig | null> {
|
||||
try {
|
||||
const data = await fs.readFile(CONFIG_FILE, "utf-8");
|
||||
return JSON.parse(data);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
import { create } from "zustand";
|
||||
import { persist } from "zustand/middleware";
|
||||
|
||||
export interface DatabaseSettings {
|
||||
host: string;
|
||||
port: string;
|
||||
database: string;
|
||||
user: string;
|
||||
password?: string;
|
||||
ssl: boolean;
|
||||
}
|
||||
|
||||
interface DatabaseSettingsState extends DatabaseSettings {
|
||||
setHost: (host: string) => void;
|
||||
setPort: (port: string) => void;
|
||||
setDatabase: (database: string) => void;
|
||||
setUser: (user: string) => void;
|
||||
setPassword: (password: string) => void;
|
||||
setSsl: (ssl: boolean) => void;
|
||||
getDatabaseUrl: () => string;
|
||||
}
|
||||
|
||||
export const useDbSettingsStore = create<DatabaseSettingsState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
host: "localhost",
|
||||
port: "5432",
|
||||
database: "collectflow",
|
||||
user: "postgres",
|
||||
password: "",
|
||||
ssl: false,
|
||||
|
||||
setHost: (host) => set({ host }),
|
||||
setPort: (port) => set({ port }),
|
||||
setDatabase: (database) => set({ database }),
|
||||
setUser: (user) => set({ user }),
|
||||
setPassword: (password) => set({ password }),
|
||||
setSsl: (ssl) => set({ ssl }),
|
||||
|
||||
getDatabaseUrl: () => {
|
||||
const { user, password, host, port, database, ssl } = get();
|
||||
const auth = password ? `${user}:${password}` : user;
|
||||
const url = `postgres://${auth}@${host}:${port}/${database}${ssl ? "?sslmode=require" : ""}`;
|
||||
return url;
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: "collectflow-db-settings",
|
||||
}
|
||||
)
|
||||
);
|
||||
Reference in new issue
Block a user