From 6215ae7eab91e55ac1f7d9f91eec31f477a6be00 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sat, 28 Sep 2024 08:36:20 -0700 Subject: [PATCH] Restyle matching Outerbase (#166) * styling sidebar * change query tab editor to match outerbase * add more style * fixing explain query * redesign side menu * restyle sidebar * add sidemenu * change all the libsql studio name to outerbase studio * pull the version from package * restyle the connection list match turso style * upgrade everything to mono color * reskin to outerbase --- next.config.js | 4 + package-lock.json | 17 +- package.json | 3 +- src/app/(public)/databases/mysql/page.tsx | 10 +- src/app/(public)/docs/layout.tsx | 3 +- src/app/(public)/login/page.tsx | 3 +- src/app/(public)/page.tsx | 32 ++- src/app/(public)/privacy/page.tsx | 3 +- src/app/(public)/terms/page.tsx | 5 +- src/app/(theme)/connect/connection-list.tsx | 61 ++++-- src/app/(theme)/connect/driver-dropdown.tsx | 189 ++++++++++-------- src/app/(theme)/connect/page-client.tsx | 106 +++------- src/app/(theme)/connect/page.tsx | 5 +- .../(theme)/connect/saved-connection-card.tsx | 56 +++--- .../connect/saved-connection-content.tsx | 12 +- .../connect/saved-connection-storage.ts | 26 ++- src/app/(theme)/connect/saved-connection.tsx | 3 +- .../(theme)/playground/client/page-client.tsx | 172 +++++++--------- src/app/globals.css | 12 +- src/app/layout.tsx | 8 +- src/components/gui/database-gui.tsx | 17 +- src/components/gui/main-connection.tsx | 3 +- src/components/gui/query-explanation.tsx | 11 +- src/components/gui/save-doc-button.tsx | 2 +- src/components/gui/schema-sidebar-list.tsx | 7 +- src/components/gui/schema-sidebar.tsx | 37 ++-- src/components/gui/sidebar-tab.tsx | 162 +++++++++++---- .../saved-doc-tab/create-namespace-button.tsx | 73 +++---- .../gui/sidebar/saved-doc-tab/index.tsx | 64 +++++- src/components/gui/sortable-tab.tsx | 26 ++- src/components/gui/sql-editor/index.tsx | 3 + src/components/gui/tabs/query-tab.tsx | 120 +++++++---- src/components/gui/windows-tab.tsx | 55 ++--- src/components/icons/outerbase-icon.tsx | 140 ++++++++++++- src/components/mdx/docs-navigation.tsx | 2 +- src/components/website-layout.tsx | 8 +- src/const.ts | 2 + src/messages/open-tab.tsx | 4 +- 38 files changed, 899 insertions(+), 567 deletions(-) diff --git a/next.config.js b/next.config.js index 6ca26217..4910094f 100644 --- a/next.config.js +++ b/next.config.js @@ -1,10 +1,14 @@ /* eslint-disable @typescript-eslint/no-var-requires */ const withMDX = require("@next/mdx")(); +const pkg = require("./package.json"); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, pageExtensions: ["js", "jsx", "mdx", "ts", "tsx"], + env: { + NEXT_PUBLIC_STUDIO_VERSION: pkg.version, + }, }; module.exports = withMDX(nextConfig); diff --git a/package-lock.json b/package-lock.json index 8eb006f5..62041abb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@libsqlstudio/studio", - "version": "0.7.1", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@libsqlstudio/studio", - "version": "0.7.1", + "version": "0.7.2", "dependencies": { "@aws-sdk/client-s3": "^3.540.0", "@blocknote/core": "^0.12.1", @@ -23,6 +23,7 @@ "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.2.4", + "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", @@ -5642,6 +5643,18 @@ "node": ">= 8" } }, + "node_modules/@phosphor-icons/react": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.1.7.tgz", + "integrity": "sha512-g2e2eVAn1XG2a+LI09QU3IORLhnFNAFkNbo2iwbX6NOKSLOwvEMmTa7CgOzEbgNWR47z8i8kwjdvYZ5fkGx1mQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">= 16.8", + "react-dom": ">= 16.8" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", diff --git a/package.json b/package.json index 5a39097e..dc90a121 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@libsqlstudio/studio", - "version": "0.7.1", + "version": "0.7.2", "private": false, "scripts": { "dev": "next dev -p 3008", @@ -42,6 +42,7 @@ "@mdx-js/loader": "^3.0.1", "@mdx-js/react": "^3.0.1", "@next/mdx": "^14.2.4", + "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", diff --git a/src/app/(public)/databases/mysql/page.tsx b/src/app/(public)/databases/mysql/page.tsx index ca900db1..f6dedfbc 100644 --- a/src/app/(public)/databases/mysql/page.tsx +++ b/src/app/(public)/databases/mysql/page.tsx @@ -1,16 +1,16 @@ import { MySQLIcon } from "@/components/icons/outerbase-icon"; import WebsiteLayout from "@/components/website-layout"; +import { WEBSITE_NAME } from "@/const"; import { Metadata } from "next"; -const siteDescription = - "LibSQL Studio is a fully-featured, lightweight GUI client for managing MySQL databases"; +const siteDescription = `${WEBSITE_NAME} is a fully-featured, lightweight GUI client for managing MySQL databases`; export const metadata: Metadata = { - title: "MySQL - LibSQL Studio", + title: `MySQL - ${WEBSITE_NAME}`, keywords: ["mysql", "studio", "browser", "editor", "gui", "online", "client"], description: siteDescription, openGraph: { - siteName: "LibSQL Studio", + siteName: WEBSITE_NAME, description: siteDescription, }, }; @@ -34,7 +34,7 @@ function HeroSection() { MySQL Support

- LibSQL Studio is a lightweight, fully-featured GUI client for MySQL + {WEBSITE_NAME} is a lightweight, fully-featured GUI client for MySQL databases. It enables you to manage and view your database, or expose your database interface externally and much more.

diff --git a/src/app/(public)/docs/layout.tsx b/src/app/(public)/docs/layout.tsx index 173dab82..9f5f774e 100644 --- a/src/app/(public)/docs/layout.tsx +++ b/src/app/(public)/docs/layout.tsx @@ -1,8 +1,9 @@ import { DocLayout } from "@/components/mdx/docs"; +import { WEBSITE_NAME } from "@/const"; const TableContent = [ { - title: "LibSQL Studio", + title: WEBSITE_NAME, href: "/docs", }, { diff --git a/src/app/(public)/login/page.tsx b/src/app/(public)/login/page.tsx index 6c96f162..e2bd2da2 100644 --- a/src/app/(public)/login/page.tsx +++ b/src/app/(public)/login/page.tsx @@ -1,5 +1,6 @@ "use server"; import LogoLoading from "@/components/gui/logo-loading"; +import { WEBSITE_NAME } from "@/const"; import { env } from "@/env"; import Link from "next/link"; @@ -18,7 +19,7 @@ export default async function LoginPage() {

- LibSQL Studio is free and doesn't require login. However, + {WEBSITE_NAME} is free and doesn't require login. However, logged-in users get extra features:

diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx index 89d06e9e..7dffc617 100644 --- a/src/app/(public)/page.tsx +++ b/src/app/(public)/page.tsx @@ -5,16 +5,14 @@ import { } from "@/components/icons/outerbase-icon"; import { buttonVariants } from "@/components/ui/button"; import WebsiteLayout from "@/components/website-layout"; +import { WEBSITE_GENERAL_DESCRIPTION, WEBSITE_NAME } from "@/const"; import { cn } from "@/lib/utils"; import type { Metadata } from "next"; import Link from "next/link"; import { PropsWithChildren } from "react"; -const siteDescription = - "LibSQL Studio is a fully-featured, lightweight GUI client for managing SQLite-based databases like Turso, LibSQL, and rqlite. It runs entirely in your browser, so there's no need to download anything"; - export const metadata: Metadata = { - title: "LibSQL Studio", + title: WEBSITE_NAME, keywords: [ "libsql", "rqlite", @@ -27,10 +25,10 @@ export const metadata: Metadata = { "online", "client", ], - description: siteDescription, + description: WEBSITE_GENERAL_DESCRIPTION, openGraph: { - siteName: "LibSQL Studio", - description: siteDescription, + siteName: WEBSITE_NAME, + description: WEBSITE_GENERAL_DESCRIPTION, }, }; @@ -84,14 +82,14 @@ function HeroSection() { Powerful Database Client

- LibSQL Studio is a fully-featured, lightweight GUI client for + {WEBSITE_NAME} is a fully-featured, lightweight GUI client for managing Turso, LibSQL, Cloudflare D1, rqlite, MySQL, and PostgreSQL. It runs entirely in your browser, so there's no need to download anything.

- +
@@ -121,7 +119,7 @@ function SupportDriver() { PostgreSQL - Coming Soon 15th Sep 2024 + Coming Soon 7th Oct 2024 @@ -186,8 +184,8 @@ function FeatureList() {

- LibSQL Studio allows you to save your queries and organize them into - folders, with the ability to sync across multiple devices. + {WEBSITE_NAME} allows you to save your queries and organize them + into folders, with the ability to sync across multiple devices.

@@ -204,7 +202,7 @@ function FeatureList() {

- LibSQL Studio features a user-friendly query editor equipped with + {WEBSITE_NAME} features a user-friendly query editor equipped with auto-completion and function hint tooltips. It allows you to execute multiple queries simultaneously and view their results efficiently.

@@ -223,8 +221,8 @@ function FeatureList() {

- LibSQL Studio allows you to quickly create, modify, and remove table - columns with just a few clicks without writing any SQL. + {WEBSITE_NAME} allows you to quickly create, modify, and remove + table columns with just a few clicks without writing any SQL.

@@ -239,7 +237,7 @@ function CommunitySection() {

Join our community for the latest updates, roadmap insights, and - discussions on the future of LibSQL Studio. + discussions on the future of {WEBSITE_NAME}.

@@ -310,7 +308,7 @@ export default async function MainPage() {

- LibSQL Studio has many features and is regularly updated. Since it + {WEBSITE_NAME} has many features and is regularly updated. Since it is an{" "} Effective Date: 2024-06-23

{` - Welcome to LibSQL Studio ("we," "our," or "us"). We are committed to protecting your privacy and ensuring + Welcome to ${WEBSITE_NAME} ("we," "our," or "us"). We are committed to protecting your privacy and ensuring that your personal information is handled in a safe and responsible manner. This Privacy Policy explains how we collect, use, and protect your information when you use our tool. `} diff --git a/src/app/(public)/terms/page.tsx b/src/app/(public)/terms/page.tsx index 9229edae..22a19935 100644 --- a/src/app/(public)/terms/page.tsx +++ b/src/app/(public)/terms/page.tsx @@ -1,4 +1,5 @@ import WebsiteLayout from "@/components/website-layout"; +import { WEBSITE_NAME } from "@/const"; export default function TermPage() { return ( @@ -9,7 +10,7 @@ export default function TermPage() {

Effective Date: 2024-06-23

{` - Welcome to LibSQL Studio ("we," "our," or "us"). + Welcome to ${WEBSITE_NAME} ("we," "our," or "us"). By accessing or using our tool, you agree to be bound by these Terms and Conditions ("Terms"). If you do not agree to these Terms, please do not use the Tool. `}

@@ -53,7 +54,7 @@ export default function TermPage() {

6. Limitation of Liability

-

{`LibSQL Studio is provided "as is" without warranties of any kind. We are not liable for any damages arising from your use of the Tool.`}

+

{`${WEBSITE_NAME} is provided "as is" without warranties of any kind. We are not liable for any damages arising from your use of the Tool.`}

By using the Tool, you acknowledge that you have read, understood, and diff --git a/src/app/(theme)/connect/connection-list.tsx b/src/app/(theme)/connect/connection-list.tsx index a0ab7959..9eb65565 100644 --- a/src/app/(theme)/connect/connection-list.tsx +++ b/src/app/(theme)/connect/connection-list.tsx @@ -20,31 +20,40 @@ import ConnectionItemCard from "./saved-connection-card"; import { getDatabases } from "@/lib/api/fetch-databases"; import { User } from "lucia"; import QuickConnect from "./quick-connect"; -import { LucideChevronDown } from "lucide-react"; +import { LucideChevronDown, LucideSearch } from "lucide-react"; import DriverDropdown from "./driver-dropdown"; +import { cn } from "@/lib/utils"; function ConnectionListSection({ data, name, + search, onRemove, onEdit, }: Readonly<{ + search: string; data: SavedConnectionItem[]; name?: string; onRemove: Dispatch>; onEdit: Dispatch>; }>) { const body = useMemo(() => { - if (data.length === 0) + const filteredData = data.filter((d) => + d.name.toLowerCase().includes(search.toLowerCase()) + ); + + if (filteredData.length === 0) return ( -

- There is no connection. Please add new connection +
+ {search + ? `There is no record with '${search}'` + : "There is no base. Please add new base"}
); return ( -
- {data.map((conn) => { +
+ {filteredData.map((conn) => { return ( ); - }, [data, onRemove, onEdit]); + }, [name, data, search, onRemove, onEdit]); return ( <> - {name &&

{name}

} + {name && ( +

{name}

+ )} {body} ); @@ -73,6 +84,7 @@ function ConnectionListSection({ export default function ConnectionList({ user, }: Readonly<{ user: User | null }>) { + const [search, setSearch] = useState(""); const [quickConnect, setQuickConnect] = useState( null ); @@ -199,24 +211,44 @@ export default function ConnectionList({ } return ( - <> -
+
+
+

+ Bases +

+ +
+
+
+ +
+ setSearch(e.currentTarget.value)} + type="text" + className="bg-inherit p-2 pl-2 pr-2 outline-none text-sm h-full grow" + placeholder="Search base name" + /> +
+
+ - - +
{dialogComponent} )} - +
); } diff --git a/src/app/(theme)/connect/driver-dropdown.tsx b/src/app/(theme)/connect/driver-dropdown.tsx index 88b3fc4c..b2d51e1b 100644 --- a/src/app/(theme)/connect/driver-dropdown.tsx +++ b/src/app/(theme)/connect/driver-dropdown.tsx @@ -8,6 +8,15 @@ import { DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { useRouter } from "next/navigation"; +import { + CloudflareIcon, + MySQLIcon, + RqliteIcon, + SQLiteIcon, + TursoIcon, + ValtownIcon, +} from "@/components/icons/outerbase-icon"; +import Link from "next/link"; interface Props { onSelect: (driver: SupportedDriver) => void; @@ -22,94 +31,110 @@ export default function DriverDropdown({ return ( {children} - - { - onSelect("turso"); - }} - > -
- turso -
-
Turso / LibSQL
-
SQLite for Product
-
-
-
- { - onSelect("cloudflare-d1"); - }} - > -
- rqlite + +
+
+ + SQLite-based Database +
-
Cloudflare D1
-
- D1 is Cloudflare’s native serverless database -
-
-
- - { - onSelect("rqlite"); - }} - > -
- rqlite -
-
rqlite
-
- Distributed database built on SQLite -
-
-
-
- { - onSelect("valtown"); - }} - > -
- valtown -
-
val.town
-
Private SQLite database
-
-
-
- - { - onSelect("sqlite-filehandler"); - }} - > -
- sqlite -
-
Open SQLite File
-
- Open SQLite file database directly in your browser. -
+ { + onSelect("turso"); + }} + > +
+ +
Turso / LibSQL
+
+
+ + { + onSelect("cloudflare-d1"); + }} + > +
+ +
+
Cloudflare D1
+
+
+
+ + { + onSelect("rqlite"); + }} + > +
+ +
rqlite
+
+
+ + { + onSelect("valtown"); + }} + > +
+ +
val.town
+
+
+ + + + { + onSelect("sqlite-filehandler"); + }} + > +
+ +
+
SQLite File
+
+
+
+ + { + router.push("/playground/client"); + }} + > +
+ +
+
Blank SQLite
+
+
+
-
- { - router.push("/playground/client"); - }} - > -
- sqlite + +
+ + Other + +
-
Blank SQLite
-
- Start a new SQLite database directly in your browser. -
+ + + +
+
MySQL
+
+ +
- +
); diff --git a/src/app/(theme)/connect/page-client.tsx b/src/app/(theme)/connect/page-client.tsx index 5aa0c7d1..ddfc3e6a 100644 --- a/src/app/(theme)/connect/page-client.tsx +++ b/src/app/(theme)/connect/page-client.tsx @@ -1,91 +1,49 @@ "use client"; import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; -import { PropsWithChildren } from "react"; import ConnectionList from "./connection-list"; import Link from "next/link"; -import { LucideCopy, LucideMoon, LucideSun } from "lucide-react"; +import { LucideMoon, LucideSun } from "lucide-react"; import { User } from "lucia"; import { useTheme } from "@/context/theme-provider"; -function TabContainer({ children }: Readonly) { - return ( -
-
- {children} -
-
- ); -} - -function TabItem({ - active, - text, -}: Readonly<{ active?: boolean; text: string }>) { - const className = cn( - "px-3 py-1", - active - ? "bg-background border-b border-b-background border-t border-x" - : "border-b border-gray-300" - ); - - return
{text}
; -} - -function Header({ user }: Readonly<{ user: User | null }>) { +export default function ConnectBody({ user }: Readonly<{ user: User | null }>) { const { theme, toggleTheme } = useTheme(); return ( -
-

-
Welcome, {user ? user.name : "Guest"}
- {user?.id && ( -
{ - window.navigator.clipboard.writeText(user.id); - }} +
+
+ + - - ID: - {user.id}   - - -
- )} -

-
-
- + + {user ? ( + + + ) : ( - + + + )} - - - {user ? ( - - - - ) : ( - - - - )} +
-
- ); -} - -export default function ConnectBody({ user }: Readonly<{ user: User | null }>) { - "use client"; - return ( -
-
- - -
diff --git a/src/app/(theme)/connect/page.tsx b/src/app/(theme)/connect/page.tsx index c02987af..318da86a 100644 --- a/src/app/(theme)/connect/page.tsx +++ b/src/app/(theme)/connect/page.tsx @@ -2,10 +2,11 @@ import { Metadata } from "next"; import { getSessionFromCookie } from "@/lib/auth"; import ConnectBody from "./page-client"; import ThemeLayout from "../theme_layout"; +import { WEBSITE_NAME } from "@/const"; export const metadata: Metadata = { - title: "LibSQL Studio", - description: "LibSQL Studio", + title: WEBSITE_NAME, + description: WEBSITE_NAME, }; export default async function Home() { diff --git a/src/app/(theme)/connect/saved-connection-card.tsx b/src/app/(theme)/connect/saved-connection-card.tsx index 311b8b72..66ea332e 100644 --- a/src/app/(theme)/connect/saved-connection-card.tsx +++ b/src/app/(theme)/connect/saved-connection-card.tsx @@ -6,7 +6,7 @@ import { ContextMenuItem, ContextMenuSeparator, } from "@/components/ui/context-menu"; -import { LucidePencil, LucideTrash, LucideUsers } from "lucide-react"; +import { LucidePencil, LucideTrash } from "lucide-react"; import { useState } from "react"; import { SavedConnectionItem, @@ -26,6 +26,8 @@ export default function ConnectionItemCard({ }>) { const [open, setOpen] = useState(false); + const DatabaseIcon = DRIVER_DETAIL[conn.driver ?? "turso"].icon; + return ( -
-
- {DRIVER_DETAIL[conn.driver -
-
-

{conn.name}

- {conn.shared ? ( -

- - Shared by {conn.shared.sharedBy.name} -

- ) : ( -

- {conn.description || No description} -

- )} +
+
+
+ +
+ +
+
{conn.name}
+
+ {DRIVER_DETAIL[conn.driver ?? "turso"].displayName} +
+
+
+ +
+
+
+ {conn.description || "No description"} +
+
diff --git a/src/app/(theme)/connect/saved-connection-content.tsx b/src/app/(theme)/connect/saved-connection-content.tsx index 75c732d1..08c33b31 100644 --- a/src/app/(theme)/connect/saved-connection-content.tsx +++ b/src/app/(theme)/connect/saved-connection-content.tsx @@ -19,6 +19,8 @@ export default function ConnectionDialogContent({ driver: SupportedDriver; }> >) { + const DatabaseIcon = DRIVER_DETAIL[driver ?? "turso"].icon; + return ( -
- - {title} +
+ +
{title}
diff --git a/src/app/(theme)/connect/saved-connection-storage.ts b/src/app/(theme)/connect/saved-connection-storage.ts index 59702360..18cf813e 100644 --- a/src/app/(theme)/connect/saved-connection-storage.ts +++ b/src/app/(theme)/connect/saved-connection-storage.ts @@ -1,5 +1,13 @@ +import { + CloudflareIcon, + RqliteIcon, + SQLiteIcon, + TursoIcon, + ValtownIcon, +} from "@/components/icons/outerbase-icon"; import { ApiUser } from "@/lib/api/api-database-response"; import parseSafeJson from "@/lib/json-safe"; +import { FunctionComponent } from "react"; export interface DriverDetailField { name: keyof SavedConnectionItemConfigConfig; @@ -14,8 +22,9 @@ export interface DriverDetailField { } export interface DriverDetail { + displayName: string; name: string; - icon: string; + icon: FunctionComponent<{ className: string }>; disableRemote?: boolean; fields: DriverDetailField[]; } @@ -23,8 +32,9 @@ export interface DriverDetail { export const DRIVER_DETAIL: Record = Object.freeze({ "sqlite-filehandler": { + displayName: "SQLite", name: "sqlite-filehandler", - icon: "/sqlite-icon.svg", + icon: SQLiteIcon, disableRemote: true, fields: [ { @@ -38,7 +48,8 @@ export const DRIVER_DETAIL: Record = }, turso: { name: "turso", - icon: "/turso.jpeg", + displayName: "Turso", + icon: TursoIcon, fields: [ { name: "url", @@ -67,7 +78,8 @@ export const DRIVER_DETAIL: Record = }, valtown: { name: "valtown", - icon: "/valtown.svg", + displayName: "Valtown", + icon: ValtownIcon, prefill: "", fields: [ { @@ -81,7 +93,8 @@ export const DRIVER_DETAIL: Record = }, "cloudflare-d1": { name: "cloudflare-d1", - icon: "/cloudflare.png", + displayName: "Cloudflare D1", + icon: CloudflareIcon, fields: [ { name: "username", @@ -108,7 +121,8 @@ export const DRIVER_DETAIL: Record = }, rqlite: { name: "rqlite", - icon: "/rqlite.png", + displayName: "rqlite", + icon: RqliteIcon, fields: [ { name: "url", diff --git a/src/app/(theme)/connect/saved-connection.tsx b/src/app/(theme)/connect/saved-connection.tsx index be9520d0..3c42edd3 100644 --- a/src/app/(theme)/connect/saved-connection.tsx +++ b/src/app/(theme)/connect/saved-connection.tsx @@ -13,13 +13,14 @@ import { import SavedConnectionConfig from "./saved-connection-config"; import { createDatabase } from "@/lib/api/fetch-databases"; import { User } from "lucia"; +import { WEBSITE_NAME } from "@/const"; type SaveConnectionStep = "storage" | "config"; export function RqliteInstruction() { return (
- You should include LibSQL Studio in the list of allowed origins for CORS + You should include {WEBSITE_NAME} in the list of allowed origins for CORS (Cross-Origin Resource Sharing)
         {`rqlited --http-allow-origin="https://libsqlstudio.com"`}
diff --git a/src/app/(theme)/playground/client/page-client.tsx b/src/app/(theme)/playground/client/page-client.tsx
index 6b4dcfbe..34bbc5a5 100644
--- a/src/app/(theme)/playground/client/page-client.tsx
+++ b/src/app/(theme)/playground/client/page-client.tsx
@@ -1,7 +1,6 @@
 "use client";
 import { saveAs } from "file-saver";
 import MyStudio from "@/components/my-studio";
-import { Button } from "@/components/ui/button";
 import SqljsDriver from "@/drivers/sqljs-driver";
 import { LucideFile, LucideLoader, LucideRefreshCw } from "lucide-react";
 import Script from "next/script";
@@ -10,14 +9,13 @@ import { Database, SqlJsStatic } from "sql.js";
 import ScreenDropZone from "@/components/screen-dropzone";
 import { toast } from "sonner";
 import downloadFileFromUrl from "@/lib/download-file";
-import {
-  Tooltip,
-  TooltipContent,
-  TooltipTrigger,
-} from "@/components/ui/tooltip";
 import { useSearchParams } from "next/navigation";
 import { localDb } from "@/indexdb";
 import { SavedConnectionLocalStorage } from "@/app/(theme)/connect/saved-connection-storage";
+import {
+  DropdownMenuItem,
+  DropdownMenuSeparator,
+} from "@/components/ui/dropdown-menu";
 
 export default function PlaygroundEditorBody({
   preloadDatabase,
@@ -132,102 +130,80 @@ export default function PlaygroundEditorBody({
             
)} -
- - - {handler && ( - - - - - -

- Refresh -

+ }, + ], + }) + .then(([fileHandler]) => { + setHandler(fileHandler); + }); + }} + > + Open SQLite file + -

- LibSQL Studio loads data into memory. If the file changes, it - is unaware of the update. -

+ {handler && ( + + + Refresh + + )} -

- To reflect the changes, use the refresh option to reload the - data from the file. -

-
-
- )} -
+
); }, [rawDb, handler, db, fileName, onReloadDatabase]); diff --git a/src/app/globals.css b/src/app/globals.css index 2a44e78c..9debf4fe 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -44,7 +44,7 @@ body { --secondary: #f1f5f9; --secondary-foreground: #444; - --muted: 210 40% 96.1%; + --muted: #eee; --muted-foreground: #888; --accent: #f1f5f9; @@ -64,7 +64,7 @@ body { } .dark { - --background: #181818; + --background: #000; --foreground: #aaa; --color-surface: black; @@ -79,7 +79,7 @@ body { --primary: white; --primary-foreground: black; - --secondary: #1f1f1f; + --secondary: #171717; --secondary-foreground: #eee; --muted: #333; @@ -91,7 +91,7 @@ body { --destructive: red; --destructive-foreground: white; - --border: #2b2b2b; + --border: #313131; --input: #333; --ring: #444; @@ -140,6 +140,10 @@ body { color: #e84393; } +.cm-focused { + outline: none !important; +} + .dark .cm-table-name { color: #fd79a8; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 35afb88c..06ca143d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,12 +3,12 @@ import type { Metadata } from "next"; import "./globals.css"; import "./codemirror-override.css"; import { Fragment } from "react"; +import { WEBSITE_NAME } from "@/const"; -const siteDescription = - "LibSQL Studio is a fully-featured, lightweight GUI client for managing SQLite-based databases like Turso, LibSQL, and rqlite. It runs entirely in your browser, so there's no need to download anything"; +const siteDescription = `${WEBSITE_NAME} is a fully-featured, lightweight GUI client for managing SQLite-based databases like Turso, LibSQL, and rqlite. It runs entirely in your browser, so there's no need to download anything`; export const metadata: Metadata = { - title: "LibSQL Studio", + title: WEBSITE_NAME, keywords: [ "libsql", "rqlite", @@ -22,7 +22,7 @@ export const metadata: Metadata = { ], description: siteDescription, openGraph: { - siteName: "LibSQL Studio", + siteName: WEBSITE_NAME, description: siteDescription, }, }; diff --git a/src/components/gui/database-gui.tsx b/src/components/gui/database-gui.tsx index b5670cf6..aa79eb98 100644 --- a/src/components/gui/database-gui.tsx +++ b/src/components/gui/database-gui.tsx @@ -11,12 +11,6 @@ import useMessageListener from "@/components/hooks/useMessageListener"; import { MessageChannelName } from "@/const"; import { OpenTabsProps, receiveOpenTabMessage } from "@/messages/open-tab"; import QueryWindow from "@/components/gui/tabs/query-tab"; -import { - LucideBookmark, - LucideCode, - LucideDatabase, - LucideSettings, -} from "lucide-react"; import SidebarTab, { SidebarTabItem } from "./sidebar-tab"; import SchemaView from "./schema-sidebar"; import SettingSidebar from "./sidebar/setting-sidebar"; @@ -24,6 +18,7 @@ import SettingSidebar from "./sidebar/setting-sidebar"; import { useDatabaseDriver } from "@/context/driver-provider"; import SavedDocTab from "./sidebar/saved-doc-tab"; import { useSchema } from "@/context/schema-provider"; +import { Binoculars, GearSix, Table } from "@phosphor-icons/react"; export default function DatabaseGui() { const DEFAULT_WIDTH = 300; @@ -44,7 +39,7 @@ export default function DatabaseGui() { identifier: "query", key: "query", component: , - icon: LucideCode, + icon: Binoculars, }, ]); @@ -93,14 +88,14 @@ export default function DatabaseGui() { key: "database", name: "Schema", content: , - icon: LucideDatabase, + icon: , }, docDriver ? { key: "saved", - name: "Saved", + name: "Queries", content: , - icon: LucideBookmark, + icon: , } : undefined, collaborationDriver @@ -108,7 +103,7 @@ export default function DatabaseGui() { key: "setting", name: "Setting", content: , - icon: LucideSettings, + icon: , } : undefined, ].filter(Boolean) as SidebarTabItem[]; diff --git a/src/components/gui/main-connection.tsx b/src/components/gui/main-connection.tsx index d499caa8..55fc1cfc 100644 --- a/src/components/gui/main-connection.tsx +++ b/src/components/gui/main-connection.tsx @@ -8,6 +8,7 @@ import { useConfig } from "@/context/config-provider"; import { AutoCompleteProvider } from "@/context/auto-complete-provider"; import InternalPubSub from "@/components/lib/internal-pubsub"; import { SchemaProvider } from "@/context/schema-provider"; +import { WEBSITE_NAME } from "@/const"; function MainConnection() { const { databaseDriver: driver } = useDatabaseDriver(); @@ -41,7 +42,7 @@ function MainConnectionContainer() { }, [driver]); useEffect(() => { - document.title = name + " - LibSQL Studio"; + document.title = name + " - " + WEBSITE_NAME; }, [name]); return ( diff --git a/src/components/gui/query-explanation.tsx b/src/components/gui/query-explanation.tsx index a6e2b1cd..8f79f974 100644 --- a/src/components/gui/query-explanation.tsx +++ b/src/components/gui/query-explanation.tsx @@ -49,9 +49,14 @@ function buildQueryExplanationTree(nodes: ExplanationRow[]) { export function QueryExplanation(props: QueryExplanationProps) { const tree = useMemo(() => { - const isExplanationRows = z - .array(queryExplanationRowSchema) - .safeParse(props.data.rows); + const isExplanationRows = z.array(queryExplanationRowSchema).safeParse( + props.data.rows.map((r) => ({ + ...r, + id: Number(r.id), + parent: Number(r.parent), + notused: Number(r.notused), + })) + ); if (isExplanationRows.error) { return { _tag: "ERROR" as const, value: isExplanationRows.error }; diff --git a/src/components/gui/save-doc-button.tsx b/src/components/gui/save-doc-button.tsx index c23fc7d0..a622fa88 100644 --- a/src/components/gui/save-doc-button.tsx +++ b/src/components/gui/save-doc-button.tsx @@ -82,8 +82,8 @@ export default function SaveDocButton({ + + +
@@ -37,17 +53,6 @@ export default function SchemaView() {
- -
- -
- -
-
); } diff --git a/src/components/gui/sidebar-tab.tsx b/src/components/gui/sidebar-tab.tsx index cd8283b0..563e78da 100644 --- a/src/components/gui/sidebar-tab.tsx +++ b/src/components/gui/sidebar-tab.tsx @@ -1,17 +1,21 @@ import { useConfig } from "@/context/config-provider"; import { useTheme } from "@/context/theme-provider"; import { cn } from "@/lib/utils"; -import { - LucideChevronLeft, - LucideIcon, - LucideMoon, - LucideSun, -} from "lucide-react"; import { ReactElement, useState } from "react"; +import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"; +import Link from "next/link"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "../ui/dropdown-menu"; +import { ArrowLeft, MoonStars, Sun } from "@phosphor-icons/react"; export interface SidebarTabItem { key: string; - icon: LucideIcon; + icon: ReactElement; name: string; content: ReactElement; } @@ -45,9 +49,9 @@ export default function SidebarTab({ tabs }: Readonly) { } return ( -
-
-
+
+
+ {/*
{config.onBack && (
) {
)} -
- - {config.sideBarFooterComponent && ( -
{config.sideBarFooterComponent}
- )} +
*/} -
-
- {tabs.map(({ key, name, icon: Icon }, idx) => { - return ( - +
+
Outerbase Studio
+
+ v{process.env.NEXT_PUBLIC_STUDIO_VERSION} +
+
+
+ + {config.sideBarFooterComponent} + + {!disableToggle && ( + { + toggleTheme(); + }} + > + {theme === "dark" ? ( + + ) : ( + + )} + Switch to {theme === "dark" ? "light mode" : "dark mode"} + + )} + {config.onBack && ( + + + Back to bases + + )} + {config.onBack && !disableToggle && } + + + Report issues + + + + + About us + + + + + + {tabs.map(({ key, name, icon }, idx) => { + return ( + + + + + {name} + ); })} -
+
{tabs.map((tab, tabIndex) => { const selected = selectedIndex === tabIndex; diff --git a/src/components/gui/sidebar/saved-doc-tab/create-namespace-button.tsx b/src/components/gui/sidebar/saved-doc-tab/create-namespace-button.tsx index 77bbcdfc..005af665 100644 --- a/src/components/gui/sidebar/saved-doc-tab/create-namespace-button.tsx +++ b/src/components/gui/sidebar/saved-doc-tab/create-namespace-button.tsx @@ -5,25 +5,24 @@ import { DialogDescription, DialogFooter, DialogTitle, - DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { useDatabaseDriver } from "@/context/driver-provider"; import { SavedDocNamespace } from "@/drivers/saved-doc/saved-doc-driver"; -import { LucidePlus } from "lucide-react"; import { useCallback, useState } from "react"; interface CreateNamespaceButtonProps { onCreated: (v: SavedDocNamespace) => void; + onClose: () => void; } -export default function CreateNamespaceButton({ +export default function CreateNamespaceDialog({ onCreated, + onClose, }: CreateNamespaceButtonProps) { const { docDriver } = useDatabaseDriver(); const [namespace, setNamespace] = useState(""); const [error, setError] = useState(""); - const [open, setOpen] = useState(false); const onCreateNamespace = useCallback(() => { if (docDriver) { @@ -32,7 +31,7 @@ export default function CreateNamespaceButton({ .createNamespace(namespace) .then((n) => { onCreated(n); - setOpen(false); + onClose(); }) .catch((e) => { setError((e as Error).message); @@ -41,50 +40,36 @@ export default function CreateNamespaceButton({ setError("The namespace name must be at least 3 characters long"); } } - }, [docDriver, namespace, onCreated]); + }, [docDriver, namespace, onCreated, onClose]); return ( - <> - { - setOpen(openState); - setNamespace(""); - setError(""); - }} - > - - - - - Create Namespace + { + if (!openState) onClose(); + }} + > + + Create Namespace - - A namespace is similar to a folder that groups your work together. - It helps you organize and arrange your queries - + + A namespace is similar to a folder that groups your work together. It + helps you organize and arrange your queries + - setNamespace(e.currentTarget.value)} - /> + setNamespace(e.currentTarget.value)} + /> - {error &&
{error}
} + {error &&
{error}
} - - - -
-
- + + + +
+
); } diff --git a/src/components/gui/sidebar/saved-doc-tab/index.tsx b/src/components/gui/sidebar/saved-doc-tab/index.tsx index 4b551077..bbdffe58 100644 --- a/src/components/gui/sidebar/saved-doc-tab/index.tsx +++ b/src/components/gui/sidebar/saved-doc-tab/index.tsx @@ -6,15 +6,23 @@ import { SavedDocNamespace, } from "@/drivers/saved-doc/saved-doc-driver"; import { ListView, ListViewItem } from "@/components/listview"; -import { LucideCode, LucideFolder, LucideTrash } from "lucide-react"; +import { LucideTrash } from "lucide-react"; import { useCallback, useEffect, useState } from "react"; -import CreateNamespaceButton from "./create-namespace-button"; import RenameNamespaceDialog from "./rename-namespace-dialog"; import RemoveDocDialog from "./remove-doc-dialog"; import { TAB_PREFIX_SAVED_QUERY } from "@/const"; import RemoveNamespaceDialog from "./remove-namespace-dialog"; import { OpenContextMenuList } from "@/messages/open-context-menu"; -import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; +import { Binoculars, Folder, Plus } from "@phosphor-icons/react"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import CreateNamespaceDialog from "./create-namespace-button"; type SavedDocListData = | { @@ -33,13 +41,13 @@ function mapDoc( return { data: { type: "namespace", data: ns.namespace }, key: ns.namespace.id, - icon: LucideFolder, + icon: Folder, name: ns.namespace.name, children: ns.docs.map((d) => { return { key: d.id, data: { type: "doc", data: d }, - icon: LucideCode, + icon: Binoculars, name: d.name, }; }) as ListViewItem[], @@ -52,6 +60,7 @@ export default function SavedDocTab() { const [selected, setSelected] = useState(); const [collapsed, setCollapsed] = useState>(new Set()); + const [namespaceCreating, setNamespaceCreating] = useState(false); const [namespaceToRename, setNamespaceToRename] = useState(); const [namespaceToRemove, setNamespaceToRemove] = @@ -133,15 +142,52 @@ export default function SavedDocTab() { ); } + if (namespaceCreating) { + dialog = ( + setNamespaceCreating(false)} + /> + ); + } + return ( <> {dialog} -
-
- +
+
+

Queries

+ + + + + + + setNamespaceCreating(true)}> + New Folder + + { + openTab({ + type: "query", + }); + }} + > + New Query + + +
- void; onClose?: () => void; } @@ -19,24 +21,34 @@ type WindowTabItemButtonProps = ButtonProps & { icon: LucideIcon; onClose?: () => void; isDragging?: boolean; + index: number; + tabCount: number; }; export const WindowTabItemButton = forwardRef< HTMLButtonElement, WindowTabItemButtonProps >(function WindowTabItemButton(props: WindowTabItemButtonProps, ref) { - const { icon: Icon, selected, title, onClose, isDragging, ...rest } = props; + const { + icon: Icon, + selected, + title, + onClose, + isDragging, + index, + ...rest + } = props; return ( + + + + + + onRunClicked()}> + Run Current Statement + + onRunClicked(true)}> + Run All Statements + + + onRunClicked(false, true)}> + Explain Current Statement + + + +
+
-
+
- -
- - - +
+
+
Ln {lineNumber}
+
Col {columnNumber + 1}
+
@@ -287,19 +336,6 @@ export default function QueryWindow({

Format SQL queries for readability

- -
-
Ln {lineNumber}
-
Col {columnNumber + 1}
-
- - {docDriver && ( - - )}
diff --git a/src/components/gui/windows-tab.tsx b/src/components/gui/windows-tab.tsx index d27586ae..7061ec33 100644 --- a/src/components/gui/windows-tab.tsx +++ b/src/components/gui/windows-tab.tsx @@ -147,31 +147,8 @@ export default function WindowTabs({ modifiers={[restrictToHorizontalAxis]} >
-
-
- {menu ? ( - - -
- -
-
- - {menu.map((menuItem, menuIdx) => { - return ( - - {menuItem.text} - - ); - })} - -
- ) : ( -
- )} +
+
tab.key)} strategy={horizontalListSortingStrategy} @@ -179,6 +156,8 @@ export default function WindowTabs({ {tabs.map((tab, idx) => ( { @@ -204,6 +183,32 @@ export default function WindowTabs({ /> ))} + + {menu && ( +
+ + +
+ New +
+
+ + {menu.map((menuItem, menuIdx) => { + return ( + + {menuItem.text} + + ); + })} + +
+
+ )} + +
diff --git a/src/components/icons/outerbase-icon.tsx b/src/components/icons/outerbase-icon.tsx index ef8b7cf5..07a3d7fb 100644 --- a/src/components/icons/outerbase-icon.tsx +++ b/src/components/icons/outerbase-icon.tsx @@ -1,8 +1,9 @@ -export function MySQLIcon() { +import { cn } from "@/lib/utils"; + +export function MySQLIcon({ className }: { className?: string }) { return ( + + + ); +} + +export function CloudflareIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + + + ); +} + +export function ValtownIcon({ className }: { className?: string }) { + return ( + + + + + + + + ); +} + +export function RqliteIcon({ className }: { className?: string }) { + return ( + + + + + + + + + + + + + + + + + ); +} + +export function SQLiteIcon({ className }: { className?: string }) { return (
- LibSQLStudio + Outerbase Studio
{title}
diff --git a/src/components/website-layout.tsx b/src/components/website-layout.tsx index 13a7bac6..ec5158ae 100644 --- a/src/components/website-layout.tsx +++ b/src/components/website-layout.tsx @@ -12,8 +12,8 @@ async function Topbar() {
-

- LibSQL Studio +

+ Outerbase Studio

@@ -51,8 +51,8 @@ function Footer() {
-

- LibSQL Studio +

+ Outerbase Studio

© 2024 Outerbase Inc.

diff --git a/src/const.ts b/src/const.ts index cb287159..ff7f6bbf 100644 --- a/src/const.ts +++ b/src/const.ts @@ -5,3 +5,5 @@ export enum MessageChannelName { } export const TAB_PREFIX_SAVED_QUERY = "saved-query-"; +export const WEBSITE_NAME = "Outerbase Studio"; +export const WEBSITE_GENERAL_DESCRIPTION = `${WEBSITE_NAME} is a fully-featured, lightweight GUI client for managing SQLite-based databases like Turso, LibSQL, and rqlite. It runs entirely in your browser, so there's no need to download anything`; diff --git a/src/messages/open-tab.tsx b/src/messages/open-tab.tsx index c585ad9c..f3082534 100644 --- a/src/messages/open-tab.tsx +++ b/src/messages/open-tab.tsx @@ -2,7 +2,6 @@ import type { WindowTabItemProps } from "@/components/gui/windows-tab"; import { MessageChannelName, TAB_PREFIX_SAVED_QUERY } from "../const"; import type { Dispatch, SetStateAction } from "react"; import { - LucideCode, LucideTable, LucideTableProperties, LucideUser, @@ -13,6 +12,7 @@ import SchemaEditorTab from "@/components/gui/tabs/schema-editor-tab"; import TableDataWindow from "@/components/gui/tabs/table-data-tab"; import UsersTab from "@/components/gui/tabs/users-tabs"; import TriggerTab from "@/components/gui/tabs/trigger-tab"; +import { Binoculars } from "@phosphor-icons/react/dist/ssr"; interface OpenTableTab { type: "table"; @@ -82,7 +82,7 @@ function generateKeyFromTab(tab: OpenTabsProps) { } function generateIconFromTab(tab: OpenTabsProps) { - if (tab.type === "query") return LucideCode; + if (tab.type === "query") return Binoculars; if (tab.type === "table") return LucideTable; if (tab.type === "schema") return LucideTableProperties; if (tab.type === "user") return LucideUser;