diff --git a/src/app/(components)/ConnectionConfigScreen.tsx b/src/app/(components)/ConnectionConfigScreen.tsx
index 48350d10..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;
@@ -69,8 +70,8 @@ function ConnectionEdit({ onComplete }: { onComplete: () => void }) {
const onSaveClicked = () => {
// Validate the connection
- const [isUrlInvalid, urlInvalidMessage] = validateConnectionEndpoint(url);
- if (isUrlInvalid) {
+ const [isValid, urlInvalidMessage] = validateConnectionEndpoint(url);
+ if (!isValid) {
setError(urlInvalidMessage);
return;
}
@@ -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 ead831a7..0d6eab00 100644
--- a/src/app/(components)/MainScreen.tsx
+++ b/src/app/(components)/MainScreen.tsx
@@ -8,6 +8,8 @@ import { AutoCompleteProvider } from "@/context/AutoCompleteProvider";
import ContextMenuHandler from "./ContentMenuHandler";
import InternalPubSub from "@/lib/internal-pubsub";
import { useRouter } from "next/navigation";
+import { normalizeConnectionEndpoint } from "@/lib/validation";
+import { SchemaProvider } from "@/screens/DatabaseScreen/SchemaProvider";
function MainConnection({
credential,
@@ -29,7 +31,9 @@ function MainConnection({
return (
-
+
+
+
);
}
@@ -47,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)/OptimizeTable/OptimizeTableState.tsx b/src/app/(components)/OptimizeTable/OptimizeTableState.tsx
index 5ff37e4b..4825262b 100644
--- a/src/app/(components)/OptimizeTable/OptimizeTableState.tsx
+++ b/src/app/(components)/OptimizeTable/OptimizeTableState.tsx
@@ -1,5 +1,5 @@
import { selectArrayFromIndexList } from "@/lib/export-helper";
-import { OptimizeTableHeaderProps } from ".";
+import { OptimizeTableHeaderProps, TableColumnDataType } from ".";
import * as hrana from "@libsql/hrana-client";
import { DatabaseTableSchema } from "@/drivers/DatabaseDriver";
import { LucideKey } from "lucide-react";
@@ -33,11 +33,31 @@ export default class OptimizeTableState {
) {
return new OptimizeTableState(
dataResult.columnNames.map((headerName, idx) => {
+ let initialSize = 150;
+ const dataType = convertSqliteType(dataResult.columnDecltypes[idx]);
+
+ if (
+ dataType === TableColumnDataType.INTEGER ||
+ dataType === TableColumnDataType.REAL
+ ) {
+ initialSize = 100;
+ } else if (dataType === TableColumnDataType.TEXT) {
+ // Use 100 first rows to determine the good initial size
+ let maxSize = 0;
+ for (let i = 0; i < Math.min(dataResult.rows.length, 100); i++) {
+ maxSize = Math.max(
+ (dataResult.rows[i][headerName ?? ""]?.toString() ?? "").length
+ );
+ }
+
+ initialSize = Math.max(150, Math.min(500, maxSize * 8));
+ }
+
return {
- initialSize: 150,
+ initialSize,
name: headerName ?? "",
resizable: true,
- dataType: convertSqliteType(dataResult.columnDecltypes[idx]),
+ dataType,
icon: schemaResult?.pk.includes(headerName ?? "") ? (
) : undefined,
diff --git a/src/app/(components)/OptimizeTable/TableHeaderResizeHandler.tsx b/src/app/(components)/OptimizeTable/TableHeaderResizeHandler.tsx
index 6e8c1f4d..6e434ae7 100644
--- a/src/app/(components)/OptimizeTable/TableHeaderResizeHandler.tsx
+++ b/src/app/(components)/OptimizeTable/TableHeaderResizeHandler.tsx
@@ -1,5 +1,5 @@
-import { useRef, useState, useEffect } from 'react';
-import styles from './styles.module.css';
+import { useRef, useState, useEffect } from "react";
+import styles from "./styles.module.css";
export default function TableHeaderResizeHandler({
idx,
@@ -62,9 +62,9 @@ export default function TableHeaderResizeHandler({
onResize(idx, width);
if (table) {
- const columns = table.style.gridTemplateColumns.split(' ');
- columns[idx] = width + 'px';
- table.style.gridTemplateColumns = columns.join(' ');
+ const columns = table.style.gridTemplateColumns.split(" ");
+ columns[idx] = width + "px";
+ table.style.gridTemplateColumns = columns.join(" ");
}
if (edgeResizing) {
@@ -76,12 +76,12 @@ export default function TableHeaderResizeHandler({
setResizing(false);
};
- document.addEventListener('mousemove', onMouseMove);
- document.addEventListener('mouseup', onMouseUp);
+ document.addEventListener("mousemove", onMouseMove);
+ document.addEventListener("mouseup", onMouseUp);
return () => {
- document.removeEventListener('mousemove', onMouseMove);
- document.removeEventListener('mouseup', onMouseUp);
+ document.removeEventListener("mousemove", onMouseMove);
+ document.removeEventListener("mouseup", onMouseUp);
};
}
}
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 3e80c5e6..7403310d 100644
--- a/src/drivers/DatabaseDriver.tsx
+++ b/src/drivers/DatabaseDriver.tsx
@@ -66,8 +66,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);
}
@@ -89,6 +91,10 @@ export default class DatabaseDriver {
return this.stream;
}
+ getEndpoint() {
+ return this.endpoint;
+ }
+
async query(stmt: hrana.InStmt) {
const stream = this.getStream();
@@ -128,17 +134,20 @@ export default class DatabaseDriver {
}));
// Check auto increment
- const seqCount = await this.query(
- `SELECT COUNT(*) AS total FROM sqlite_sequence WHERE name=${escapeSqlValue(
- tableName
- )};`
- );
-
let hasAutoIncrement = false;
- const seqRow = seqCount.rows[0];
- if (seqRow && Number(seqRow[0] ?? 0) > 0) {
- hasAutoIncrement = true;
- }
+
+ try {
+ const seqCount = await this.query(
+ `SELECT COUNT(*) AS total FROM sqlite_sequence WHERE name=${escapeSqlValue(
+ tableName
+ )};`
+ );
+
+ const seqRow = seqCount.rows[0];
+ if (seqRow && Number(seqRow[0] ?? 0) > 0) {
+ hasAutoIncrement = true;
+ }
+ } catch {}
return {
columns,
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}
+ );
+}