diff --git a/src/app/(components)/ConnectionConfigScreen.tsx b/src/app/(components)/ConnectionConfigScreen.tsx
index 04f2dda4..dea3017b 100644
--- a/src/app/(components)/ConnectionConfigScreen.tsx
+++ b/src/app/(components)/ConnectionConfigScreen.tsx
@@ -24,6 +24,7 @@ import {
import { Textarea } from "@/components/ui/textarea";
import { validateConnectionEndpoint } from "@/lib/validation";
import { appVersion } from "@/env";
+import ErrorMessage from "@/components/custom/ErrorMessage";
interface ConnectionItem {
id: string;
@@ -173,11 +174,14 @@ export default function ConnectionConfigScreen() {
[router]
);
- const onConnectClicked = () => {
+ const [valid, errorMessage] = validateConnectionEndpoint(url);
+
+ const onConnectClicked = useCallback(() => {
+ if (!valid) return;
if (url && token) {
connect(url, token);
}
- };
+ }, [valid, connect, url, token]);
const onConnectionListChange = () => {
setConnections(getConnections());
@@ -256,6 +260,7 @@ export default function ConnectionConfigScreen() {
value={url}
onChange={(e) => setUrl(e.currentTarget.value)}
/>
+ {url && errorMessage && }
diff --git a/src/app/(components)/MainScreen.tsx b/src/app/(components)/MainScreen.tsx
index 51e633ed..960088b7 100644
--- a/src/app/(components)/MainScreen.tsx
+++ b/src/app/(components)/MainScreen.tsx
@@ -7,8 +7,9 @@ import { TooltipProvider } from "@/components/ui/tooltip";
import { AutoCompleteProvider } from "@/context/AutoCompleteProvider";
import ContextMenuHandler from "./ContentMenuHandler";
import InternalPubSub from "@/lib/internal-pubsub";
-import { useParams, useRouter } from "next/navigation";
-import { Metadata } from "next";
+import { useRouter } from "next/navigation";
+import { normalizeConnectionEndpoint } from "@/lib/validation";
+import { SchemaProvider } from "@/screens/DatabaseScreen/SchemaProvider";
function MainConnection({
credential,
@@ -30,7 +31,9 @@ function MainConnection({
return (
-
+
+
+
);
}
@@ -48,7 +51,11 @@ function InvalidSession() {
export default function MainScreen() {
const router = useRouter();
const sessionCredential: { url: string; token: string } = useMemo(() => {
- return JSON.parse(sessionStorage.getItem("connection") ?? "{}");
+ const config = JSON.parse(sessionStorage.getItem("connection") ?? "{}");
+ return {
+ url: normalizeConnectionEndpoint(config.url),
+ token: config.token,
+ };
}, []);
/**
diff --git a/src/app/(components)/SchemaView.tsx b/src/app/(components)/SchemaView.tsx
index eae9d72a..8639f49b 100644
--- a/src/app/(components)/SchemaView.tsx
+++ b/src/app/(components)/SchemaView.tsx
@@ -1,17 +1,15 @@
import { buttonVariants } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
-import { useAutoComplete } from "@/context/AutoCompleteProvider";
-import { useDatabaseDriver } from "@/context/DatabaseDriverProvider";
-import { DatabaseSchemaItem } from "@/drivers/DatabaseDriver";
import { cn } from "@/lib/utils";
import { openTabs } from "@/messages/openTabs";
import { LucideIcon, Table2 } from "lucide-react";
-import { useCallback, useEffect, useState } from "react";
+import { useCallback, useState } from "react";
import {
OpenContextMenuList,
openContextMenuFromEvent,
} from "@/messages/openContextMenu";
import OpacityLoading from "./OpacityLoading";
+import { useSchema } from "@/screens/DatabaseScreen/SchemaProvider";
interface SchemaViewItemProps {
icon: LucideIcon;
@@ -59,26 +57,8 @@ function SchemaViewmItem({
}
export default function SchemaView() {
- const { updateTableList } = useAutoComplete();
- const [schemaItems, setSchemaItems] = useState
([]);
- const [loading, setLoading] = useState(false);
+ const { refresh, schema } = useSchema();
const [selectedIndex, setSelectedIndex] = useState(-1);
- const { databaseDriver } = useDatabaseDriver();
-
- const fetchSchema = useCallback(() => {
- setLoading(true);
-
- databaseDriver.getTableList().then((tableList) => {
- const sortedTableList = [...tableList];
- sortedTableList.sort((a, b) => {
- return a.name.localeCompare(b.name);
- });
-
- setSchemaItems(sortedTableList);
- updateTableList(tableList.map((table) => table.name));
- setLoading(false);
- });
- }, [databaseDriver, updateTableList]);
const prepareContextMenu = useCallback(
(tableName?: string) => {
@@ -91,24 +71,19 @@ export default function SchemaView() {
},
},
{ separator: true },
- { title: "Refresh", onClick: fetchSchema },
+ { title: "Refresh", onClick: () => refresh() },
] as OpenContextMenuList;
},
- [fetchSchema]
+ [refresh]
);
- useEffect(() => {
- fetchSchema();
- }, [fetchSchema]);
-
return (
- {loading && }
- {schemaItems.map((item, schemaIndex) => {
+ {schema.map((item, schemaIndex) => {
return (
+
+
+
+ );
+}
diff --git a/src/components/custom/ErrorMessage.tsx b/src/components/custom/ErrorMessage.tsx
new file mode 100644
index 00000000..43f6520b
--- /dev/null
+++ b/src/components/custom/ErrorMessage.tsx
@@ -0,0 +1,7 @@
+export default function ErrorMessage({
+ message,
+}: {
+ readonly message: string;
+}) {
+ return {message}
;
+}
diff --git a/src/drivers/DatabaseDriver.tsx b/src/drivers/DatabaseDriver.tsx
index c666dfbf..e25e8dc3 100644
--- a/src/drivers/DatabaseDriver.tsx
+++ b/src/drivers/DatabaseDriver.tsx
@@ -23,8 +23,10 @@ export interface DatabaseTableSchema {
export default class DatabaseDriver {
protected client: hrana.WsClient;
protected stream?: hrana.WsStream;
+ protected endpoint: string = "";
constructor(url: string, authToken: string) {
+ this.endpoint = url;
this.client = hrana.openWs(url, authToken);
}
@@ -46,6 +48,10 @@ export default class DatabaseDriver {
return this.stream;
}
+ getEndpoint() {
+ return this.endpoint;
+ }
+
async query(stmt: hrana.InStmt) {
const stream = this.getStream();
diff --git a/src/lib/validation.test.ts b/src/lib/validation.test.ts
index 5d35b8c0..95e39199 100644
--- a/src/lib/validation.test.ts
+++ b/src/lib/validation.test.ts
@@ -1,4 +1,8 @@
-import { validateConnectionEndpoint, validateOperation } from "./validation";
+import {
+ normalizeConnectionEndpoint,
+ validateConnectionEndpoint,
+ validateOperation,
+} from "./validation";
describe("Operation Validation", () => {
it("UPDATE with primary key SHOULD be valid operation", () => {
@@ -209,4 +213,14 @@ describe("Validate the connection endpoint", () => {
false
);
});
+
+ it("Transform libsql:// to wss://", () => {
+ expect(normalizeConnectionEndpoint("libsql://testing.example.com")).toBe(
+ "wss://testing.example.com"
+ );
+
+ expect(normalizeConnectionEndpoint("wss://testing.example.com")).toBe(
+ "wss://testing.example.com"
+ );
+ });
});
diff --git a/src/lib/validation.ts b/src/lib/validation.ts
index 10a76ce5..2d4253c1 100644
--- a/src/lib/validation.ts
+++ b/src/lib/validation.ts
@@ -73,8 +73,8 @@ export function validateConnectionEndpoint(
try {
const url = new URL(endpoint);
- if (url.protocol !== "wss:") {
- return [false, "We only support wss:// at the moment."];
+ if (url.protocol !== "wss:" && url.protocol !== "libsql:") {
+ return [false, "We only support wss:// or libsql:// at the moment."];
}
return [true, ""];
@@ -82,3 +82,7 @@ export function validateConnectionEndpoint(
return [false, "Your URL is not valid"];
}
}
+
+export function normalizeConnectionEndpoint(endpoint: string) {
+ return endpoint.replace(/^libsql:\/\//, "wss://");
+}
diff --git a/src/screens/DatabaseScreen/ConnectingDialog.tsx b/src/screens/DatabaseScreen/ConnectingDialog.tsx
new file mode 100644
index 00000000..91134034
--- /dev/null
+++ b/src/screens/DatabaseScreen/ConnectingDialog.tsx
@@ -0,0 +1,60 @@
+import { Button } from "@/components/ui/button";
+import { LucideLoader } from "lucide-react";
+import { useRouter } from "next/navigation";
+
+export default function ConnectingDialog({
+ message,
+ url,
+ onRetry,
+}: Readonly<{
+ loading?: boolean;
+ url?: string;
+ message?: string;
+ onRetry?: () => void;
+}>) {
+ const router = useRouter();
+
+ let body = (
+
+
+
+ Connecting to {url}
+
+
+ );
+
+ if (message) {
+ body = (
+ <>
+
+ We have problem connecting to database
+
+
+
{message}
+
+
+
+
+
+ >
+ );
+ }
+
+ return (
+
+
+
+
+
LibSQL Studio
+
+
+
+ {body}
+
+ );
+}
diff --git a/src/screens/DatabaseScreen/SchemaProvider.tsx b/src/screens/DatabaseScreen/SchemaProvider.tsx
new file mode 100644
index 00000000..e3b055d8
--- /dev/null
+++ b/src/screens/DatabaseScreen/SchemaProvider.tsx
@@ -0,0 +1,85 @@
+import { useAutoComplete } from "@/context/AutoCompleteProvider";
+import { useDatabaseDriver } from "@/context/DatabaseDriverProvider";
+import { DatabaseSchemaItem } from "@/drivers/DatabaseDriver";
+import {
+ PropsWithChildren,
+ createContext,
+ useCallback,
+ useContext,
+ useEffect,
+ useMemo,
+ useState,
+} from "react";
+import ConnectingDialog from "./ConnectingDialog";
+
+const SchemaContext = createContext<{
+ schema: DatabaseSchemaItem[];
+ refresh: () => void;
+}>({
+ schema: [],
+ refresh: () => {
+ throw new Error("Not implemented");
+ },
+});
+
+export function useSchema() {
+ return useContext(SchemaContext);
+}
+
+export function SchemaProvider({ children }: Readonly) {
+ const { updateTableList } = useAutoComplete();
+ const [error, setError] = useState();
+ const [schemaItems, setSchemaItems] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const { databaseDriver } = useDatabaseDriver();
+
+ const fetchSchema = useCallback(
+ (refresh?: boolean) => {
+ if (refresh) {
+ setLoading(true);
+ }
+
+ databaseDriver
+ .getTableList()
+ .then((tableList) => {
+ const sortedTableList = [...tableList];
+ sortedTableList.sort((a, b) => {
+ return a.name.localeCompare(b.name);
+ });
+
+ setSchemaItems(sortedTableList);
+ updateTableList(tableList.map((table) => table.name));
+
+ setError(undefined);
+ setLoading(false);
+ })
+ .catch((e) => {
+ setError(e.message);
+ setLoading(false);
+ });
+ },
+ [databaseDriver, updateTableList, setError]
+ );
+
+ useEffect(() => {
+ fetchSchema(true);
+ }, [fetchSchema]);
+
+ const props = useMemo(() => {
+ return { schema: schemaItems, refresh: fetchSchema };
+ }, [schemaItems, fetchSchema]);
+
+ if (error || loading) {
+ return (
+
+ );
+ }
+
+ return (
+ {children}
+ );
+}