From aa3f41d5fb3cd99ec18ad5c48de294910310df28 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Fri, 19 May 2023 23:11:15 +0800 Subject: [PATCH 01/17] chore: disable account, payment, quota for database migration --- src/utils/feature.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/feature.ts b/src/utils/feature.ts index 28ce67f..be48f74 100644 --- a/src/utils/feature.ts +++ b/src/utils/feature.ts @@ -9,9 +9,9 @@ const matrix: { [key: string]: { [feature: string]: boolean } } = { }, production: { debug: false, - account: true, - payment: true, - quota: true, + account: false, + payment: false, + quota: false, }, }; From 1a86bc4d7ffe05273a3a021e2c9306ea4bf6d9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tianzhou=20=28=E5=A4=A9=E8=88=9F=29?= Date: Fri, 19 May 2023 23:26:11 +0800 Subject: [PATCH 02/17] feat: use connection pooling (#102) --- prisma/schema.prisma | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 764afb7..adf9174 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -10,8 +10,11 @@ generator client { } datasource db { - provider = "postgresql" - url = env("DATABASE_URL") + provider = "postgresql" + // Connection pooling URL + url = env("DATABASE_URL") + // Connection pooling is not supported by Prisma migrate yet. So need to use directUrl + directUrl = env("DATABASE_DIRECT_URL") } model Chat { From d0c6ea48e37006b37ff018e59d17d5a7e47a2563 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Fri, 19 May 2023 23:26:57 +0800 Subject: [PATCH 03/17] chore: reenable feature after db migration --- src/utils/feature.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/feature.ts b/src/utils/feature.ts index be48f74..28ce67f 100644 --- a/src/utils/feature.ts +++ b/src/utils/feature.ts @@ -9,9 +9,9 @@ const matrix: { [key: string]: { [feature: string]: boolean } } = { }, production: { debug: false, - account: false, - payment: false, - quota: false, + account: true, + payment: true, + quota: true, }, }; From 822812fb7cb573e9add6651ffac2369d4f43f725 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Fri, 19 May 2023 23:57:03 +0800 Subject: [PATCH 04/17] chore: only show schema drawer icon in debug --- src/components/ConversationView/Header.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/ConversationView/Header.tsx b/src/components/ConversationView/Header.tsx index 8b190f5..8145bc2 100644 --- a/src/components/ConversationView/Header.tsx +++ b/src/components/ConversationView/Header.tsx @@ -49,9 +49,11 @@ const Header = (props: Props) => { > - + {hasFeature("debug") && ( + + )} From cda4fd3f2293850757b53c3f86e5bf93b6a71b48 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sat, 20 May 2023 00:05:45 +0800 Subject: [PATCH 05/17] =?UTF-8?q?chore:=20enable=20wechat=20and=20alipay?= =?UTF-8?q?=20as=20it=E2=80=99s=20approved?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/api/checkout_sessions/index.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pages/api/checkout_sessions/index.ts b/src/pages/api/checkout_sessions/index.ts index e6f045b..22f4c9a 100644 --- a/src/pages/api/checkout_sessions/index.ts +++ b/src/pages/api/checkout_sessions/index.ts @@ -30,15 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) const params: Stripe.Checkout.SessionCreateParams = { submit_type: "pay", mode: "payment", - payment_method_types: [ - "affirm", - // "alipay", - "card", - "cashapp", - "klarna", - "link", - // "wechat_pay", - ], + payment_method_types: ["affirm", "alipay", "card", "cashapp", "klarna", "link", "wechat_pay"], line_items: [ { price: process.env.STRIPE_PRICE_ID_PRO_1_YEAR_SUBSCRIPTION, From 8b2b10b13dbd5cb3c23ed4bfa501d6913617f89c Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sat, 20 May 2023 23:26:44 +0800 Subject: [PATCH 06/17] feat: allow user to use gpt-4 --- src/components/ConversationView/index.tsx | 5 +- src/components/OpenAIApiConfigView.tsx | 66 ++++++++++++++++++++--- src/components/kit/Radio.tsx | 39 ++++++++++++++ src/locales/en.json | 10 +++- src/locales/es.json | 10 +++- src/locales/zh.json | 10 +++- src/pages/api/chat.ts | 11 ++-- src/pages/api/collect.ts | 4 +- src/store/setting.ts | 1 + src/types/setting.ts | 1 + src/utils/model.ts | 20 ++++++- 11 files changed, 156 insertions(+), 21 deletions(-) create mode 100644 src/components/kit/Radio.tsx diff --git a/src/components/ConversationView/index.tsx b/src/components/ConversationView/index.tsx index e2e5e9d..39e6979 100644 --- a/src/components/ConversationView/index.tsx +++ b/src/components/ConversationView/index.tsx @@ -217,6 +217,9 @@ const ConversationView = () => { if (settingStore.setting.openAIApiConfig?.endpoint) { requestHeaders["x-openai-endpoint"] = settingStore.setting.openAIApiConfig?.endpoint; } + if (settingStore.setting.openAIApiConfig?.model) { + requestHeaders["x-openai-model"] = settingStore.setting.openAIApiConfig?.model; + } const rawRes = await fetch("/api/chat", { method: "POST", body: JSON.stringify({ @@ -311,7 +314,7 @@ const ConversationView = () => { messages: usageMessageList, }, { - headers: session?.user.id ? { Authorization: `Bearer ${session?.user.id}` } : undefined, + headers: requestHeaders, } ) .catch(() => { diff --git a/src/components/OpenAIApiConfigView.tsx b/src/components/OpenAIApiConfigView.tsx index 9c875a9..a70ae9a 100644 --- a/src/components/OpenAIApiConfigView.tsx +++ b/src/components/OpenAIApiConfigView.tsx @@ -3,14 +3,31 @@ import { useTranslation } from "react-i18next"; import { useDebounce } from "react-use"; import { useSettingStore } from "@/store"; import { OpenAIApiConfig } from "@/types"; +import Radio from "./kit/Radio"; import TextField from "./kit/TextField"; - +import Tooltip from "./kit/Tooltip"; const OpenAIApiConfigView = () => { const { t } = useTranslation(); const settingStore = useSettingStore(); const [openAIApiConfig, setOpenAIApiConfig] = useState(settingStore.setting.openAIApiConfig); const [maskKey, setMaskKey] = useState(true); + const models = [ + { + id: "gpt-3.5-turbo", + title: `GPT-3.5 (${t("setting.openai-api-configuration.quota-per-ask", { count: 1 })})`, + disabled: false, + tooltip: "", + }, + // Disable GPT-4 if user doesn't provide key (because SQL Chat own key hasn't been whitelisted yet). + { + id: "gpt-4", + title: `GPT-4 (${t("setting.openai-api-configuration.quota-per-ask", { count: 10 })})`, + disabled: !settingStore.setting.openAIApiConfig.key, + tooltip: t("setting.openai-api-configuration.provide-gpt4-key"), + }, + ]; + const maskedKey = (str: string) => { if (str.length < 7) { return str; @@ -37,21 +54,58 @@ const OpenAIApiConfigView = () => { setMaskKey(false); }; + const modelRadio = (model: any) => { + return ( +
+ handleSetOpenAIApiConfig({ model: value })} + /> + +
+ ); + }; + return ( <>
-
- +
+ +

{t("setting.openai-api-configuration.model-description")}

+
+
+ {models.map((model) => + model.disabled ? ( + + {modelRadio(model)} + + ) : ( + modelRadio(model) + ) + )} +
+
+
+
+ +

{t("setting.openai-api-configuration.key-description")}

handleSetOpenAIApiConfig({ key: value })} />
-
- +
+ +

{t("setting.openai-api-configuration.endpoint-description")}

handleSetOpenAIApiConfig({ endpoint: value })} /> diff --git a/src/components/kit/Radio.tsx b/src/components/kit/Radio.tsx new file mode 100644 index 0000000..73a5f48 --- /dev/null +++ b/src/components/kit/Radio.tsx @@ -0,0 +1,39 @@ +import { HTMLInputTypeAttribute } from "react"; + +interface Props { + value: string; + onChange?: (value: string) => void; + type?: HTMLInputTypeAttribute; + className?: string; + disabled?: boolean; + checked?: boolean; +} + +const getDefaultProps = () => ({ + value: "", + onChange: () => {}, + type: "radio", + className: "", + disabled: false, + checked: false, +}); + +const Radio = (props: Props) => { + const { value, disabled, className, type, checked, onChange } = { + ...getDefaultProps(), + ...props, + }; + + return ( + onChange(e.target.value)} + /> + ); +}; + +export default Radio; diff --git a/src/locales/en.json b/src/locales/en.json index 8a5d334..41dd2b9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -68,7 +68,7 @@ "upgrade": "Upgrade", "renew": "Renew", "expired": "Expired", - "n-question-per-month": "{{count}} questions / month", + "n-question-per-month": "{{count}} quota / month", "early-bird-checkout": "Early bird discount, 50% off for 1 year" }, "billing": { @@ -89,7 +89,13 @@ "dark": "Dark" }, "openai-api-configuration": { - "self": "OpenAI API configuration" + "self": "OpenAI API configuration", + "model": "Model", + "model-description": "Quota won't be consumed if you provide your own key below.", + "quota-per-ask": "{{count}} quata per ask", + "provide-gpt4-key": "Require your own GPT-4 enabled API key", + "key-description": "Bring your own key to waive quota requirement.", + "endpoint-description": "Optional endpoint pointing to your own compatible server or gateway." }, "data": { "self": "Data", diff --git a/src/locales/es.json b/src/locales/es.json index d15cd9e..0ba472b 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -66,7 +66,7 @@ "upgrade": "Mejora", "renew": "Renovar", "expired": "Expirado", - "n-question-per-month": "{{count}} preguntas / mes", + "n-question-per-month": "{{count}} Cuota / mes", "early-bird-checkout": "Descuento por reserva anticipada, 50 % de descuento durante 1 año" }, "billing": { @@ -87,7 +87,13 @@ "dark": "Oscuro" }, "openai-api-configuration": { - "self": "Configuración del API de OpenAI" + "self": "Configuración del API de OpenAI", + "model": "Modelo", + "model-description": "La cuota no se consumirá si proporciona su propia clave a continuación.", + "quota-per-ask": "{{count}} cuotas por pedido", + "provide-gpt4-key": "Requerir su propia clave API habilitada para GPT-4", + "key-description": "Traiga su propia llave para renunciar al requisito de cuota.", + "endpoint-description": "Punto final opcional que apunta a su propio servidor o puerta de enlace compatible." }, "data": { "self": "Datos", diff --git a/src/locales/zh.json b/src/locales/zh.json index df87cc5..51c182a 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -68,7 +68,7 @@ "upgrade": "升级", "renew": "续费", "expired": "已过期", - "n-question-per-month": "{{count}} 次提问 / 月", + "n-question-per-month": "{{count}} 点额度 / 月", "early-bird-checkout": "早鸟优惠,5 折购买 1 年" }, "billing": { @@ -89,7 +89,13 @@ "dark": "深色" }, "openai-api-configuration": { - "self": "OpenAI API 配置" + "self": "OpenAI API 配置", + "model": "模型", + "model-description": "如果您提供自己的 key,额度是不会消耗的。", + "quota-per-ask": "每一个提问消耗 {{count}} 点额度", + "provide-gpt4-key": "需提供您自己的,可以使用 GPT-4 的 key", + "key-description": "一旦您提供了自己的 key,额度就不受限制了。", + "endpoint-description": "可选的 endpoint 指向接口兼容的服务器或者网关。" }, "data": { "self": "数据", diff --git a/src/pages/api/chat.ts b/src/pages/api/chat.ts index 3decfd0..0b5cf43 100644 --- a/src/pages/api/chat.ts +++ b/src/pages/api/chat.ts @@ -1,6 +1,6 @@ import { createParser, ParsedEvent, ReconnectInterval } from "eventsource-parser"; import { NextRequest } from "next/server"; -import { openAIApiEndpoint, openAIApiKey, gpt35, hasFeature } from "@/utils"; +import { openAIApiEndpoint, openAIApiKey, hasFeature, getModel } from "@/utils"; // Needs Edge for streaming response. export const config = { @@ -90,6 +90,7 @@ const handler = async (req: NextRequest) => { } const apiEndpoint = getApiEndpoint(req.headers.get("x-openai-endpoint") || openAIApiEndpoint); + const model = getModel(req.headers.get("x-openai-model") || ""); const remoteRes = await fetch(apiEndpoint, { headers: { "Content-Type": "application/json", @@ -97,11 +98,11 @@ const handler = async (req: NextRequest) => { }, method: "POST", body: JSON.stringify({ - model: gpt35.name, + model: model.name, messages: reqBody.messages, - temperature: gpt35.temperature, - frequency_penalty: gpt35.frequency_penalty, - presence_penalty: gpt35.presence_penalty, + temperature: model.temperature, + frequency_penalty: model.frequency_penalty, + presence_penalty: model.presence_penalty, stream: true, // Send end-user ID to help OpenAI monitor and detect abuse. user: req.ip, diff --git a/src/pages/api/collect.ts b/src/pages/api/collect.ts index f0d5647..1e96795 100644 --- a/src/pages/api/collect.ts +++ b/src/pages/api/collect.ts @@ -1,7 +1,7 @@ import { PrismaClient } from "@prisma/client"; import { NextApiRequest, NextApiResponse } from "next"; import { Conversation, Message } from "@/types"; -import { gpt35 } from "@/utils"; +import { getModel, gpt35 } from "@/utils"; import { getEndUser } from "./auth/end-user"; const prisma = new PrismaClient(); @@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data: { id: conversation.id, createdAt: new Date(conversation.createdAt), - model: gpt35, + model: getModel((req.headers["x-openai-model"] as string) || ""), ctx: {}, messages: { create: messages.map((message) => ({ diff --git a/src/store/setting.ts b/src/store/setting.ts index 7086e4c..b51abaa 100644 --- a/src/store/setting.ts +++ b/src/store/setting.ts @@ -10,6 +10,7 @@ const getDefaultSetting = (): Setting => { openAIApiConfig: { key: "", endpoint: "", + model: "gpt-3.5-turbo", }, }; }; diff --git a/src/types/setting.ts b/src/types/setting.ts index 636a85f..886dbf2 100644 --- a/src/types/setting.ts +++ b/src/types/setting.ts @@ -5,6 +5,7 @@ export type Theme = "light" | "dark" | "system"; export interface OpenAIApiConfig { key: string; endpoint: string; + model: string; } export interface Setting { diff --git a/src/utils/model.ts b/src/utils/model.ts index af555a4..0764f11 100644 --- a/src/utils/model.ts +++ b/src/utils/model.ts @@ -1,6 +1,24 @@ -export const gpt35 = { +const gpt35 = { name: "gpt-3.5-turbo", temperature: 0, frequency_penalty: 0.0, presence_penalty: 0.0, }; + +const gpt4 = { + name: "gpt-4", + temperature: 0, + frequency_penalty: 0.0, + presence_penalty: 0.0, +}; + +export const models = [gpt35, gpt4]; + +export const getModel = (name: string) => { + for (const model of models) { + if (model.name === name) { + return model; + } + } + return gpt35; +}; From 353f5bdd8b6cae600f824c6c0343d32ef751d2dd Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 01:32:41 +0800 Subject: [PATCH 07/17] chore: add privacy policy --- README.md | 6 +--- public/privacy.md | 46 +++++++++++++++++++++++++++++ src/components/MarkdownRenderer.tsx | 33 +++++++++++++++++++++ src/components/SettingView.tsx | 8 +++++ src/pages/api/chat.ts | 3 +- src/pages/privacy/index.tsx | 8 +++++ 6 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 public/privacy.md create mode 100644 src/components/MarkdownRenderer.tsx create mode 100644 src/pages/privacy/index.tsx diff --git a/README.md b/README.md index 160a822..33c9bc3 100644 --- a/README.md +++ b/README.md @@ -34,11 +34,7 @@ SQL Chat is built by [Next.js](https://nextjs.org/), it supports the following d ## Data Privacy -- All database connection configs are stored locally in your browser. You can also visit settings to clear the data. - -- Only the database schema will be sent to the OpenAI API. No table data will be sent there. - -- If you use [sqlchat.ai](https://sqlchat.ai), it will record the anonymised conversations. +See [SQL Chat Privacy Policy](https://sqlchat.ai/privacy). ## IP Whitelisting diff --git a/public/privacy.md b/public/privacy.md new file mode 100644 index 0000000..76a01fd --- /dev/null +++ b/public/privacy.md @@ -0,0 +1,46 @@ +# SQL Chat Privacy Policy + +We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, and disclose your personal information when you use our application that calls LLM provider's API. + +## Collection of Application Data + +* All database connection configs are stored locally in your browser. You can also visit settings to clear the data. +* We send the database schema to the LLM provider. No table data will be sent there. +* We collect the conversation message to improve the product. + +## Collection of Personal Information + +We may collect certain personal information from you when you use our application, such as your name and email address. We collect this information in order to communicate with you about our services and to provide you with technical support. + +## Use of Personal Information + +We may use your personal information to: + +* Communicate with you about our services +* Provide you with technical support +* Improve our application and services +* Comply with legal obligations + +## Disclosure of Personal Information + +We do not sell, trade, or otherwise transfer your personal information to third parties. However, we may disclose your personal information in the following circumstances: + +* To our service providers who assist us in providing our services to you +* To comply with legal obligations or to protect our rights or the rights of others + +## Security of Personal Information + +We take reasonable measures to protect your personal information from unauthorized access, disclosure, or destruction. However, no method of transmission over the internet or electronic storage is completely secure, and we cannot guarantee absolute security. + + +## Changes to this Privacy Policy + +We may update this Privacy Policy from time to time by posting a new versionon our website. We encourage you to review this Privacy Policy periodically for any changes. Your continued use of our application and services after any modifications to this Privacy Policy will constitute your acceptance of such modifications. + +For more information about our privacy and data protection practices, including how to exercise your privacy rights, please visit the [Bytebase Privacy Policy](https://bytebase.com/privacy). + +## Contact Us + +Please contact us at support@bytebase.com if you have questions about this Privacy Policy. + +*Last Updated: May 21, 2023* \ No newline at end of file diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx new file mode 100644 index 0000000..7e2445d --- /dev/null +++ b/src/components/MarkdownRenderer.tsx @@ -0,0 +1,33 @@ +import React, { useState, useEffect } from "react"; +import ReactMarkdown from "react-markdown"; + +interface MarkdownRendererProps { + url: string; +} + +function MarkdownRenderer(props: MarkdownRendererProps) { + const [markdown, setMarkdown] = useState(""); + + useEffect(() => { + const fetchData = async () => { + const response = await fetch(props.url); + const data = await response.text(); + setMarkdown(data); + }; + fetchData(); + }, [props.url]); + + return ( +

{children}

, + li: ({ children }) =>
  • {children}
  • , + }} + > + {markdown} +
    + ); +} + +export default MarkdownRenderer; diff --git a/src/components/SettingView.tsx b/src/components/SettingView.tsx index 1b9c21e..9fe67da 100644 --- a/src/components/SettingView.tsx +++ b/src/components/SettingView.tsx @@ -61,6 +61,14 @@ const SettingView = () => {
    + +
    {t("setting.data.clear-all-data")} diff --git a/src/pages/api/chat.ts b/src/pages/api/chat.ts index 0b5cf43..976a647 100644 --- a/src/pages/api/chat.ts +++ b/src/pages/api/chat.ts @@ -104,7 +104,8 @@ const handler = async (req: NextRequest) => { frequency_penalty: model.frequency_penalty, presence_penalty: model.presence_penalty, stream: true, - // Send end-user ID to help OpenAI monitor and detect abuse. + // Send end-user IP to help OpenAI monitor and detect abuse. + // It's intentionally not using email to avoid leaking user's email to OpenAI. user: req.ip, }), }); diff --git a/src/pages/privacy/index.tsx b/src/pages/privacy/index.tsx new file mode 100644 index 0000000..9da2dfd --- /dev/null +++ b/src/pages/privacy/index.tsx @@ -0,0 +1,8 @@ +import MarkdownRenderer from "@/components/MarkdownRenderer"; +import { NextPage } from "next"; + +const PrivacyPage: NextPage = () => { + return ; +}; + +export default PrivacyPage; From 504d938488c474abc6e0b5e92132019fc7efb0c1 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 01:50:49 +0800 Subject: [PATCH 08/17] chore: misc dark mode fix --- src/components/OpenAIApiConfigView.tsx | 6 +++--- src/components/QuotaView.tsx | 2 +- src/pages/setting/index.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/OpenAIApiConfigView.tsx b/src/components/OpenAIApiConfigView.tsx index a70ae9a..b45fac7 100644 --- a/src/components/OpenAIApiConfigView.tsx +++ b/src/components/OpenAIApiConfigView.tsx @@ -74,7 +74,7 @@ const OpenAIApiConfigView = () => { <>
    - +

    {t("setting.openai-api-configuration.model-description")}

    @@ -91,7 +91,7 @@ const OpenAIApiConfigView = () => {
    - +

    {t("setting.openai-api-configuration.key-description")}

    { />
    - +

    {t("setting.openai-api-configuration.endpoint-description")}

    {
    {t("common.quota")}
    -
    = quota.limit ? "text-red-600" : "text-black"}> +
    = quota.limit ? "text-red-600" : "text-black dark:text-gray-300"}> {quota.current}/{quota.limit}
    diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index 2efb8ce..2c0621e 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -174,7 +174,7 @@ const SettingPage: NextPage = () => {
    {router.query.session_id && } -
    +
    From bfb2664b54ee9057fcee1f4dd8b75a036d1d30f2 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 12:34:44 +0800 Subject: [PATCH 09/17] chore: add ToS --- public/privacy.md | 6 +++--- public/terms.md | 12 ++++++++++++ src/components/SettingView.tsx | 9 +++++++-- src/pages/terms/index.tsx | 8 ++++++++ 4 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 public/terms.md create mode 100644 src/pages/terms/index.tsx diff --git a/public/privacy.md b/public/privacy.md index 76a01fd..6b7ed96 100644 --- a/public/privacy.md +++ b/public/privacy.md @@ -1,5 +1,7 @@ # SQL Chat Privacy Policy +*Last Updated: May 21, 2023* + We respect your privacy and are committed to protecting your personal information. This Privacy Policy explains how we collect, use, and disclose your personal information when you use our application that calls LLM provider's API. ## Collection of Application Data @@ -41,6 +43,4 @@ For more information about our privacy and data protection practices, including ## Contact Us -Please contact us at support@bytebase.com if you have questions about this Privacy Policy. - -*Last Updated: May 21, 2023* \ No newline at end of file +Please contact us at support@bytebase.com if you have questions about this Privacy Policy. \ No newline at end of file diff --git a/public/terms.md b/public/terms.md new file mode 100644 index 0000000..39c9383 --- /dev/null +++ b/public/terms.md @@ -0,0 +1,12 @@ +# Terms of Service + +*Last Updated: May 21, 2023* + +## Refund Policy + +All fees and charges are earned upon receipt by us and are nonrefundable. + + +## Contact Us + +Please contact us at support@bytebase.com if you have questions about this Terms of Service. \ No newline at end of file diff --git a/src/components/SettingView.tsx b/src/components/SettingView.tsx index 9fe67da..ee53bfe 100644 --- a/src/components/SettingView.tsx +++ b/src/components/SettingView.tsx @@ -17,7 +17,7 @@ const SettingView = () => { const { data: session } = useSession(); return ( -
    + -
    +
    diff --git a/src/pages/terms/index.tsx b/src/pages/terms/index.tsx new file mode 100644 index 0000000..8eaec0c --- /dev/null +++ b/src/pages/terms/index.tsx @@ -0,0 +1,8 @@ +import MarkdownRenderer from "@/components/MarkdownRenderer"; +import { NextPage } from "next"; + +const PrivacyPage: NextPage = () => { + return ; +}; + +export default PrivacyPage; From f9f909b82c21806a04394244c2779f53c2a57f22 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 13:12:24 +0800 Subject: [PATCH 10/17] chore: use radio instead for dropdown for database type Present type upfront --- src/components/CreateConnectionModal.tsx | 60 ++++++++++++++++++------ src/components/kit/Radio.tsx | 2 +- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/components/CreateConnectionModal.tsx b/src/components/CreateConnectionModal.tsx index 2d61872..e4bb8a4 100644 --- a/src/components/CreateConnectionModal.tsx +++ b/src/components/CreateConnectionModal.tsx @@ -4,13 +4,14 @@ import { toast } from "react-hot-toast"; import TextareaAutosize from "react-textarea-autosize"; import { useConnectionStore } from "@/store"; import { Connection, Engine, ResponseObject, SSLOptions } from "@/types"; -import Select from "./kit/Select"; +import Radio from "./kit/Radio"; import TextField from "./kit/TextField"; import Modal from "./kit/Modal"; import Icon from "./Icon"; import DataStorageBanner from "./DataStorageBanner"; import ActionConfirmModal from "./ActionConfirmModal"; import { useTranslation } from "react-i18next"; +import EngineIcon from "./EngineIcon"; interface Props { connection?: Connection; @@ -43,6 +44,29 @@ const defaultPort = { [Engine.TiDBServerless]: "4000", }; +const engines = [ + { + type: Engine.MySQL, + name: "MySQL", + defualtPort: "3306", + }, + { + type: Engine.PostgreSQL, + name: "PostgreSQL", + defualtPort: "5432", + }, + { + type: Engine.MSSQL, + name: "SQL Server", + defualtPort: "1433", + }, + { + type: Engine.TiDBServerless, + name: "TiDB Serverless", + defualtPort: "4000", + }, +]; + const defaultConnection: Connection = { id: "", title: "", @@ -216,17 +240,24 @@ const CreateConnectionModal = (props: Props) => {
    - setSSLType(e.target.value as SSLType)} + onChange={(value) => setSSLType(value as SSLType)} /> {option.label} diff --git a/src/components/kit/Radio.tsx b/src/components/kit/Radio.tsx index 73a5f48..694c887 100644 --- a/src/components/kit/Radio.tsx +++ b/src/components/kit/Radio.tsx @@ -26,7 +26,7 @@ const Radio = (props: Props) => { return ( Date: Sun, 21 May 2023 13:13:24 +0800 Subject: [PATCH 11/17] chore: missing locale string --- src/components/CreateConnectionModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CreateConnectionModal.tsx b/src/components/CreateConnectionModal.tsx index e4bb8a4..16a55b0 100644 --- a/src/components/CreateConnectionModal.tsx +++ b/src/components/CreateConnectionModal.tsx @@ -396,7 +396,7 @@ const CreateConnectionModal = (props: Props) => {
    {isEditing && ( )}
    From a994013fcd22b8b0fa1c5aa6ea6350b9a4481fbe Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 15:22:02 +0800 Subject: [PATCH 12/17] chore: adjust clickable area for engine type radio --- src/components/CreateConnectionModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CreateConnectionModal.tsx b/src/components/CreateConnectionModal.tsx index 16a55b0..aeea38d 100644 --- a/src/components/CreateConnectionModal.tsx +++ b/src/components/CreateConnectionModal.tsx @@ -244,7 +244,7 @@ const CreateConnectionModal = (props: Props) => { {engines.map((engine) => (
    { onChange={(value) => setPartialConnection({ engineType: value as Engine })} /> -
    From ad31e18ea2708d82f91808566c926e96447f7d46 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 15:22:28 +0800 Subject: [PATCH 13/17] fix: make port field editable --- src/components/CreateConnectionModal.tsx | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/components/CreateConnectionModal.tsx b/src/components/CreateConnectionModal.tsx index aeea38d..3066a3e 100644 --- a/src/components/CreateConnectionModal.tsx +++ b/src/components/CreateConnectionModal.tsx @@ -37,33 +37,26 @@ const SSLTypeOptions = [ }, ]; -const defaultPort = { - [Engine.MySQL]: "3306", - [Engine.PostgreSQL]: "5432", - [Engine.MSSQL]: "1433", - [Engine.TiDBServerless]: "4000", -}; - const engines = [ { type: Engine.MySQL, name: "MySQL", - defualtPort: "3306", + defaultPort: "3306", }, { type: Engine.PostgreSQL, name: "PostgreSQL", - defualtPort: "5432", + defaultPort: "5432", }, { type: Engine.MSSQL, name: "SQL Server", - defualtPort: "1433", + defaultPort: "1433", }, { type: Engine.TiDBServerless, name: "TiDB Serverless", - defualtPort: "4000", + defaultPort: "4000", }, ]; @@ -72,7 +65,7 @@ const defaultConnection: Connection = { title: "", engineType: Engine.MySQL, host: "", - port: defaultPort[Engine.MySQL], + port: "3306", username: "", password: "", }; @@ -126,7 +119,6 @@ const CreateConnectionModal = (props: Props) => { setConnection({ ...connection, ...state, - port: defaultPort[state.engineType || Engine.MySQL], }); }; @@ -249,7 +241,7 @@ const CreateConnectionModal = (props: Props) => { setPartialConnection({ engineType: value as Engine })} + onChange={(value) => setPartialConnection({ engineType: value as Engine, port: engine.defaultPort as string })} /> ))}
    - {sslType !== "none" && ( + {sslType !== "preferred" && ( <>
    { +const newPostgresClient = async (connection: Connection) => { const clientConfig: ClientConfig = { host: connection.host, port: Number(connection.port), user: connection.username, password: connection.password, database: connection.database, + application_name: "sqlchat", }; if (connection.ssl) { clientConfig.ssl = { @@ -16,21 +17,27 @@ const newPostgresClient = (connection: Connection) => { cert: connection.ssl?.cert, key: connection.ssl?.key, }; + } else { + // Use rejectUnauthorized to infer sslmode=prefer since hosted PG venders have SSL enabled. + clientConfig.ssl = { + rejectUnauthorized: false, + }; } - return new Client(clientConfig); + + let client = new Client(clientConfig); + await client.connect(); + return client; }; const testConnection = async (connection: Connection): Promise => { - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); await client.end(); return true; }; const execute = async (connection: Connection, databaseName: string, statement: string): Promise => { connection.database = databaseName; - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); const { rows, rowCount } = await client.query(statement); await client.end(); @@ -46,8 +53,7 @@ const execute = async (connection: Connection, databaseName: string, statement: }; const getDatabases = async (connection: Connection): Promise => { - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); if (connection.database) { await client.end(); return [connection.database]; @@ -67,8 +73,7 @@ const getDatabases = async (connection: Connection): Promise => { const getTables = async (connection: Connection, databaseName: string): Promise => { connection.database = databaseName; - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); const { rows } = await client.query( `SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' AND table_catalog=$1;`, [databaseName] @@ -90,8 +95,7 @@ const getTableStructure = async ( structureFetched: (tableName: string, structure: string) => void ): Promise => { connection.database = databaseName; - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); const { rows } = await client.query( `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1;`, [tableName] @@ -119,8 +123,7 @@ const getTableStructureBatch = async ( structureFetched: (tableName: string, structure: string) => void ): Promise => { connection.database = databaseName; - const client = newPostgresClient(connection); - await client.connect(); + const client = await newPostgresClient(connection); await Promise.all( tableNameList.map(async (tableName) => { const { rows } = await client.query( From bf43b5830d68e2fc81c661744bd5bb5f1fcf4a47 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 17:29:16 +0800 Subject: [PATCH 15/17] chore: add missing comment --- src/lib/connectors/postgres/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/connectors/postgres/index.ts b/src/lib/connectors/postgres/index.ts index 013f61f..8095518 100644 --- a/src/lib/connectors/postgres/index.ts +++ b/src/lib/connectors/postgres/index.ts @@ -18,7 +18,7 @@ const newPostgresClient = async (connection: Connection) => { key: connection.ssl?.key, }; } else { - // Use rejectUnauthorized to infer sslmode=prefer since hosted PG venders have SSL enabled. + // rejectUnauthorized=false to infer sslmode=prefer since hosted PG venders have SSL enabled. clientConfig.ssl = { rejectUnauthorized: false, }; From c6b4c7f7f8091e43e5398ed784c7a5c2c30f7ab3 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 17:36:52 +0800 Subject: [PATCH 16/17] feat: pg support fetch non-public schema --- src/lib/connectors/postgres/index.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/lib/connectors/postgres/index.ts b/src/lib/connectors/postgres/index.ts index 8095518..692b838 100644 --- a/src/lib/connectors/postgres/index.ts +++ b/src/lib/connectors/postgres/index.ts @@ -2,6 +2,11 @@ import { Client, ClientConfig } from "pg"; import { Connection, ExecutionResult } from "@/types"; import { Connector } from ".."; +const systemSchemas = + "'information_schema', 'pg_catalog', 'pg_toast', '_timescaledb_cache', '_timescaledb_catalog', '_timescaledb_internal', '_timescaledb_config', 'timescaledb_information', 'timescaledb_experimental'"; + +const systemTables = "'_prisma_migrations'"; + const newPostgresClient = async (connection: Connection) => { const clientConfig: ClientConfig = { host: connection.host, @@ -75,13 +80,17 @@ const getTables = async (connection: Connection, databaseName: string): Promise< connection.database = databaseName; const client = await newPostgresClient(connection); const { rows } = await client.query( - `SELECT table_name FROM information_schema.tables WHERE table_schema='public' AND table_type='BASE TABLE' AND table_catalog=$1;`, + `SELECT table_schema, table_name FROM information_schema.tables WHERE table_schema NOT IN (${systemSchemas}) AND table_name NOT IN (${systemTables}) AND table_type='BASE TABLE' AND table_catalog=$1;`, [databaseName] ); await client.end(); const tableList = []; for (const row of rows) { if (row["table_name"]) { + if (row["table_schema"] !== "public") { + tableList.push(`${row["table_schema"]}.${row["table_name"]}`); + continue; + } tableList.push(row["table_name"]); } } @@ -97,7 +106,7 @@ const getTableStructure = async ( connection.database = databaseName; const client = await newPostgresClient(connection); const { rows } = await client.query( - `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1;`, + `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema NOT IN (${systemSchemas}) AND table_name=$1;`, [tableName] ); await client.end(); @@ -127,7 +136,7 @@ const getTableStructureBatch = async ( await Promise.all( tableNameList.map(async (tableName) => { const { rows } = await client.query( - `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema='public' AND table_name=$1;`, + `SELECT column_name, data_type, is_nullable FROM information_schema.columns WHERE table_schema NOT IN (${systemSchemas}) AND table_name=$1;`, [tableName] ); const columnList = []; From 86c6abe24cdc417722d563b2fa0075f52b0e4506 Mon Sep 17 00:00:00 2001 From: Tianzhou Chen Date: Sun, 21 May 2023 18:08:36 +0800 Subject: [PATCH 17/17] chore: change setting view to setting general view --- src/components/{SettingView.tsx => SettingGeneralView.tsx} | 4 ++-- src/components/SubscriptionHistoryTable.tsx | 2 +- src/locales/en.json | 4 ++-- src/locales/es.json | 4 ++-- src/locales/zh.json | 4 ++-- src/pages/setting/index.tsx | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) rename src/components/{SettingView.tsx => SettingGeneralView.tsx} (97%) diff --git a/src/components/SettingView.tsx b/src/components/SettingGeneralView.tsx similarity index 97% rename from src/components/SettingView.tsx rename to src/components/SettingGeneralView.tsx index ee53bfe..a95928f 100644 --- a/src/components/SettingView.tsx +++ b/src/components/SettingGeneralView.tsx @@ -12,7 +12,7 @@ import LocaleSelector from "./LocaleSelector"; import ThemeSelector from "./ThemeSelector"; import OpenAIApiConfigView from "./OpenAIApiConfigView"; -const SettingView = () => { +const SettingGeneralView = () => { const { t } = useTranslation(); const { data: session } = useSession(); @@ -84,4 +84,4 @@ const SettingView = () => { ); }; -export default SettingView; +export default SettingGeneralView; diff --git a/src/components/SubscriptionHistoryTable.tsx b/src/components/SubscriptionHistoryTable.tsx index eceaf1e..60ecbce 100644 --- a/src/components/SubscriptionHistoryTable.tsx +++ b/src/components/SubscriptionHistoryTable.tsx @@ -65,7 +65,7 @@ const SubscriptionHistoryTable = () => { - {t("setting.billing.view-receipt")} + {t("setting.subscription.view-receipt")} diff --git a/src/locales/en.json b/src/locales/en.json index 41dd2b9..e9e397a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -71,8 +71,8 @@ "n-question-per-month": "{{count}} quota / month", "early-bird-checkout": "Early bird discount, 50% off for 1 year" }, - "billing": { - "self": "Billing", + "subscription": { + "self": "Subscription", "view-receipt": "View receipt" }, "account": { diff --git a/src/locales/es.json b/src/locales/es.json index 0ba472b..7e1970e 100644 --- a/src/locales/es.json +++ b/src/locales/es.json @@ -69,8 +69,8 @@ "n-question-per-month": "{{count}} Cuota / mes", "early-bird-checkout": "Descuento por reserva anticipada, 50 % de descuento durante 1 año" }, - "billing": { - "self": "Facturación", + "subscription": { + "self": "Suscripción", "view-receipt": "Ver recibo" }, "account": { diff --git a/src/locales/zh.json b/src/locales/zh.json index 51c182a..033c85c 100644 --- a/src/locales/zh.json +++ b/src/locales/zh.json @@ -71,8 +71,8 @@ "n-question-per-month": "{{count}} 点额度 / 月", "early-bird-checkout": "早鸟优惠,5 折购买 1 年" }, - "billing": { - "self": "计费", + "subscription": { + "self": "订阅", "view-receipt": "查看收据" }, "account": { diff --git a/src/pages/setting/index.tsx b/src/pages/setting/index.tsx index 2c0621e..b6e00c4 100644 --- a/src/pages/setting/index.tsx +++ b/src/pages/setting/index.tsx @@ -5,7 +5,7 @@ import { useTranslation } from "react-i18next"; import Link from "next/link"; import { Dialog, Transition } from "@headlessui/react"; import { ArrowUturnLeftIcon, Bars3Icon, Cog6ToothIcon, XMarkIcon, CreditCardIcon } from "@heroicons/react/24/outline"; -import SettingView from "../../components/SettingView"; +import SettingGeneralView from "../../components/SettingGeneralView"; import StripeCheckPaymentBanner from "../../components/StripeCheckPaymentBanner"; function classNames(...classes: string[]) { @@ -175,7 +175,7 @@ const SettingPage: NextPage = () => {
    {router.query.session_id && }
    - +