mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-09-28 02:24:49 +08:00
feat: to select table directly in the sidebar (#106)
This commit is contained in:
@ -7,12 +7,11 @@ import useLoading from "@/hooks/useLoading";
|
||||
import Select from "./kit/Select";
|
||||
import Icon from "./Icon";
|
||||
import DarkModeSwitch from "./DarkModeSwitch";
|
||||
import ConversationList from "./Sidebar/ConversationList";
|
||||
import ConnectionList from "./Sidebar/ConnectionList";
|
||||
import QuotaView from "./QuotaView";
|
||||
import { hasFeature } from "../utils";
|
||||
import MultipleSelect from "./kit/MultipleSelect";
|
||||
import { countTextTokens, hasFeature } from "../utils";
|
||||
import SettingAvatarIcon from "./SettingAvatarIcon";
|
||||
import Checkbox from "./kit/Checkbox";
|
||||
|
||||
interface State {}
|
||||
|
||||
@ -34,6 +33,12 @@ const ConnectionSidebar = () => {
|
||||
conversationStore.getConversationById(conversationStore.currentConversationId)?.selectedSchemaName || "";
|
||||
const tableSchemaLoadingState = useLoading();
|
||||
const currentConversation = conversationStore.getConversationById(conversationStore.currentConversationId);
|
||||
const [totalToken, setTotalToken] = useState<number>(0);
|
||||
useEffect(() => {
|
||||
updateHasSchemaProperty(
|
||||
currentConnectionCtx?.connection.engineType === Engine.PostgreSQL || currentConnectionCtx?.connection.engineType === Engine.MSSQL
|
||||
);
|
||||
}, [currentConnectionCtx?.connection]);
|
||||
|
||||
useEffect(() => {
|
||||
updateHasSchemaProperty(
|
||||
@ -60,6 +65,16 @@ const ConnectionSidebar = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// update total token
|
||||
const totalToken = selectedTablesName.reduce((totalToken, tableName) => {
|
||||
const table = tableList.find((table) => table.name === tableName);
|
||||
// because old cache didn't have token, So the value may is undefined.
|
||||
return totalToken + (table?.token || countTextTokens(table?.structure || ""));
|
||||
}, 0);
|
||||
setTotalToken(totalToken);
|
||||
}, [selectedTablesName, tableList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentConnectionCtx?.connection) {
|
||||
setIsRequestingDatabase(true);
|
||||
@ -134,16 +149,12 @@ const ConnectionSidebar = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTableNameSelect = async (selectedTablesName: string[]) => {
|
||||
conversationStore.updateSelectedTablesName(selectedTablesName);
|
||||
};
|
||||
|
||||
const handleAllSelect = async () => {
|
||||
conversationStore.updateSelectedTablesName(tableList.map((table) => table.name));
|
||||
};
|
||||
|
||||
const handleEmptySelect = async () => {
|
||||
conversationStore.updateSelectedTablesName([]);
|
||||
const handleTableCheckboxChange = async (tableName: string, value: boolean) => {
|
||||
if (value) {
|
||||
conversationStore.updateSelectedTablesName([...selectedTablesName, tableName]);
|
||||
} else {
|
||||
conversationStore.updateSelectedTablesName(selectedTablesName.filter((name) => name !== tableName));
|
||||
}
|
||||
};
|
||||
|
||||
const handleSchemaNameSelect = async (schemaName: string) => {
|
||||
@ -211,42 +222,31 @@ const ConnectionSidebar = () => {
|
||||
)}
|
||||
{currentConnectionCtx &&
|
||||
(tableSchemaLoadingState.isLoading ? (
|
||||
<div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 border z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600 dark:text-gray-400">
|
||||
<div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600 dark:text-gray-400">
|
||||
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> {t("common.loading")}
|
||||
</div>
|
||||
) : (
|
||||
tableList.length > 0 && (
|
||||
<div className="w-full sticky top-0 z-1 my-4">
|
||||
<MultipleSelect
|
||||
className="w-full px-4 py-3 !text-base"
|
||||
value={selectedTablesName}
|
||||
itemList={tableList.map((table) => {
|
||||
return {
|
||||
label: table.name === "" ? t("connection.all-tables") : table.name,
|
||||
value: table.name,
|
||||
};
|
||||
})}
|
||||
onValueChange={(tableName) => handleTableNameSelect(tableName)}
|
||||
placeholder={(selectedTablesName.length ? selectedTablesName.join(",") : t("connection.all-tables")) || ""}
|
||||
>
|
||||
<button
|
||||
className="whitespace-nowrap rounded w-full bg-indigo-600 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-indigo-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||
onClick={(e) => {
|
||||
selectedTablesName.length ? handleEmptySelect() : handleAllSelect();
|
||||
// The Button area is a option that have select event. So must to stop Propagation
|
||||
e.stopPropagation();
|
||||
}}
|
||||
tableList.length > 0 &&
|
||||
tableList.map((table) => {
|
||||
return (
|
||||
<div key={table.name}>
|
||||
<Checkbox
|
||||
value={selectedTablesName.includes(table.name)}
|
||||
label={table.name}
|
||||
onValueChange={handleTableCheckboxChange}
|
||||
>
|
||||
{selectedTablesName.length ? t("connection.empty-select") : t("connection.select-all")}
|
||||
</button>
|
||||
</MultipleSelect>
|
||||
</div>
|
||||
)
|
||||
<div className="text-black dark:text-gray-300">{table.token || countTextTokens(table.structure)}</div>
|
||||
</Checkbox>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
))}
|
||||
{/* TODO(steven): remove this after we finish left sidebar */}
|
||||
<ConversationList />
|
||||
</div>
|
||||
|
||||
<div className="sticky bottom-0 w-full flex flex-col justify-center bg-gray-100 dark:bg-zinc-700 backdrop-blur bg-opacity-60 pb-4 py-2">
|
||||
<div className="text-black dark:text-gray-300">
|
||||
{t("connection.total-token")} {totalToken}
|
||||
</div>
|
||||
{!settingStore.setting.openAIApiConfig?.key && hasFeature("quota") && (
|
||||
<div className="mb-4">
|
||||
<QuotaView />
|
||||
|
34
src/components/kit/Checkbox.tsx
Normal file
34
src/components/kit/Checkbox.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import * as CheckboxUI from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon, DividerHorizontalIcon } from "@radix-ui/react-icons";
|
||||
import { ReactNode } from "react";
|
||||
|
||||
interface CheckboxProps {
|
||||
value: boolean;
|
||||
label: string;
|
||||
onValueChange: (tableName: string, value: boolean) => void;
|
||||
}
|
||||
const Checkbox = (props: CheckboxProps & { children?: ReactNode }) => {
|
||||
const { value, label, onValueChange, children } = props;
|
||||
return (
|
||||
<form>
|
||||
<div className="flex justify-between items-center px-3 py-2">
|
||||
<CheckboxUI.Root
|
||||
checked={value}
|
||||
onCheckedChange={(value: boolean) => onValueChange(label, value)}
|
||||
className="bg-white w-5 h-5 shrink-0 cursor-pointer rounded-sm flex border border-gray-300 hover:border-black m-auto"
|
||||
id={label}
|
||||
>
|
||||
<CheckboxUI.Indicator className="m-auto text-black">
|
||||
<CheckIcon />
|
||||
</CheckboxUI.Indicator>
|
||||
</CheckboxUI.Root>
|
||||
<label className="Label grow m-auto px-3 py-2 cursor-pointer truncate text-black dark:text-gray-300" htmlFor={label}>
|
||||
{label}
|
||||
</label>
|
||||
{children}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default Checkbox;
|
@ -1,59 +0,0 @@
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { Fragment, ReactNode, useState } from "react";
|
||||
import * as SelectUI from "@radix-ui/react-select";
|
||||
import Icon from "../Icon";
|
||||
|
||||
interface Props<T = any> {
|
||||
value: T[];
|
||||
itemList: {
|
||||
value: T;
|
||||
label: string;
|
||||
}[];
|
||||
className?: string;
|
||||
placeholder?: string;
|
||||
selectedPlaceholder?: string;
|
||||
onValueChange: (value: T) => void;
|
||||
}
|
||||
|
||||
const MultipleSelect = (props: Props & { children?: ReactNode }) => {
|
||||
const { itemList, value, placeholder, className, onValueChange, children } = props;
|
||||
return (
|
||||
<Listbox value={value} onChange={onValueChange} multiple>
|
||||
<Listbox.Button
|
||||
className={`${
|
||||
className || ""
|
||||
} flex flex-row justify-between items-center text-sm whitespace-nowrap dark:text-gray-300 bg-white dark:bg-zinc-700 border dark:border-zinc-800 px-3 py-2 rounded-lg`}
|
||||
>
|
||||
<div className="truncate">{placeholder}</div>
|
||||
<SelectUI.Icon className="ml-1 w-5 h-auto shrink-0">
|
||||
<Icon.BiChevronDown className="w-full h-auto opacity-60" />
|
||||
</SelectUI.Icon>
|
||||
</Listbox.Button>
|
||||
<Transition as={Fragment} leave="transition ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0">
|
||||
<Listbox.Options className="absolute border rounded-lg drop-shadow-lg dark:border-zinc-800 p-1 mt-1 max-h-80 overflow-y-auto scrollbar-hide w-full overflow-auto bg-white dark:bg-zinc-700 py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm">
|
||||
{children && (
|
||||
<Listbox.Option className="px-2 py-2" key="button" value="button">
|
||||
{children}
|
||||
</Listbox.Option>
|
||||
)}
|
||||
|
||||
{itemList.map((item) => (
|
||||
<Listbox.Option
|
||||
className="w-full px-3 py-2 whitespace-nowrap truncate text-ellipsis overflow-x-hidden text-sm rounded-lg flex flex-row justify-between items-center cursor-pointer dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-zinc-800"
|
||||
key={item.value}
|
||||
value={item.value}
|
||||
>
|
||||
<div className="truncate">{item.label}</div>
|
||||
{(value.find((v: string) => v === item.value) ? true : false) ? (
|
||||
<span className="w-5 h-auto">
|
||||
<Icon.BiCheck className="w-full h-auto" aria-hidden="true" />
|
||||
</span>
|
||||
) : null}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</Listbox>
|
||||
);
|
||||
};
|
||||
export default MultipleSelect;
|
Reference in New Issue
Block a user