mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-08-02 14:01:59 +08:00
feat: implement connection edit (#6)
This commit is contained in:
@ -7,13 +7,11 @@ import Icon from "./Icon";
|
||||
import EngineIcon from "./EngineIcon";
|
||||
import CreateConnectionModal from "./CreateConnectionModal";
|
||||
import SettingModal from "./SettingModal";
|
||||
import ActionConfirmModal, { ActionConfirmModalProps } from "./ActionConfirmModal";
|
||||
import EditChatTitleModal from "./EditChatTitleModal";
|
||||
|
||||
interface State {
|
||||
showCreateConnectionModal: boolean;
|
||||
showSettingModal: boolean;
|
||||
showDeleteConnectionModal: boolean;
|
||||
showEditChatTitleModal: boolean;
|
||||
}
|
||||
|
||||
@ -24,10 +22,9 @@ const ConnectionSidebar = () => {
|
||||
const [state, setState] = useState<State>({
|
||||
showCreateConnectionModal: false,
|
||||
showSettingModal: false,
|
||||
showDeleteConnectionModal: false,
|
||||
showEditChatTitleModal: false,
|
||||
});
|
||||
const [deleteConnectionModalContext, setDeleteConnectionModalContext] = useState<ActionConfirmModalProps>();
|
||||
const [editConnectionModalContext, setEditConnectionModalContext] = useState<Connection>();
|
||||
const [editChatTitleModalContext, setEditChatTitleModalContext] = useState<Chat>();
|
||||
const connectionList = connectionStore.connectionList;
|
||||
const currentConnectionCtx = connectionStore.currentConnectionCtx;
|
||||
@ -41,6 +38,7 @@ const ConnectionSidebar = () => {
|
||||
...state,
|
||||
showCreateConnectionModal: show,
|
||||
});
|
||||
setEditConnectionModalContext(undefined);
|
||||
};
|
||||
|
||||
const toggleSettingModal = (show = true) => {
|
||||
@ -65,28 +63,12 @@ const ConnectionSidebar = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteConnection = (connection: Connection) => {
|
||||
const handleEditConnection = (connection: Connection) => {
|
||||
setState({
|
||||
...state,
|
||||
showDeleteConnectionModal: true,
|
||||
});
|
||||
setDeleteConnectionModalContext({
|
||||
title: "Delete Connection",
|
||||
content: "Are you sure to delete this connection?",
|
||||
confirmButtonStyle: "btn-error",
|
||||
close: () => {
|
||||
setState({
|
||||
...state,
|
||||
showDeleteConnectionModal: false,
|
||||
});
|
||||
},
|
||||
confirm: () => {
|
||||
connectionStore.clearConnection((item) => item.id !== connection.id);
|
||||
if (currentConnectionCtx?.connection.id === connection.id) {
|
||||
connectionStore.setCurrentConnectionCtx(undefined);
|
||||
}
|
||||
},
|
||||
showCreateConnectionModal: true,
|
||||
});
|
||||
setEditConnectionModalContext(connection);
|
||||
};
|
||||
|
||||
const handleDatabaseNameSelect = async (databaseName: string) => {
|
||||
@ -145,19 +127,19 @@ const ConnectionSidebar = () => {
|
||||
{connectionList.map((connection) => (
|
||||
<button
|
||||
key={connection.id}
|
||||
className={`w-full h-14 rounded-l-lg p-2 mt-2 group ${
|
||||
className={`relative w-full h-14 rounded-l-lg p-2 mt-2 group ${
|
||||
currentConnectionCtx?.connection.id === connection.id && "bg-gray-100 shadow"
|
||||
}`}
|
||||
onClick={() => handleConnectionSelect(connection)}
|
||||
>
|
||||
<span
|
||||
className="absolute -ml-1.5 -mt-1.5 hidden opacity-60 group-hover:block hover:opacity-80"
|
||||
className="absolute right-0.5 -mt-1.5 opacity-60 hidden group-hover:block hover:opacity-80"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDeleteConnection(connection);
|
||||
handleEditConnection(connection);
|
||||
}}
|
||||
>
|
||||
<Icon.IoClose className="w-4 h-auto" />
|
||||
<Icon.FiEdit3 className="w-3.5 h-auto" />
|
||||
</span>
|
||||
<EngineIcon engine={connection.engineType} className="w-auto h-full mx-auto" />
|
||||
</button>
|
||||
@ -239,7 +221,7 @@ const ConnectionSidebar = () => {
|
||||
New Chat
|
||||
</button>
|
||||
</div>
|
||||
<div className="sticky bottom-0 w-full flex justify-center bg-gray-100 backdrop-blur bg-opacity-60 pb-4 py-2">
|
||||
<div className="sticky bottom-0 w-full flex justify-center bg-gray-100 backdrop-blur bg-opacity-60 pb-6 py-2">
|
||||
<a
|
||||
href="https://discord.com/invite/huyw7gRsyA"
|
||||
className="text-indigo-600 text-sm font-medium flex flex-row justify-center items-center hover:underline"
|
||||
@ -254,24 +236,16 @@ const ConnectionSidebar = () => {
|
||||
</aside>
|
||||
|
||||
{createPortal(
|
||||
<CreateConnectionModal show={state.showCreateConnectionModal} close={() => toggleCreateConnectionModal(false)} />,
|
||||
<CreateConnectionModal
|
||||
show={state.showCreateConnectionModal}
|
||||
connection={editConnectionModalContext}
|
||||
close={() => toggleCreateConnectionModal(false)}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{createPortal(<SettingModal show={state.showSettingModal} close={() => toggleSettingModal(false)} />, document.body)}
|
||||
|
||||
{state.showDeleteConnectionModal &&
|
||||
createPortal(
|
||||
<ActionConfirmModal
|
||||
title={deleteConnectionModalContext?.title ?? ""}
|
||||
content={deleteConnectionModalContext?.content ?? ""}
|
||||
confirmButtonStyle={deleteConnectionModalContext?.confirmButtonStyle ?? ""}
|
||||
close={deleteConnectionModalContext?.close ?? (() => {})}
|
||||
confirm={deleteConnectionModalContext?.confirm ?? (() => {})}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
|
||||
{state.showEditChatTitleModal &&
|
||||
editChatTitleModalContext &&
|
||||
createPortal(<EditChatTitleModal close={() => toggleEditChatTitleModal(false)} chat={editChatTitleModalContext} />, document.body)}
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { cloneDeep, head } from "lodash-es";
|
||||
import { useEffect, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { toast } from "react-hot-toast";
|
||||
import { testConnection, useConnectionStore } from "@/store";
|
||||
import { Connection, Engine } from "@/types";
|
||||
import Icon from "./Icon";
|
||||
import DataStorageBanner from "./DataStorageBanner";
|
||||
import ActionConfirmModal from "./ActionConfirmModal";
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
connection?: Connection;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
@ -22,15 +25,17 @@ const defaultConnection: Connection = {
|
||||
};
|
||||
|
||||
const CreateConnectionModal = (props: Props) => {
|
||||
const { show, close } = props;
|
||||
const { show, connection: editConnection, close } = props;
|
||||
const connectionStore = useConnectionStore();
|
||||
const [connection, setConnection] = useState<Connection>(defaultConnection);
|
||||
const [showDeleteConnectionModal, setShowDeleteConnectionModal] = useState(false);
|
||||
const [isRequesting, setIsRequesting] = useState(false);
|
||||
const showDatabaseField = connection.engineType === Engine.PostgreSQL;
|
||||
const isEditing = editConnection !== undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (show) {
|
||||
setConnection(defaultConnection);
|
||||
setConnection(isEditing ? editConnection : defaultConnection);
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
@ -47,22 +52,32 @@ const CreateConnectionModal = (props: Props) => {
|
||||
}
|
||||
|
||||
setIsRequesting(true);
|
||||
const connectionCreate = cloneDeep(connection);
|
||||
const tempConnection = cloneDeep(connection);
|
||||
if (!showDatabaseField) {
|
||||
connectionCreate.database = undefined;
|
||||
tempConnection.database = undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await testConnection(connectionCreate);
|
||||
if (!result) {
|
||||
setIsRequesting(false);
|
||||
toast.error("Failed to test connection");
|
||||
return;
|
||||
await testConnection(tempConnection);
|
||||
} catch (error) {
|
||||
setIsRequesting(false);
|
||||
toast.error("Failed to test connection, please check your connection settings");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let connection: Connection;
|
||||
if (isEditing) {
|
||||
connectionStore.updateConnection(tempConnection.id, tempConnection);
|
||||
connection = tempConnection;
|
||||
} else {
|
||||
connection = connectionStore.createConnection(tempConnection);
|
||||
}
|
||||
const createdConnection = connectionStore.createConnection(connectionCreate);
|
||||
|
||||
// Set the created connection as the current connection.
|
||||
const databaseList = await connectionStore.getOrFetchDatabaseList(createdConnection);
|
||||
const databaseList = await connectionStore.getOrFetchDatabaseList(connection, true);
|
||||
connectionStore.setCurrentConnectionCtx({
|
||||
connection: createdConnection,
|
||||
connection: connection,
|
||||
database: head(databaseList),
|
||||
});
|
||||
} catch (error) {
|
||||
@ -76,90 +91,121 @@ const CreateConnectionModal = (props: Props) => {
|
||||
close();
|
||||
};
|
||||
|
||||
const handleDeleteConnection = () => {
|
||||
connectionStore.clearConnection((item) => item.id !== connection.id);
|
||||
if (connectionStore.currentConnectionCtx?.connection.id === connection.id) {
|
||||
connectionStore.setCurrentConnectionCtx(undefined);
|
||||
}
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`modal modal-middle ${show && "modal-open"}`}>
|
||||
<div className="modal-box relative">
|
||||
<h3 className="font-bold text-lg">Create Connection</h3>
|
||||
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
|
||||
<Icon.IoMdClose className="w-5 h-auto" />
|
||||
</button>
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
|
||||
<DataStorageBanner className="rounded-lg bg-white border py-2" alwaysShow={true} />
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={connection.engineType}
|
||||
onChange={(e) => setPartialConnection({ engineType: e.target.value as Engine })}
|
||||
>
|
||||
<option value={Engine.MySQL}>MySQL</option>
|
||||
<option value={Engine.PostgreSQL}>PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Host</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect host"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.host}
|
||||
onChange={(e) => setPartialConnection({ host: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Port</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect port"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.port}
|
||||
onChange={(e) => setPartialConnection({ port: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect username"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.username}
|
||||
onChange={(e) => setPartialConnection({ username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect password"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.password}
|
||||
onChange={(e) => setPartialConnection({ password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{showDatabaseField && (
|
||||
<>
|
||||
<div className={`modal modal-middle ${show && "modal-open"}`}>
|
||||
<div className="modal-box relative">
|
||||
<h3 className="font-bold text-lg">{isEditing ? "Edit Connection" : "Create Connection"}</h3>
|
||||
<button className="btn btn-sm btn-circle absolute right-4 top-4" onClick={close}>
|
||||
<Icon.IoMdClose className="w-5 h-auto" />
|
||||
</button>
|
||||
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
|
||||
<DataStorageBanner className="rounded-lg bg-white border py-2" alwaysShow={true} />
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Database Name</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label>
|
||||
<select
|
||||
className="select select-bordered w-full"
|
||||
value={connection.engineType}
|
||||
onChange={(e) => setPartialConnection({ engineType: e.target.value as Engine })}
|
||||
>
|
||||
<option value={Engine.MySQL}>MySQL</option>
|
||||
<option value={Engine.PostgreSQL}>PostgreSQL</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Host</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect database"
|
||||
placeholder="Connect host"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.database}
|
||||
onChange={(e) => setPartialConnection({ database: e.target.value })}
|
||||
value={connection.host}
|
||||
onChange={(e) => setPartialConnection({ host: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="modal-action">
|
||||
<button className="btn btn-outline" onClick={close}>
|
||||
Close
|
||||
</button>
|
||||
<button className="btn" disabled={isRequesting} onClick={handleCreateConnection}>
|
||||
{isRequesting && <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />}
|
||||
Save
|
||||
</button>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Port</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect port"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.port}
|
||||
onChange={(e) => setPartialConnection({ port: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
{showDatabaseField && (
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Database Name</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect database"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.database}
|
||||
onChange={(e) => setPartialConnection({ database: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect username"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.username}
|
||||
onChange={(e) => setPartialConnection({ username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full flex flex-col">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Connect password"
|
||||
className="input input-bordered w-full"
|
||||
value={connection.password}
|
||||
onChange={(e) => setPartialConnection({ password: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-action w-full flex flex-row justify-between items-center space-x-2">
|
||||
<div>
|
||||
{isEditing && (
|
||||
<button className="btn btn-ghost" onClick={() => setShowDeleteConnectionModal(true)}>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-x-2 flex flex-row justify-center">
|
||||
<button className="btn btn-outline" onClick={close}>
|
||||
Close
|
||||
</button>
|
||||
<button className="btn" disabled={isRequesting} onClick={handleCreateConnection}>
|
||||
{isRequesting && <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" />}
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showDeleteConnectionModal &&
|
||||
createPortal(
|
||||
<ActionConfirmModal
|
||||
title="Delete Connection"
|
||||
content="Are you sure you want to delete this connection?"
|
||||
confirmButtonStyle="btn-error"
|
||||
close={() => setShowDeleteConnectionModal(false)}
|
||||
confirm={() => handleDeleteConnection()}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user