mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
feat: Add ability to edit existing SQL agent connections (#4848)
* Add the ability to edit existing SQL connections * Enhance SQL connection management by adding connections prop to DBConnection and SQLConnectionModal components for improved duplicate detection and handling. * format * fix: prevent input defocus in SQL connection edit modal Fixed an issue where typing in input fields would cause the field to lose focus during editing. The useEffect dependency array was using the entire existingConnection object, which could change reference on parent re-renders, triggering unnecessary re-fetches and unmounting form inputs. Changed the dependency to use the primitive database_id value instead of the object reference, ensuring the effect only runs when the actual connection being edited changes. * fix: prevent duplicate SQL connections from being created Fixed an issue where saving SQL connections multiple times would create duplicate entries with auto-generated hash suffixes (e.g., my-db-abc7). This occurred because the frontend maintained stale action properties on connections after saves, causing the backend to treat already-saved connections as new additions. Backend changes (server/models/systemSettings.js): - Modified mergeConnections to skip action:add items that already exist - Reject duplicate updates instead of auto-renaming with UUID suffixes - Check if original connection exists before applying updates Frontend changes: - Added hasChanges prop to SQL connector component - Automatically refresh connections from backend after successful save - Ensures local state has clean data without stale action properties This prevents the creation of confusing duplicate entries and ensures only the connections the user explicitly created are stored. * Refactor to use existing system settings endpoint for getting agent SQL connections | Add better documentation * Simplify handleUpdateConnection handler * refactor mergeConnections to use map * remove console log * fix bug where edit SQL connection modal values werent recomputed after re-opening * Add loading state for fetching agent SQL connections * tooltip * remove unused import * Put skip conditions in switch statement * throw error if default switch case is triggered --------- Co-authored-by: shatfield4 <seanhatfield5@gmail.com> Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
import PostgreSQLLogo from "./icons/postgresql.png";
|
||||
import MySQLLogo from "./icons/mysql.png";
|
||||
import MSSQLLogo from "./icons/mssql.png";
|
||||
import { X } from "@phosphor-icons/react";
|
||||
import { PencilSimple, X } from "@phosphor-icons/react";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import EditSQLConnection from "./SQLConnectionModal";
|
||||
|
||||
export const DB_LOGOS = {
|
||||
postgresql: PostgreSQLLogo,
|
||||
@@ -9,8 +11,16 @@ export const DB_LOGOS = {
|
||||
"sql-server": MSSQLLogo,
|
||||
};
|
||||
|
||||
export default function DBConnection({ connection, onRemove }) {
|
||||
export default function DBConnection({
|
||||
connection,
|
||||
onRemove,
|
||||
onUpdate,
|
||||
setHasChanges,
|
||||
connections = [],
|
||||
}) {
|
||||
const { database_id, engine } = connection;
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
|
||||
function removeConfirmation() {
|
||||
if (
|
||||
!window.confirm(
|
||||
@@ -33,14 +43,33 @@ export default function DBConnection({ connection, onRemove }) {
|
||||
<div className="text-sm font-semibold text-white">{database_id}</div>
|
||||
<div className="mt-1 text-xs text-description">{engine}</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={removeConfirmation}
|
||||
className="border-none text-white hover:text-red-500"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
<div className="flex gap-x-2">
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip-id="edit-sql-connection-tooltip"
|
||||
className="border-none text-theme-text-secondary hover:text-theme-text-primary transition-colors duration-200 p-1 rounded"
|
||||
onClick={openModal}
|
||||
>
|
||||
<PencilSimple size={18} />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-tooltip-id="delete-sql-connection-tooltip"
|
||||
onClick={removeConfirmation}
|
||||
className="border-none text-theme-text-secondary hover:text-red-500"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<EditSQLConnection
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
existingConnection={connection}
|
||||
onSubmit={onUpdate}
|
||||
setHasChanges={setHasChanges}
|
||||
connections={connections}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import ModalWrapper from "@/components/ModalWrapper";
|
||||
import { WarningOctagon, X } from "@phosphor-icons/react";
|
||||
@@ -7,6 +7,33 @@ import System from "@/models/system";
|
||||
import showToast from "@/utils/toast";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
|
||||
/**
|
||||
* Converts a string to a URL-friendly slug format.
|
||||
* Matches backend slugify behavior for consistent database_id generation.
|
||||
* @param {string} str - The string to slugify
|
||||
* @returns {string} - The slugified string (lowercase, hyphens, no special chars)
|
||||
*/
|
||||
function slugify(str) {
|
||||
return str
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, "") // Remove special characters
|
||||
.replace(/[\s_]+/g, "-") // Replace spaces and underscores with hyphens
|
||||
.replace(/^-+|-+$/g, ""); // Remove leading/trailing hyphens
|
||||
}
|
||||
|
||||
/**
|
||||
* Assembles a database connection string based on the engine type and configuration.
|
||||
* @param {Object} params - Connection parameters
|
||||
* @param {string} params.engine - The database engine ('postgresql', 'mysql', or 'sql-server')
|
||||
* @param {string} [params.username=""] - Database username
|
||||
* @param {string} [params.password=""] - Database password
|
||||
* @param {string} [params.host=""] - Database host/endpoint
|
||||
* @param {string} [params.port=""] - Database port
|
||||
* @param {string} [params.database=""] - Database name
|
||||
* @param {boolean} [params.encrypt=false] - Enable encryption (SQL Server only)
|
||||
* @returns {string|null} - The assembled connection string, error message if fields missing, or null if engine invalid
|
||||
*/
|
||||
function assembleConnectionString({
|
||||
engine,
|
||||
username = "",
|
||||
@@ -32,6 +59,7 @@ function assembleConnectionString({
|
||||
|
||||
const DEFAULT_ENGINE = "postgresql";
|
||||
const DEFAULT_CONFIG = {
|
||||
name: "",
|
||||
username: null,
|
||||
password: null,
|
||||
host: null,
|
||||
@@ -41,15 +69,59 @@ const DEFAULT_CONFIG = {
|
||||
encrypt: false,
|
||||
};
|
||||
|
||||
export default function NewSQLConnection({
|
||||
/**
|
||||
* Modal component for creating or editing SQL database connections.
|
||||
* Supports PostgreSQL, MySQL, and SQL Server with connection validation.
|
||||
* Handles duplicate connection name detection and connection string assembly.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {boolean} props.isOpen - Whether the modal is currently open
|
||||
* @param {Function} props.closeModal - Callback to close the modal
|
||||
* @param {Function} props.onSubmit - Callback when connection is successfully validated and saved
|
||||
* @param {Function} props.setHasChanges - Callback to mark that changes have been made
|
||||
* @param {Object|null} [props.existingConnection=null] - Existing connection data for edit mode (contains database_id, engine, username, password, host, port, database, scheme, encrypt)
|
||||
* @param {Array} [props.connections=[]] - List of all existing connections for duplicate detection
|
||||
* @returns {React.ReactPortal|null} - Portal containing the modal UI, or null if not open
|
||||
*/
|
||||
export default function SQLConnectionModal({
|
||||
isOpen,
|
||||
closeModal,
|
||||
onSubmit,
|
||||
setHasChanges,
|
||||
existingConnection = null, // { database_id, engine } for edit mode
|
||||
connections = [], // List of all existing connections for duplicate detection
|
||||
}) {
|
||||
const isEditMode = !!existingConnection;
|
||||
const [engine, setEngine] = useState(DEFAULT_ENGINE);
|
||||
const [config, setConfig] = useState(DEFAULT_CONFIG);
|
||||
const [isValidating, setIsValidating] = useState(false);
|
||||
|
||||
// Sync state when modal opens - useState initial values only run once on mount,
|
||||
// so we need this effect to update state when the modal is reopened
|
||||
useEffect(() => {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (existingConnection) {
|
||||
setEngine(existingConnection.engine);
|
||||
setConfig({
|
||||
name: existingConnection.database_id,
|
||||
username: existingConnection.username,
|
||||
password: existingConnection.password,
|
||||
host: existingConnection.host,
|
||||
port: existingConnection.port,
|
||||
database: existingConnection.database,
|
||||
scheme: existingConnection.scheme,
|
||||
encrypt: existingConnection?.encrypt,
|
||||
});
|
||||
} else {
|
||||
setEngine(DEFAULT_ENGINE);
|
||||
setConfig(DEFAULT_CONFIG);
|
||||
}
|
||||
}, [isOpen, existingConnection]);
|
||||
|
||||
// Track original database ID to send to server for updating if in edit mode
|
||||
const originalDatabaseId = isEditMode ? existingConnection.database_id : null;
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
function handleClose() {
|
||||
@@ -61,6 +133,7 @@ export default function NewSQLConnection({
|
||||
function onFormChange(e) {
|
||||
const form = new FormData(e.target.form);
|
||||
setConfig({
|
||||
name: form.get("name").trim(),
|
||||
username: form.get("username").trim(),
|
||||
password: form.get("password"),
|
||||
host: form.get("host").trim(),
|
||||
@@ -70,14 +143,61 @@ export default function NewSQLConnection({
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a connection name (slugified) already exists in the connections list.
|
||||
* For edit mode, excludes the original connection being edited.
|
||||
* @param {string} slugifiedName - The slugified name to check
|
||||
* @returns {boolean} - True if duplicate exists, false otherwise
|
||||
*/
|
||||
function isDuplicateConnectionName(slugifiedName) {
|
||||
// Get active connections (not marked for removal)
|
||||
const activeConnections = connections.filter(
|
||||
(conn) => conn.action !== "remove"
|
||||
);
|
||||
|
||||
// Check for duplicates, excluding the original connection in edit mode
|
||||
return activeConnections.some((conn) => {
|
||||
// In edit mode, skip the original connection being edited
|
||||
if (isEditMode && conn.database_id === originalDatabaseId) {
|
||||
return false;
|
||||
}
|
||||
return conn.database_id === slugifiedName;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles form submission for both creating new connections and updating existing ones.
|
||||
* Process:
|
||||
* 1. Slugify the connection name to match backend behavior
|
||||
* 2. Check for duplicate names (prevents frontend from sending invalid updates)
|
||||
* 3. Validate the connection string by attempting to connect to the database
|
||||
* 4. If valid, submit with appropriate action ("add" or "update")
|
||||
*
|
||||
* For updates: Includes originalDatabaseId so backend can find and replace the old connection
|
||||
* For new connections: Just includes the new connection data
|
||||
*/
|
||||
async function handleUpdate(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const form = new FormData(e.target);
|
||||
const connectionString = assembleConnectionString({ engine, ...config });
|
||||
|
||||
// Slugify the database_id immediately to match backend behavior
|
||||
const slugifiedDatabaseId = slugify(form.get("name"));
|
||||
|
||||
// Check for duplicate connection names before validation
|
||||
if (isDuplicateConnectionName(slugifiedDatabaseId)) {
|
||||
showToast(
|
||||
`A connection with the name "${slugifiedDatabaseId}" already exists. Please choose a different name.`,
|
||||
"error",
|
||||
{ clear: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsValidating(true);
|
||||
try {
|
||||
// Validate that we can actually connect to this database
|
||||
const { success, error } = await System.validateSQLConnection(
|
||||
engine,
|
||||
connectionString
|
||||
@@ -93,11 +213,30 @@ export default function NewSQLConnection({
|
||||
return;
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
const connectionData = {
|
||||
engine,
|
||||
database_id: form.get("name"),
|
||||
database_id: slugifiedDatabaseId,
|
||||
connectionString,
|
||||
});
|
||||
};
|
||||
|
||||
if (isEditMode) {
|
||||
// EDIT MODE: Send update action with originalDatabaseId
|
||||
// This tells the backend to find the connection with originalDatabaseId
|
||||
// and replace it with the new connection data
|
||||
onSubmit({
|
||||
...connectionData,
|
||||
action: "update",
|
||||
originalDatabaseId: originalDatabaseId,
|
||||
});
|
||||
} else {
|
||||
// CREATE MODE: Send add action
|
||||
// Backend will check for duplicates and add if unique
|
||||
onSubmit({
|
||||
...connectionData,
|
||||
action: "add",
|
||||
});
|
||||
}
|
||||
|
||||
setHasChanges(true);
|
||||
handleClose();
|
||||
} catch (error) {
|
||||
@@ -123,7 +262,7 @@ export default function NewSQLConnection({
|
||||
<div className="relative p-6 border-b rounded-t border-theme-modal-border">
|
||||
<div className="w-full flex gap-x-2 items-center">
|
||||
<h3 className="text-xl font-semibold text-white overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||
New SQL Connection
|
||||
{isEditMode ? "Edit SQL Connection" : "New SQL Connection"}
|
||||
</h3>
|
||||
</div>
|
||||
<button
|
||||
@@ -142,8 +281,9 @@ export default function NewSQLConnection({
|
||||
<div className="px-7 py-6">
|
||||
<div className="space-y-6 max-h-[60vh] overflow-y-auto pr-2">
|
||||
<p className="text-sm text-white/60">
|
||||
Add the connection information for your database below and it
|
||||
will be available for future SQL agent calls.
|
||||
{isEditMode
|
||||
? "Update the connection information for your database below."
|
||||
: "Add the connection information for your database below and it will be available for future SQL agent calls."}
|
||||
</p>
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="border border-red-800 bg-zinc-800 light:bg-red-200/50 p-4 rounded-lg flex items-center gap-x-2 text-sm text-red-400 light:text-red-500">
|
||||
@@ -191,6 +331,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.name || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -207,6 +348,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.username || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
@@ -214,13 +356,14 @@ export default function NewSQLConnection({
|
||||
Database user password
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
type="password"
|
||||
name="password"
|
||||
className="border-none bg-theme-settings-input-bg w-full text-white placeholder:text-theme-settings-input-placeholder text-sm rounded-lg focus:outline-primary-button active:outline-primary-button outline-none block w-full p-2.5"
|
||||
placeholder="password123"
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.password || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,6 +381,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.host || ""}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -252,6 +396,7 @@ export default function NewSQLConnection({
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.port || ""}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,6 +413,7 @@ export default function NewSQLConnection({
|
||||
required={true}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.database || ""}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -284,6 +430,7 @@ export default function NewSQLConnection({
|
||||
required={false}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
defaultValue={config.schema || ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -328,6 +475,16 @@ export default function NewSQLConnection({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Database engine selection button component.
|
||||
* Displays a database logo and handles selection state.
|
||||
*
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.provider - The database provider identifier ('postgresql', 'mysql', 'sql-server')
|
||||
* @param {boolean} props.active - Whether this engine is currently selected
|
||||
* @param {Function} props.onClick - Callback when the engine is clicked
|
||||
* @returns {JSX.Element} - Button element with database logo
|
||||
*/
|
||||
function DBEngine({ provider, active, onClick }) {
|
||||
return (
|
||||
<button
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import DBConnection from "./DBConnection";
|
||||
import { Plus, Database } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./NewConnectionModal";
|
||||
import { Plus, Database, CircleNotch } from "@phosphor-icons/react";
|
||||
import NewSQLConnection from "./SQLConnectionModal";
|
||||
import { useModal } from "@/hooks/useModal";
|
||||
import SQLAgentImage from "@/media/agents/sql-agent.png";
|
||||
import Admin from "@/models/admin";
|
||||
import Toggle from "@/components/lib/Toggle";
|
||||
import { Tooltip } from "react-tooltip";
|
||||
|
||||
export default function AgentSQLConnectorSelection({
|
||||
skill,
|
||||
@@ -13,15 +14,40 @@ export default function AgentSQLConnectorSelection({
|
||||
toggleSkill,
|
||||
enabled = false,
|
||||
setHasChanges,
|
||||
hasChanges = false,
|
||||
}) {
|
||||
const { isOpen, openModal, closeModal } = useModal();
|
||||
const [connections, setConnections] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const prevHasChanges = useRef(hasChanges);
|
||||
|
||||
// Load connections on mount
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
Admin.systemPreferencesByFields(["agent_sql_connections"])
|
||||
.then((res) => setConnections(res?.settings?.agent_sql_connections ?? []))
|
||||
.catch(() => setConnections([]));
|
||||
.catch(() => setConnections([]))
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
// Refresh connections from backend when save completes (hasChanges: true -> false)
|
||||
// This ensures we get clean data without stale action properties
|
||||
useEffect(() => {
|
||||
if (prevHasChanges.current === true && hasChanges === false) {
|
||||
Admin.systemPreferencesByFields(["agent_sql_connections"])
|
||||
.then((res) =>
|
||||
setConnections(res?.settings?.agent_sql_connections ?? [])
|
||||
)
|
||||
.catch(() => {});
|
||||
}
|
||||
prevHasChanges.current = hasChanges;
|
||||
}, [hasChanges]);
|
||||
|
||||
/**
|
||||
* Marks a connection for removal by adding action: "remove".
|
||||
* The connection stays in the array (for undo capability) until saved.
|
||||
* @param {string} databaseId - The database_id of the connection to remove
|
||||
*/
|
||||
function handleRemoveConnection(databaseId) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) =>
|
||||
@@ -33,6 +59,38 @@ export default function AgentSQLConnectorSelection({
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing connection by replacing it in the local state.
|
||||
* This removes the old connection (by originalDatabaseId) and adds the updated version.
|
||||
*
|
||||
* Note: The old connection is removed from local state immediately, but the backend
|
||||
* handles the actual update logic when saved. See mergeConnections in server/models/systemSettings.js
|
||||
*
|
||||
* @param {Object} updatedConnection - The updated connection data
|
||||
* @param {string} updatedConnection.originalDatabaseId - The original database_id before the update
|
||||
* @param {string} updatedConnection.database_id - The new database_id
|
||||
* @param {string} updatedConnection.action - Should be "update"
|
||||
*/
|
||||
function handleUpdateConnection(updatedConnection) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) =>
|
||||
prev.map((conn) =>
|
||||
conn.database_id === updatedConnection.originalDatabaseId
|
||||
? updatedConnection
|
||||
: conn
|
||||
)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Adds a new connection to the local state with action: "add".
|
||||
* The backend will validate and deduplicate when saved.
|
||||
* @param {Object} newConnection - The new connection data with action: "add"
|
||||
*/
|
||||
function handleAddConnection(newConnection) {
|
||||
setHasChanges(true);
|
||||
setConnections((prev) => [...prev, newConnection]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="p-2">
|
||||
@@ -84,15 +142,27 @@ export default function AgentSQLConnectorSelection({
|
||||
Your database connections
|
||||
</p>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={handleRemoveConnection}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-4">
|
||||
<CircleNotch
|
||||
size={24}
|
||||
className="animate-spin text-theme-text-primary"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
connections
|
||||
.filter((connection) => connection.action !== "remove")
|
||||
.map((connection) => (
|
||||
<DBConnection
|
||||
key={connection.database_id}
|
||||
connection={connection}
|
||||
onRemove={handleRemoveConnection}
|
||||
onUpdate={handleUpdateConnection}
|
||||
setHasChanges={setHasChanges}
|
||||
connections={connections}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={openModal}
|
||||
@@ -121,9 +191,32 @@ export default function AgentSQLConnectorSelection({
|
||||
isOpen={isOpen}
|
||||
closeModal={closeModal}
|
||||
setHasChanges={setHasChanges}
|
||||
onSubmit={(newDb) =>
|
||||
setConnections((prev) => [...prev, { action: "add", ...newDb }])
|
||||
}
|
||||
onSubmit={handleAddConnection}
|
||||
connections={connections}
|
||||
/>
|
||||
<Tooltip
|
||||
id="edit-sql-connection-tooltip"
|
||||
content="Edit SQL connection"
|
||||
place="top"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs !opacity-100"
|
||||
style={{
|
||||
maxWidth: "250px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
/>
|
||||
<Tooltip
|
||||
id="delete-sql-connection-tooltip"
|
||||
content="Delete SQL connection"
|
||||
place="top"
|
||||
delayShow={300}
|
||||
className="tooltip !text-xs !opacity-100"
|
||||
style={{
|
||||
maxWidth: "250px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -400,6 +400,7 @@ export default function AdminAgents() {
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChanges={hasChanges}
|
||||
{...configurableSkills[selectedSkill]}
|
||||
/>
|
||||
)}
|
||||
@@ -591,6 +592,7 @@ export default function AdminAgents() {
|
||||
configurableSkills[selectedSkill]?.skill
|
||||
)}
|
||||
setHasChanges={setHasChanges}
|
||||
hasChanges={hasChanges}
|
||||
{...configurableSkills[selectedSkill]}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -376,7 +376,7 @@ function adminEndpoints(app) {
|
||||
break;
|
||||
case "agent_sql_connections":
|
||||
requestedSettings[label] =
|
||||
await SystemSettings.brief.agent_sql_connections();
|
||||
await SystemSettings.agent_sql_connections();
|
||||
break;
|
||||
case "default_agent_skills":
|
||||
requestedSettings[label] = safeJsonParse(setting?.value, []);
|
||||
|
||||
@@ -10,6 +10,9 @@ const { MetaGenerator } = require("../utils/boot/MetaGenerator");
|
||||
const { PGVector } = require("../utils/vectorDbProviders/pgvector");
|
||||
const { NativeEmbedder } = require("../utils/EmbeddingEngines/native");
|
||||
const { getBaseLLMProviderModel } = require("../utils/helpers");
|
||||
const {
|
||||
ConnectionStringParser,
|
||||
} = require("../utils/agents/aibitat/plugins/sql-agent/SQLConnectors/utils");
|
||||
|
||||
function isNullOrNaN(value) {
|
||||
if (value === null) return true;
|
||||
@@ -681,18 +684,31 @@ const SystemSettings = {
|
||||
};
|
||||
},
|
||||
|
||||
// For special retrieval of a key setting that does not expose any credential information
|
||||
brief: {
|
||||
agent_sql_connections: async function () {
|
||||
const setting = await SystemSettings.get({
|
||||
label: "agent_sql_connections",
|
||||
});
|
||||
if (!setting) return [];
|
||||
return safeJsonParse(setting.value, []).map((dbConfig) => {
|
||||
const { connectionString, ...rest } = dbConfig;
|
||||
return rest;
|
||||
});
|
||||
},
|
||||
agent_sql_connections: async function () {
|
||||
const setting = await SystemSettings.get({
|
||||
label: "agent_sql_connections",
|
||||
});
|
||||
if (!setting) return [];
|
||||
|
||||
const connections = safeJsonParse(setting.value, []).map((conn) => {
|
||||
let scheme = conn.engine;
|
||||
if (scheme === "sql-server") scheme = "mssql";
|
||||
if (scheme === "postgresql") scheme = "postgres";
|
||||
const parser = new ConnectionStringParser({ scheme });
|
||||
|
||||
const parsed = parser.parse(conn.connectionString);
|
||||
return {
|
||||
...conn,
|
||||
username: parsed.username,
|
||||
password: parsed.password,
|
||||
host: parsed.hosts?.[0]?.host,
|
||||
port: parsed.hosts?.[0]?.port,
|
||||
database: parsed.endpoint,
|
||||
scheme: parsed.scheme,
|
||||
};
|
||||
});
|
||||
|
||||
return connections;
|
||||
},
|
||||
getFeatureFlags: async function () {
|
||||
return {
|
||||
@@ -742,42 +758,90 @@ const SystemSettings = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Merges SQL connection updates from the frontend with existing backend connections.
|
||||
* Processes three types of actions: "remove", "update", and "add".
|
||||
*
|
||||
* @param {Array<Object>} existingConnections - Current connections stored in the database
|
||||
* @param {Array<Object>} updates - Connection updates from frontend, each with an action property
|
||||
* @returns {Array<Object>} - The merged connections array
|
||||
*/
|
||||
function mergeConnections(existingConnections = [], updates = []) {
|
||||
let updatedConnections = [...existingConnections];
|
||||
const existingDbIds = existingConnections.map((conn) => conn.database_id);
|
||||
|
||||
// First remove all 'action:remove' candidates from existing connections.
|
||||
const toRemove = updates
|
||||
.filter((conn) => conn.action === "remove")
|
||||
.map((conn) => conn.database_id);
|
||||
updatedConnections = updatedConnections.filter(
|
||||
(conn) => !toRemove.includes(conn.database_id)
|
||||
const connectionsMap = new Map(
|
||||
existingConnections.map((conn) => [conn.database_id, conn])
|
||||
);
|
||||
|
||||
// Next add all 'action:add' candidates into the updatedConnections; We DO NOT validate the connection strings.
|
||||
// but we do validate their database_id is unique.
|
||||
updates
|
||||
.filter((conn) => conn.action === "add")
|
||||
.forEach((update) => {
|
||||
if (!update.connectionString) return; // invalid connection string
|
||||
for (const update of updates) {
|
||||
const {
|
||||
action,
|
||||
database_id,
|
||||
originalDatabaseId,
|
||||
connectionString,
|
||||
engine,
|
||||
} = update;
|
||||
|
||||
// Remap name to be unique to entire set.
|
||||
if (existingDbIds.includes(update.database_id)) {
|
||||
update.database_id = slugify(
|
||||
`${update.database_id}-${v4().slice(0, 4)}`
|
||||
);
|
||||
} else {
|
||||
update.database_id = slugify(update.database_id);
|
||||
switch (action) {
|
||||
case "remove": {
|
||||
connectionsMap.delete(database_id);
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
if (!connectionString) continue;
|
||||
const newId = slugify(database_id);
|
||||
|
||||
// Verify original connection exists
|
||||
if (!connectionsMap.has(originalDatabaseId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Update skipped: Original connection "${originalDatabaseId}" not found`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Check for name conflict (excluding the one being updated)
|
||||
if (newId !== originalDatabaseId && connectionsMap.has(newId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Update skipped: New name "${newId}" conflicts with existing connection`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove old and add updated connection
|
||||
connectionsMap.delete(originalDatabaseId);
|
||||
connectionsMap.set(newId, {
|
||||
engine,
|
||||
database_id: newId,
|
||||
connectionString,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
updatedConnections.push({
|
||||
engine: update.engine,
|
||||
database_id: update.database_id,
|
||||
connectionString: update.connectionString,
|
||||
});
|
||||
});
|
||||
case "add": {
|
||||
if (!connectionString) continue;
|
||||
const slugifiedId = slugify(database_id);
|
||||
|
||||
return updatedConnections;
|
||||
// Skip if already exists
|
||||
if (connectionsMap.has(slugifiedId)) {
|
||||
console.warn(
|
||||
`[mergeConnections] Add skipped: Connection "${slugifiedId}" already exists`
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
connectionsMap.set(slugifiedId, {
|
||||
engine,
|
||||
database_id: slugifiedId,
|
||||
connectionString,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
throw new Error("SQL connection update contains an invalid action.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(connectionsMap.values());
|
||||
}
|
||||
|
||||
module.exports.SystemSettings = SystemSettings;
|
||||
|
||||
Reference in New Issue
Block a user