diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 00000000..18c91471
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to a positive environment for our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+ and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+ overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+ advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+ address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
+.
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior, harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..80cab4eb
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,11 @@
+# Security Policy
+
+The Outerbase Studio team takes security seriously. If you discover a security issue, please report it immediately. Your security reports are highly valued!
+
+## Reporting a Vulnerability
+
+Please DO NOT create a public issue. Instead, report the issue privately by emailing invisal@outerbase.com.
+
+## Supported Version
+
+We exclusively support the latest release and the main branch.
diff --git a/package-lock.json b/package-lock.json
index 153a6dd7..83989cf0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -49,6 +49,7 @@
"@tiptap/core": "^2.3.0",
"@tiptap/react": "^2.3.0",
"@types/mdx": "^2.0.13",
+ "@types/react-grid-layout": "^1.3.5",
"@uiw/codemirror-extensions-langs": "^4.21.24",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
@@ -63,6 +64,7 @@
"dexie": "^4.0.8",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.1",
+ "echarts": "^5.6.0",
"eslint-plugin-jest": "^27.6.3",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
@@ -72,12 +74,15 @@
"lucia": "^3.2.0",
"lucide-react": "^0.474.0",
"next": "15.1.4",
+ "next-themes": "^0.4.4",
"oslo": "^1.1.3",
"react": "19.0.0",
"react-dom": "19.0.0",
+ "react-grid-layout": "^1.5.0",
"react-resizable-panels": "^2.1.7",
"sonner": "^1.4.41",
"sql-formatter": "^15.3.2",
+ "swr": "^2.3.0",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"use-immer": "^0.11.0",
@@ -6700,6 +6705,14 @@
"@types/react": "^18.0.0"
}
},
+ "node_modules/@types/react-grid-layout": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/@types/react-grid-layout/-/react-grid-layout-1.3.5.tgz",
+ "integrity": "sha512-WH/po1gcEcoR6y857yAnPGug+ZhkF4PaTUxgAbwfeSH/QOgVSakKHBXoPGad/sEznmkiaK3pqHk+etdWisoeBQ==",
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
@@ -9108,6 +9121,20 @@
"node": ">= 0.4"
}
},
+ "node_modules/echarts": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
+ "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
+ "dependencies": {
+ "tslib": "2.3.0",
+ "zrender": "5.6.1"
+ }
+ },
+ "node_modules/echarts/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.88",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.88.tgz",
@@ -10469,6 +10496,11 @@
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
},
+ "node_modules/fast-equals": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
+ "integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg=="
+ },
"node_modules/fast-glob": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
@@ -13097,7 +13129,6 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "devOptional": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -13667,7 +13698,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
- "dev": true,
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
@@ -14830,6 +14860,16 @@
}
}
},
+ "node_modules/next-themes": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.4.tgz",
+ "integrity": "sha512-LDQ2qIOJF0VnuVrrMSMLrWGjRMkq+0mpgl6e0juCLqdJ+oo8Q84JRWT6Wh11VDQKkMMe+dVzDKLWs5n87T+PkQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/next-tick": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
@@ -14950,7 +14990,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -15610,7 +15649,6 @@
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
@@ -15622,7 +15660,6 @@
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/property-information": {
@@ -15964,6 +16001,44 @@
"react": "^19.0.0"
}
},
+ "node_modules/react-draggable": {
+ "version": "4.4.6",
+ "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.4.6.tgz",
+ "integrity": "sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==",
+ "dependencies": {
+ "clsx": "^1.1.1",
+ "prop-types": "^15.8.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0",
+ "react-dom": ">= 16.3.0"
+ }
+ },
+ "node_modules/react-draggable/node_modules/clsx": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
+ "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/react-grid-layout": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-1.5.0.tgz",
+ "integrity": "sha512-WBKX7w/LsTfI99WskSu6nX2nbJAUD7GD6nIXcwYLyPpnslojtmql2oD3I2g5C3AK8hrxIarYT8awhuDIp7iQ5w==",
+ "dependencies": {
+ "clsx": "^2.0.0",
+ "fast-equals": "^4.0.3",
+ "prop-types": "^15.8.1",
+ "react-draggable": "^4.4.5",
+ "react-resizable": "^3.0.5",
+ "resize-observer-polyfill": "^1.5.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3.0",
+ "react-dom": ">= 16.3.0"
+ }
+ },
"node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
@@ -16019,6 +16094,18 @@
}
}
},
+ "node_modules/react-resizable": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.0.5.tgz",
+ "integrity": "sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==",
+ "dependencies": {
+ "prop-types": "15.x",
+ "react-draggable": "^4.0.3"
+ },
+ "peerDependencies": {
+ "react": ">= 16.3"
+ }
+ },
"node_modules/react-resizable-panels": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz",
@@ -16285,6 +16372,11 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="
+ },
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -17261,6 +17353,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/swr": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.0.tgz",
+ "integrity": "sha512-NyZ76wA4yElZWBHzSgEJc28a0u6QZvhb6w0azeL2k7+Q1gAzVK+IqQYXhVOC/mzi+HZIozrZvBVeSeOZNR2bqA==",
+ "license": "MIT",
+ "dependencies": {
+ "dequal": "^2.0.3",
+ "use-sync-external-store": "^1.4.0"
+ },
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -18862,6 +18967,19 @@
"url": "https://github.com/sponsors/colinhacks"
}
},
+ "node_modules/zrender": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
+ "integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
+ "dependencies": {
+ "tslib": "2.3.0"
+ }
+ },
+ "node_modules/zrender/node_modules/tslib": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
+ "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
+ },
"node_modules/zustand": {
"version": "4.5.6",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.6.tgz",
diff --git a/package.json b/package.json
index 19635557..3fb9fa50 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,8 @@
"format:fix": "prettier --write .",
"lint": "next lint",
"lint:fix": "next lint --fix .",
- "dialect": "node build-dialect.js"
+ "dialect": "node build-dialect.js",
+ "remove-branch": "npx git-removed-branches --prune -f"
},
"overrides": {
"@libsql/client": "^0.5.3"
@@ -68,6 +69,7 @@
"@tiptap/core": "^2.3.0",
"@tiptap/react": "^2.3.0",
"@types/mdx": "^2.0.13",
+ "@types/react-grid-layout": "^1.3.5",
"@uiw/codemirror-extensions-langs": "^4.21.24",
"@uiw/codemirror-themes": "^4.21.21",
"@uiw/react-codemirror": "^4.21.21",
@@ -82,6 +84,7 @@
"dexie": "^4.0.8",
"dotenv": "^16.4.5",
"drizzle-orm": "^0.30.1",
+ "echarts": "^5.6.0",
"eslint-plugin-jest": "^27.6.3",
"file-saver": "^2.0.5",
"html-to-image": "^1.11.11",
@@ -91,12 +94,15 @@
"lucia": "^3.2.0",
"lucide-react": "^0.474.0",
"next": "15.1.4",
+ "next-themes": "^0.4.4",
"oslo": "^1.1.3",
"react": "19.0.0",
"react-dom": "19.0.0",
+ "react-grid-layout": "^1.5.0",
"react-resizable-panels": "^2.1.7",
"sonner": "^1.4.41",
"sql-formatter": "^15.3.2",
+ "swr": "^2.3.0",
"tailwind-merge": "^2.2.2",
"tailwindcss-animate": "^1.0.7",
"use-immer": "^0.11.0",
diff --git a/public/assets/login-planet.png b/public/assets/login-planet.png
new file mode 100644
index 00000000..d6b1e9db
Binary files /dev/null and b/public/assets/login-planet.png differ
diff --git a/public/assets/login-portal.png b/public/assets/login-portal.png
new file mode 100644
index 00000000..ea1c0562
Binary files /dev/null and b/public/assets/login-portal.png differ
diff --git a/public/assets/login-stars.png b/public/assets/login-stars.png
new file mode 100644
index 00000000..00143153
Binary files /dev/null and b/public/assets/login-stars.png differ
diff --git a/src/app/(outerbase)/navigation.tsx b/src/app/(outerbase)/navigation.tsx
new file mode 100644
index 00000000..f9d2cc71
--- /dev/null
+++ b/src/app/(outerbase)/navigation.tsx
@@ -0,0 +1,93 @@
+import { getDatabaseIcon } from "@/components/resource-card/utils";
+import { Button, buttonVariants } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { cn } from "@/lib/utils";
+import { useParams, useRouter } from "next/navigation";
+import { useMemo, useState } from "react";
+import { useWorkspaces } from "./workspace-provider";
+
+function WorkspaceSelector() {
+ const router = useRouter();
+ const { workspaces } = useWorkspaces();
+ const { workspaceId } = useParams<{ workspaceId: string }>();
+ const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(workspaceId);
+
+ const selectedWorkspace = workspaces.find(
+ (w) => w.id === selectedWorkspaceId || w.short_name === selectedWorkspaceId
+ );
+
+ const bases = useMemo(() => {
+ const currentBases = [...(selectedWorkspace?.bases ?? [])];
+ currentBases.sort((a, b) => a.name.localeCompare(b.name));
+ return currentBases;
+ }, [selectedWorkspace]);
+
+ return (
+
+
+
+ WORKSPACES
+
+ {workspaces.map((workspace) => (
+
router.push(`/w/${workspace.short_name}`)}
+ key={workspace.id}
+ onMouseEnter={() => setSelectedWorkspaceId(workspace.short_name)}
+ className={cn(
+ buttonVariants({ variant: "ghost", size: "sm" }),
+ "cursor-pointer justify-start py-0.5"
+ )}
+ >
+ {workspace.name}
+
+ ))}
+
+
+
+ BASES
+
+ {bases.map((base) => {
+ const IconComponent = getDatabaseIcon(base.sources[0]?.type);
+
+ return (
+
+ router.push(`/w/${selectedWorkspaceId}/${base.short_name}`, {})
+ }
+ key={base.id}
+ className={cn(
+ buttonVariants({
+ variant: "ghost",
+ size: "sm",
+ }),
+ "cursor-pointer justify-start p-2"
+ )}
+ >
+
+ {base.name}
+
+ );
+ })}
+
+
+ );
+}
+
+export function NavigationBar() {
+ return (
+
+
+
+ Workspace
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(outerbase)/session-provider.tsx b/src/app/(outerbase)/session-provider.tsx
new file mode 100644
index 00000000..8cbf1fd1
--- /dev/null
+++ b/src/app/(outerbase)/session-provider.tsx
@@ -0,0 +1,56 @@
+"use client";
+import { useRouter } from "next/navigation";
+import { createContext, PropsWithChildren, useContext } from "react";
+import useSWR from "swr";
+import { getOuterbaseSession } from "../../outerbase-cloud/api";
+import {
+ OuterbaseAPISession,
+ OuterbaseAPIUser,
+} from "../../outerbase-cloud/api-type";
+
+interface OuterebaseSessionContextProps {
+ session: OuterbaseAPISession;
+ user: OuterbaseAPIUser;
+}
+
+const OuterbaseSessionContext = createContext<{
+ session: OuterbaseAPISession;
+ user: OuterbaseAPIUser;
+}>({} as OuterebaseSessionContextProps);
+
+export function useSession() {
+ return useContext(OuterbaseSessionContext);
+}
+
+export function OuterbaseSessionProvider({ children }: PropsWithChildren) {
+ const router = useRouter();
+
+ const { data, isLoading } = useSWR(
+ "session",
+ () => {
+ return getOuterbaseSession();
+ },
+ {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
+ );
+
+ if (isLoading) {
+ return Session Loading...
;
+ }
+
+ if (!data?.session || !data?.user) {
+ router.push("/signin");
+ return Redirecting...
;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page-client.tsx
similarity index 52%
rename from src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx
rename to src/app/(outerbase)/w/[workspaceId]/[baseId]/page-client.tsx
index 2b2ffeab..f1d4d8b8 100644
--- a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx
+++ b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page-client.tsx
@@ -2,11 +2,18 @@
import OpacityLoading from "@/components/gui/loading-opacity";
import { Studio } from "@/components/gui/studio";
+import { StudioExtensionManager } from "@/core/extension-manager";
+import {
+ createMySQLExtensions,
+ createPostgreSQLExtensions,
+ createSQLiteExtensions,
+} from "@/core/standard-extension";
import { getOuterbaseBase } from "@/outerbase-cloud/api";
import { OuterbaseAPISource } from "@/outerbase-cloud/api-type";
import { OuterbaseMySQLDriver } from "@/outerbase-cloud/database/mysql";
import { OuterbasePostgresDriver } from "@/outerbase-cloud/database/postgresql";
import { OuterbaseSqliteDriver } from "@/outerbase-cloud/database/sqlite";
+import OuterbaseQueryDriver from "@/outerbase-cloud/query-driver";
import { useEffect, useMemo, useState } from "react";
export default function OuterbaseSourcePageClient({
@@ -28,8 +35,13 @@ export default function OuterbaseSourcePageClient({
});
}, [workspaceId, baseId]);
- const outerbaseDriver = useMemo(() => {
- if (!workspaceId || !source) return null;
+ const savedDocDriver = useMemo(() => {
+ if (!workspaceId || !source?.id || !baseId) return null;
+ return new OuterbaseQueryDriver(workspaceId, baseId, source.id);
+ }, [workspaceId, baseId, source?.id]);
+
+ const [outerbaseDriver, extensions] = useMemo(() => {
+ if (!workspaceId || !source) return [null, null];
const dialect = source.type;
const outerbaseConfig = {
@@ -40,19 +52,34 @@ export default function OuterbaseSourcePageClient({
};
if (dialect === "postgres") {
- return new OuterbasePostgresDriver(outerbaseConfig);
+ return [
+ new OuterbasePostgresDriver(outerbaseConfig),
+ new StudioExtensionManager(createPostgreSQLExtensions()),
+ ];
} else if (dialect === "mysql") {
- return new OuterbaseMySQLDriver(outerbaseConfig);
+ return [
+ new OuterbaseMySQLDriver(outerbaseConfig),
+ new StudioExtensionManager(createMySQLExtensions()),
+ ];
}
- return new OuterbaseSqliteDriver(outerbaseConfig);
+ return [
+ new OuterbaseSqliteDriver(outerbaseConfig),
+ new StudioExtensionManager(createSQLiteExtensions()),
+ ];
}, [workspaceId, source]);
- if (!outerbaseDriver) {
+ if (!outerbaseDriver || !savedDocDriver) {
return ;
}
return (
-
+
);
}
diff --git a/src/app/(theme)/w/[workspaceId]/[baseId]/page.tsx b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx
similarity index 59%
rename from src/app/(theme)/w/[workspaceId]/[baseId]/page.tsx
rename to src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx
index d72da9cb..b2908d20 100644
--- a/src/app/(theme)/w/[workspaceId]/[baseId]/page.tsx
+++ b/src/app/(outerbase)/w/[workspaceId]/[baseId]/page.tsx
@@ -1,6 +1,5 @@
import ClientOnly from "@/components/client-only";
import OuterbaseSourcePageClient from "./page-client";
-import ThemeLayout from "@/app/(theme)/theme_layout";
interface OuterbaseSourcePageProps {
params: Promise<{
@@ -15,13 +14,11 @@ export default async function OuterbaseSourcePage(
const params = await props.params;
return (
-
-
-
-
-
+
+
+
);
}
diff --git a/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page-client.tsx b/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page-client.tsx
new file mode 100644
index 00000000..78b65fde
--- /dev/null
+++ b/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page-client.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+export default function BoardPageClient({
+ workspaceId,
+ boardId,
+}: {
+ workspaceId: string;
+ boardId: string;
+}) {
+ return (
+
+ {workspaceId} {boardId}
+
+ );
+}
diff --git a/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page.tsx b/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page.tsx
new file mode 100644
index 00000000..253e83f1
--- /dev/null
+++ b/src/app/(outerbase)/w/[workspaceId]/board/[boardId]/page.tsx
@@ -0,0 +1,19 @@
+import ClientOnly from "@/components/client-only";
+import BoardPageClient from "./page-client";
+
+interface BoardPageProps {
+ params: Promise<{ workspaceId: string; boardId: string }>;
+}
+
+export default async function BoardPage(props: BoardPageProps) {
+ const params = await props.params;
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(outerbase)/w/[workspaceId]/layout.tsx b/src/app/(outerbase)/w/[workspaceId]/layout.tsx
new file mode 100644
index 00000000..f9fe8102
--- /dev/null
+++ b/src/app/(outerbase)/w/[workspaceId]/layout.tsx
@@ -0,0 +1,17 @@
+import { OuterbaseSessionProvider } from "@/app/(outerbase)/session-provider";
+import ThemeLayout from "../../../(theme)/theme_layout";
+import { WorkspaceProvider } from "../../workspace-provider";
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/(outerbase)/w/[workspaceId]/page-client.tsx b/src/app/(outerbase)/w/[workspaceId]/page-client.tsx
new file mode 100644
index 00000000..bf96afac
--- /dev/null
+++ b/src/app/(outerbase)/w/[workspaceId]/page-client.tsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { ButtonGroup, ButtonGroupItem } from "@/components/button-group";
+import { Toolbar } from "@/components/gui/toolbar";
+import ResourceCard from "@/components/resource-card";
+import {
+ getDatabaseFriendlyName,
+ getDatabaseIcon,
+ getDatabaseVisual,
+} from "@/components/resource-card/utils";
+import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
+import {
+ CalendarDots,
+ SortAscending,
+ SortDescending,
+} from "@phosphor-icons/react";
+import { useParams } from "next/navigation";
+import { NavigationBar } from "../../navigation";
+import { useWorkspaces } from "../../workspace-provider";
+
+export default function WorkspaceListPageClient() {
+ const { workspaces } = useWorkspaces();
+ const { workspaceId } = useParams<{ workspaceId: string }>();
+
+ // const boards = data?.boards ?? [];
+ const bases =
+ workspaces.find(
+ (workspace) =>
+ workspace.short_name === workspaceId || workspace.id === workspaceId
+ )?.bases ?? [];
+
+ return (
+
+ {/*
Board
+
+ {boards.map((board) => (
+
+ {board.name}
+
+ ))}
+
+
+
Base */}
+
+
+
+
+
+
+ All
+ Bases
+ Boards
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {bases.map((base) => (
+
+ Remove
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/app/(outerbase)/w/[workspaceId]/page.tsx b/src/app/(outerbase)/w/[workspaceId]/page.tsx
new file mode 100644
index 00000000..006063e3
--- /dev/null
+++ b/src/app/(outerbase)/w/[workspaceId]/page.tsx
@@ -0,0 +1,11 @@
+"use client";
+import ClientOnly from "@/components/client-only";
+import WorkspaceListPageClient from "./page-client";
+
+export default function WorkspaceListPage() {
+ return (
+
+
+
+ );
+}
diff --git a/src/app/(outerbase)/workspace-provider.tsx b/src/app/(outerbase)/workspace-provider.tsx
new file mode 100644
index 00000000..c9d24426
--- /dev/null
+++ b/src/app/(outerbase)/workspace-provider.tsx
@@ -0,0 +1,36 @@
+"use client";
+import { getOuterbaseWorkspace } from "@/outerbase-cloud/api";
+import { OuterbaseAPIWorkspace } from "@/outerbase-cloud/api-type";
+import { createContext, PropsWithChildren, useContext } from "react";
+import useSWR from "swr";
+
+const WorkspaceContext = createContext<{
+ workspaces: OuterbaseAPIWorkspace[];
+ loading: boolean;
+}>({ workspaces: [], loading: true });
+
+export function useWorkspaces() {
+ return useContext(WorkspaceContext);
+}
+
+export function WorkspaceProvider({ children }: PropsWithChildren) {
+ const { data, isLoading } = useSWR(
+ "workspaces",
+ () => {
+ return getOuterbaseWorkspace();
+ },
+ {
+ revalidateIfStale: false,
+ revalidateOnFocus: false,
+ revalidateOnReconnect: false,
+ }
+ );
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/app/(public)/docs/page.mdx b/src/app/(public)/docs/page.mdx
index 6e95dfd7..87d86cb9 100644
--- a/src/app/(public)/docs/page.mdx
+++ b/src/app/(public)/docs/page.mdx
@@ -6,8 +6,8 @@ export const metadata = {
-
LibSQL Studio is an extremely powerful and lightweight SQLite GUI that runs in your browser. It comes packed with a ton of features, including:
+
- A powerful data editor capable of handling thousands of rows and columns without overwhelming your RAM.
- A SQL query editor with syntax highlighting, tooltips, and auto-completion to boost your productivity.
- Advanced tools for editing your table schema and indexes.
diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx
index 74c50a31..0fca86e2 100644
--- a/src/app/(public)/layout.tsx
+++ b/src/app/(public)/layout.tsx
@@ -1,12 +1,5 @@
-import { Analytics } from "@vercel/analytics/react";
-import { Inter } from "next/font/google";
-import { Toaster } from "@/components/ui/sonner";
import { Fragment } from "react";
-import Script from "next/script";
-import { cn } from "@/lib/utils";
-import PageTracker from "@/components/page-tracker";
-
-const inter = Inter({ subsets: ["latin"] });
+import ThemeLayout from "../(theme)/theme_layout";
export default async function RootLayout({
children,
@@ -14,12 +7,8 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
return (
-
+
{children}
-
-
-
-
-
+
);
}
diff --git a/src/app/(theme)/connect/page-client.tsx b/src/app/(theme)/connect/page-client.tsx
index ddfc3e6a..675c098c 100644
--- a/src/app/(theme)/connect/page-client.tsx
+++ b/src/app/(theme)/connect/page-client.tsx
@@ -1,22 +1,19 @@
"use client";
+import ThemeToggle from "@/components/theme-toggle";
import { Button } from "@/components/ui/button";
-import ConnectionList from "./connection-list";
-import Link from "next/link";
-import { LucideMoon, LucideSun } from "lucide-react";
import { User } from "lucia";
-import { useTheme } from "@/context/theme-provider";
+import Link from "next/link";
+import ConnectionList from "./connection-list";
export default function ConnectBody({ user }: Readonly<{ user: User | null }>) {
- const { theme, toggleTheme } = useTheme();
-
return (
-
-
-
+
+
+
@@ -24,14 +21,8 @@ export default function ConnectBody({ user }: Readonly<{ user: User | null }>) {
-
-
toggleTheme()}>
- {theme === "dark" ? (
-
- ) : (
-
- )}
-
+
+
{user ? (
diff --git a/src/app/(theme)/connect/saved-connection-card.tsx b/src/app/(theme)/connect/saved-connection-card.tsx
index c4105f81..e8178f65 100644
--- a/src/app/(theme)/connect/saved-connection-card.tsx
+++ b/src/app/(theme)/connect/saved-connection-card.tsx
@@ -1,19 +1,12 @@
-import { cn } from "@/lib/utils";
+import ResourceCard from "@/components/resource-card";
import {
- ContextMenu,
- ContextMenuTrigger,
- ContextMenuContent,
- ContextMenuItem,
- ContextMenuSeparator,
-} from "@/components/ui/context-menu";
+ getDatabaseFriendlyName,
+ getDatabaseIcon,
+ getDatabaseVisual,
+} from "@/components/resource-card/utils";
+import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { LucidePencil, LucideTrash } from "lucide-react";
-import { useState } from "react";
-import {
- SavedConnectionItem,
- CONNECTION_LABEL_COLORS,
- DRIVER_DETAIL,
-} from "./saved-connection-storage";
-import Link from "next/link";
+import { SavedConnectionItem } from "./saved-connection-storage";
export default function ConnectionItemCard({
conn,
@@ -24,74 +17,29 @@ export default function ConnectionItemCard({
onEdit: () => void;
onRemove: () => void;
}>) {
- const [open, setOpen] = useState(false);
-
- const DatabaseIcon = DRIVER_DETAIL[conn.driver ?? "turso"].icon;
-
return (
-
- {
- if (conn.shared) {
- e.preventDefault();
- }
- }}
- >
-
-
-
-
-
-
-
-
-
- {conn.name}
-
-
- {DRIVER_DETAIL[conn.driver ?? "turso"].displayName}
-
-
-
-
-
-
-
- {conn.description || "No description"}
-
-
-
-
-
-
-
-
-
- Edit
-
-
-
- Remove
-
-
-
+
+
+
+ Edit
+
+
+
+ Remove
+
+
);
}
diff --git a/src/app/(theme)/embed/[driver]/page-client.tsx b/src/app/(theme)/embed/[driver]/page-client.tsx
new file mode 100644
index 00000000..bf3f533c
--- /dev/null
+++ b/src/app/(theme)/embed/[driver]/page-client.tsx
@@ -0,0 +1,90 @@
+"use client";
+import { Studio } from "@/components/gui/studio";
+import { StudioExtensionManager } from "@/core/extension-manager";
+import {
+ createMySQLExtensions,
+ createPostgreSQLExtensions,
+ createSQLiteExtensions,
+} from "@/core/standard-extension";
+import {
+ IframeMySQLDriver,
+ IframePostgresDriver,
+ IframeSQLiteDriver,
+} from "@/drivers/iframe-driver";
+import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
+import DoltExtension from "@/extensions/dolt";
+import { useSearchParams } from "next/navigation";
+import { useEffect, useMemo } from "react";
+
+export default function EmbedPageClient({
+ driverName,
+}: {
+ driverName: string;
+}) {
+ const searchParams = useSearchParams();
+
+ const driver = useMemo(() => {
+ return createDatabaseDriver(driverName);
+ }, [driverName]);
+
+ const savedDocDriver = useMemo(() => {
+ if (window.outerbaseIpc?.docs) {
+ return new ElectronSavedDocs();
+ }
+ }, []);
+
+ const extensions = useMemo(() => {
+ return new StudioExtensionManager(createEmbedExtensions(driverName));
+ }, [driverName]);
+
+ useEffect(() => {
+ return driver.listen();
+ }, [driver]);
+
+ return (
+
+ );
+}
+
+function createDatabaseDriver(driverName: string) {
+ if (driverName === "turso") {
+ return new IframeSQLiteDriver({
+ supportPragmaList: false,
+ supportBigInt: true,
+ });
+ } else if (driverName === "sqlite") {
+ return new IframeSQLiteDriver();
+ } else if (driverName === "starbase") {
+ return new IframeSQLiteDriver({
+ supportPragmaList: false,
+ });
+ } else if (driverName === "mysql" || driverName === "dolt") {
+ return new IframeMySQLDriver();
+ } else if (driverName === "postgres") {
+ return new IframePostgresDriver();
+ }
+
+ return new IframeSQLiteDriver();
+}
+
+function createEmbedExtensions(driverName: string) {
+ if (driverName === "turso") {
+ return createSQLiteExtensions();
+ } else if (driverName === "sqlite" || driverName === "starbase") {
+ return createSQLiteExtensions();
+ } else if (driverName === "mysql") {
+ return createMySQLExtensions();
+ } else if (driverName === "dolt") {
+ return [...createMySQLExtensions(), new DoltExtension()];
+ } else if (driverName === "postgres") {
+ return createPostgreSQLExtensions();
+ }
+
+ return createSQLiteExtensions();
+}
diff --git a/src/app/(theme)/embed/[driver]/page.tsx b/src/app/(theme)/embed/[driver]/page.tsx
new file mode 100644
index 00000000..83ba5eef
--- /dev/null
+++ b/src/app/(theme)/embed/[driver]/page.tsx
@@ -0,0 +1,46 @@
+import ClientOnly from "@/components/client-only";
+import ThemeLayout from "../../theme_layout";
+import EmbedPageClient from "./page-client";
+
+export interface EmbedPageProps {
+ searchParams: Promise<{
+ theme?: string;
+ disableThemeToggle?: string;
+ [key: string]: any;
+ }>;
+ params: Promise<{
+ driver: string;
+ }>;
+}
+
+export default async function EmbedPage(props: EmbedPageProps) {
+ const searchParams = await props.searchParams;
+ const driver = (await props.params).driver;
+
+ let overrideTheme: "dark" | "light" | undefined = undefined;
+
+ if (searchParams.theme) {
+ overrideTheme = searchParams.theme === "dark" ? "dark" : "light";
+ }
+
+ const overrideThemeVariables: Record
= {};
+
+ for (const key in searchParams) {
+ if (!key.startsWith("themeVariables[")) {
+ continue;
+ }
+
+ overrideThemeVariables[key.slice(15, -1)] = searchParams[key];
+ }
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/app/(theme)/embed/board/[boardId]/page.tsx b/src/app/(theme)/embed/board/[boardId]/page.tsx
new file mode 100644
index 00000000..18b6e5db
--- /dev/null
+++ b/src/app/(theme)/embed/board/[boardId]/page.tsx
@@ -0,0 +1,31 @@
+import ThemeLayout from "@/app/(theme)/theme_layout";
+import Chart from "@/components/chart";
+import ClientOnly from "@/components/client-only";
+import { getOuterbaseEmbedChart } from "@/outerbase-cloud/api";
+
+interface EmbedBoardPageProps {
+ params: Promise<{ boardId: string }>;
+ searchParams: Promise<{ key: string; theme: string }>;
+}
+
+export default async function EmbedBoardPage(props: EmbedBoardPageProps) {
+ const searchParams = await props.searchParams;
+ const params = await props.params;
+
+ const result = await getOuterbaseEmbedChart(params.boardId, searchParams.key);
+ const data = result.response.result?.items ?? [];
+
+ return (
+
+
+
+
+
+ );
+}
diff --git a/src/app/(theme)/embed/dolt/page-client.tsx b/src/app/(theme)/embed/dolt/page-client.tsx
deleted file mode 100644
index 35f96e69..00000000
--- a/src/app/(theme)/embed/dolt/page-client.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createMySQLExtensions } from "@/core/standard-extension";
-import { IframeDoltDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import DoltExtension from "@/extensions/dolt";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
- const driver = useMemo(() => new IframeDoltDriver(), []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager([
- ...createMySQLExtensions(),
- new DoltExtension(),
- ]);
- }, []);
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/dolt/page.tsx b/src/app/(theme)/embed/dolt/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/dolt/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/embed/embed-page.tsx b/src/app/(theme)/embed/embed-page.tsx
deleted file mode 100644
index 37041c72..00000000
--- a/src/app/(theme)/embed/embed-page.tsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { ReactElement } from "react";
-import ThemeLayout from "../theme_layout";
-import ClientOnly from "@/components/client-only";
-
-export interface EmbedPageProps {
- searchParams: Promise<{
- theme?: string;
- disableThemeToggle?: string;
- [key: string]: any;
- }>;
-}
-
-export function createEmbedPage(render: () => ReactElement) {
- return async function EmbedPage(props: EmbedPageProps) {
- const searchParams = await props.searchParams;
-
- let overrideTheme: "dark" | "light" | undefined = undefined;
- const disableToggle = searchParams.disableThemeToggle === "1";
-
- if (searchParams.theme) {
- overrideTheme = searchParams.theme === "dark" ? "dark" : "light";
- }
-
- const overrideThemeVariables: Record = {};
-
- for (const key in searchParams) {
- if (!key.startsWith("themeVariables[")) {
- continue;
- }
-
- overrideThemeVariables[key.slice(15, -1)] = searchParams[key];
- }
-
- return (
-
- {render()}
-
- );
- };
-}
diff --git a/src/app/(theme)/embed/mysql/page-client.tsx b/src/app/(theme)/embed/mysql/page-client.tsx
deleted file mode 100644
index 5fa3207f..00000000
--- a/src/app/(theme)/embed/mysql/page-client.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createMySQLExtensions } from "@/core/standard-extension";
-import { IframeMySQLDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
- const driver = useMemo(() => new IframeMySQLDriver(), []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager(createMySQLExtensions());
- }, []);
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/mysql/page.tsx b/src/app/(theme)/embed/mysql/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/mysql/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/embed/postgres/page-client.tsx b/src/app/(theme)/embed/postgres/page-client.tsx
deleted file mode 100644
index 3aadbd34..00000000
--- a/src/app/(theme)/embed/postgres/page-client.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createPostgreSQLExtensions } from "@/core/standard-extension";
-import { IframePostgresDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
- const driver = useMemo(() => new IframePostgresDriver(), []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager(createPostgreSQLExtensions());
- }, []);
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/postgres/page.tsx b/src/app/(theme)/embed/postgres/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/postgres/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/embed/sqlite/page-client.tsx b/src/app/(theme)/embed/sqlite/page-client.tsx
deleted file mode 100644
index e01933b0..00000000
--- a/src/app/(theme)/embed/sqlite/page-client.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createSQLiteExtensions } from "@/core/standard-extension";
-import { IframeSQLiteDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
- const driver = useMemo(() => new IframeSQLiteDriver(), []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager(createSQLiteExtensions());
- }, []);
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/sqlite/page.tsx b/src/app/(theme)/embed/sqlite/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/sqlite/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/embed/starbase/page-client.tsx b/src/app/(theme)/embed/starbase/page-client.tsx
deleted file mode 100644
index b41d30ba..00000000
--- a/src/app/(theme)/embed/starbase/page-client.tsx
+++ /dev/null
@@ -1,40 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createSQLiteExtensions } from "@/core/standard-extension";
-import { IframeSQLiteDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
- const driver = useMemo(
- () => new IframeSQLiteDriver({ supportPragmaList: false }),
- []
- );
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager(createSQLiteExtensions());
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/starbase/page.tsx b/src/app/(theme)/embed/starbase/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/starbase/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/embed/turso/page-client.tsx b/src/app/(theme)/embed/turso/page-client.tsx
deleted file mode 100644
index 35f1afc3..00000000
--- a/src/app/(theme)/embed/turso/page-client.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-"use client";
-import { Studio } from "@/components/gui/studio";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { createSQLiteExtensions } from "@/core/standard-extension";
-import { IframeSQLiteDriver } from "@/drivers/iframe-driver";
-import ElectronSavedDocs from "@/drivers/saved-doc/electron-saved-doc";
-import { useSearchParams } from "next/navigation";
-import { useEffect, useMemo } from "react";
-
-export default function EmbedPageClient() {
- const searchParams = useSearchParams();
-
- const driver = useMemo(
- () =>
- new IframeSQLiteDriver({
- supportPragmaList: false,
- supportBigInt: true,
- }),
- []
- );
-
- const savedDocDriver = useMemo(() => {
- if (window.outerbaseIpc?.docs) {
- return new ElectronSavedDocs();
- }
- }, []);
-
- const extensions = useMemo(() => {
- return new StudioExtensionManager(createSQLiteExtensions());
- }, []);
-
- useEffect(() => {
- return driver.listen();
- }, [driver]);
-
- return (
-
- );
-}
diff --git a/src/app/(theme)/embed/turso/page.tsx b/src/app/(theme)/embed/turso/page.tsx
deleted file mode 100644
index 655e6635..00000000
--- a/src/app/(theme)/embed/turso/page.tsx
+++ /dev/null
@@ -1,4 +0,0 @@
-import { createEmbedPage } from "../embed-page";
-import EmbedPageClient from "./page-client";
-
-export default createEmbedPage(() => );
diff --git a/src/app/(theme)/theme_layout.tsx b/src/app/(theme)/theme_layout.tsx
index 0a8cc229..37c9e602 100644
--- a/src/app/(theme)/theme_layout.tsx
+++ b/src/app/(theme)/theme_layout.tsx
@@ -1,44 +1,40 @@
-import { Analytics } from "@vercel/analytics/react";
-import { Inter } from "next/font/google";
-import ThemeProvider from "@/context/theme-provider";
-import { cookies } from "next/headers";
+"use client";
+import PageTracker from "@/components/page-tracker";
import { Toaster } from "@/components/ui/sonner";
-import { Fragment, PropsWithChildren } from "react";
+import { Analytics } from "@vercel/analytics/react";
+import { ThemeProvider } from "next-themes";
import Script from "next/script";
-import { cn } from "@/lib/utils";
-import PageTracker from "@/components/page-tracker";
-
-const inter = Inter({ subsets: ["latin"] });
+import { Fragment, PropsWithChildren, useEffect } from "react";
-export default async function ThemeLayout({
+export default function ThemeLayout({
children,
overrideTheme,
- disableToggle,
overrideThemeVariables,
}: PropsWithChildren<{
overrideTheme?: "dark" | "light";
- disableToggle?: boolean;
overrideThemeVariables?: Record;
}>) {
- const cookieStore = await cookies();
- const theme =
- overrideTheme ??
- (cookieStore.get("theme")?.value === "dark" ? "dark" : "light");
- const style = overrideThemeVariables ?? {};
+ useEffect(() => {
+ if (overrideThemeVariables && typeof window === "undefined") {
+ Object.entries(overrideThemeVariables).forEach(([key, value]) => {
+ document.body.style.setProperty(key, value);
+ });
+ }
+ }, [overrideThemeVariables]);
return (
-
-
+ <>
+
{children}
-
+ >
);
}
diff --git a/src/app/globals.css b/src/app/globals.css
index 793b1676..1e1981d1 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -1,4 +1,5 @@
@import "tailwindcss";
+@import "./styles/markdown.css";
@custom-variant dark (&:is(.dark *));
@@ -51,32 +52,40 @@
from {
height: 0;
}
+
to {
height: var(--radix-accordion-content-height);
}
}
+
@keyframes accordion-up {
from {
height: var(--radix-accordion-content-height);
}
+
to {
height: 0;
}
}
+
@keyframes shake {
+
10%,
90% {
transform: translate3d(-1px, 0, 0);
}
+
20%,
80% {
transform: translate3d(2px, 0, 0);
}
+
30%,
50%,
70% {
transform: translate3d(-4px, 0, 0);
}
+
40%,
60% {
transform: translate3d(4px, 0, 0);
@@ -87,10 +96,12 @@
@utility container {
margin-inline: auto;
padding-inline: 2rem;
- @media (width >= --theme(--breakpoint-sm)) {
+
+ @media (width >=--theme(--breakpoint-sm)) {
max-width: none;
}
- @media (width >= 1400px) {
+
+ @media (width >=1400px) {
max-width: 1400px;
}
}
@@ -104,6 +115,7 @@
color utility to any element that depends on these defaults.
*/
@layer base {
+
*,
::after,
::before,
@@ -114,6 +126,7 @@
}
@layer utilities {
+
html,
body {
overscroll-behavior-x: none;
@@ -237,7 +250,7 @@
@layer base {
* {
- @apply border-border;
+ @apply border-border antialiased;
}
body {
@@ -347,345 +360,305 @@
opacity: 0.5;
}
-/*
-For Markdown Side
-*/
-.mdx-content {
- @apply leading-7;
-}
-
-.mdx-content h1 {
- @apply text-3xl;
-}
-
-.mdx-content h2 {
- @apply my-4 border-b pb-2 text-2xl font-bold;
-}
-
-.mdx-content h3 {
- @apply my-4 border-b pb-2 text-xl font-bold;
-}
-
-.mdx-content ul {
- @apply my-4 list-disc pl-8;
-}
-
-.mdx-content ol {
- @apply my-4 list-decimal pl-8;
-}
-
-.mdx-content pre {
- @apply m-0 overflow-x-auto rounded bg-gray-200 p-4 leading-5 dark:bg-gray-700;
-}
-
-.mdx-content p {
- @apply my-2;
-}
-
-.mdx-content a {
- @apply text-blue-500 underline;
-}
-
/* cyrillic-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
+
/* cyrillic */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
+
/* greek-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}
+
/* greek */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
+
/* vietnamese */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2")
- format("woff2");
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
- U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
- U+1EA0-1EF9, U+20AB;
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2") format("woff2");
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
+ U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
+ U+0329, U+1EA0-1EF9, U+20AB;
}
+
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
+
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+
/* cyrillic-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
+
/* cyrillic */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
+
/* greek-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}
+
/* greek */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
+
/* vietnamese */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2")
- format("woff2");
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
- U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
- U+1EA0-1EF9, U+20AB;
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2") format("woff2");
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
+ U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
+ U+0329, U+1EA0-1EF9, U+20AB;
}
+
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
+
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 500;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+
/* cyrillic-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
+
/* cyrillic */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
+
/* greek-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}
+
/* greek */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
+
/* vietnamese */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2")
- format("woff2");
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
- U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
- U+1EA0-1EF9, U+20AB;
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2") format("woff2");
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
+ U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
+ U+0329, U+1EA0-1EF9, U+20AB;
}
+
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
+
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 600;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
+
/* cyrillic-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F,
U+FE2E-FE2F;
}
+
/* cyrillic */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa0ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
+
/* greek-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2ZL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+1F00-1FFF;
}
+
/* greek */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1pL7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0370-03FF;
}
+
/* vietnamese */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2")
- format("woff2");
- unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1,
- U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329,
- U+1EA0-1EF9, U+20AB;
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa2pL7W0Q5n-wU.woff2") format("woff2");
+ unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169,
+ U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323,
+ U+0329, U+1EA0-1EF9, U+20AB;
}
+
/* latin-ext */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa25L7W0Q5n-wU.woff2") format("woff2");
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF,
U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
+
/* latin */
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 700;
font-display: swap;
- src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2")
- format("woff2");
+ src: url("./fonts/UcC73FwrK3iLTeHuS_fvQtMwCp50KnMa1ZL7W0Q5nw.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191,
U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
@@ -706,9 +679,11 @@ For Markdown Side
}
.window-tab-scrollbar:hover::-webkit-scrollbar-thumb {
- background-color: #555; /* Color of the scrollbar thumb on hover */
+ background-color: #555;
+ /* Color of the scrollbar thumb on hover */
}
.window-tab-scrollbar:hover::-webkit-scrollbar-thumb:hover {
- background-color: #888; /* Color of the scrollbar thumb on hover */
-}
+ background-color: #888;
+ /* Color of the scrollbar thumb on hover */
+}
\ No newline at end of file
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 06ca143d..10297a0e 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,14 @@
+import { WEBSITE_NAME } from "@/const";
import type { Metadata } from "next";
-import "./globals.css";
import "./codemirror-override.css";
-import { Fragment } from "react";
-import { WEBSITE_NAME } from "@/const";
+import "./globals.css";
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`;
+import { Inter } from "next/font/google";
+const inter = Inter({ subsets: ["latin"] });
+
export const metadata: Metadata = {
title: WEBSITE_NAME,
keywords: [
@@ -33,8 +35,8 @@ export default async function RootLayout({
children: React.ReactNode;
}) {
return (
-
- {children}
+
+ {children}
);
}
diff --git a/src/app/signin/page.tsx b/src/app/signin/page.tsx
new file mode 100644
index 00000000..f49e66c2
--- /dev/null
+++ b/src/app/signin/page.tsx
@@ -0,0 +1,100 @@
+"use client";
+import LabelInput from "@/components/label-input";
+import { Button } from "@/components/ui/button";
+import {
+ getOuterbaseWorkspace,
+ loginOuterbaseByPassword,
+} from "@/outerbase-cloud/api";
+import { OuterbaseAPIError } from "@/outerbase-cloud/api-type";
+import { LucideLoader } from "lucide-react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useCallback, useState } from "react";
+import ThemeLayout from "../(theme)/theme_layout";
+import { LoginBaseSpaceship } from "./starbase-portal";
+
+export default function SigninPage() {
+ const [email, setEmail] = useState("");
+ const [password, setPassword] = useState("");
+ const [error, setError] = useState("");
+ const router = useRouter();
+ const [loading, setLoading] = useState(false);
+
+ const onLoginClicked = useCallback(() => {
+ setLoading(true);
+
+ loginOuterbaseByPassword(email, password)
+ .then((session) => {
+ localStorage.setItem("session", JSON.stringify(session));
+ localStorage.setItem("ob-token", session.token);
+
+ getOuterbaseWorkspace()
+ .then((w) => {
+ router.push(`/w/${w.items[0].short_name}`);
+ })
+ .catch(console.error)
+ .finally(() => {
+ setLoading(false);
+ });
+ })
+ .catch((e) => {
+ setLoading(false);
+ if (e instanceof OuterbaseAPIError) {
+ setError(e.description);
+ }
+ });
+ }, [email, password, router]);
+
+ return (
+
+
+
+
+
+
+
+
Welcome back
+
Sign in to your existing account
+
+
+
setEmail(e.currentTarget.value)}
+ />
+
+ setPassword(e.currentTarget.value)}
+ />
+
+ {error && {error}
}
+
+
+ {loading && }
+ Continue with email
+
+
+
+ Forget password
+
+
+
+
+
+ );
+}
diff --git a/src/app/signin/starbase-portal.tsx b/src/app/signin/starbase-portal.tsx
new file mode 100644
index 00000000..9ad41526
--- /dev/null
+++ b/src/app/signin/starbase-portal.tsx
@@ -0,0 +1,48 @@
+import "./styles.css";
+
+export function LoginBaseSpaceship() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/signin/styles.css b/src/app/signin/styles.css
new file mode 100644
index 00000000..0c507818
--- /dev/null
+++ b/src/app/signin/styles.css
@@ -0,0 +1,53 @@
+@keyframes moveStars {
+ 0% {
+ bottom: 32%;
+ transform: rotate(0deg);
+ }
+ 25% {
+ bottom: 33%;
+ transform: rotate(-1deg);
+ }
+ 50% {
+ bottom: 34%;
+ transform: rotate(0deg);
+ }
+ 75% {
+ bottom: 33%;
+ transform: rotate(1deg);
+ }
+ 100% {
+ bottom: 32%;
+ transform: rotate(0deg);
+ }
+}
+
+@keyframes movePlanet {
+ 0% {
+ bottom: 16%;
+ transform: rotate(0deg);
+ }
+ 25% {
+ bottom: 17.5%;
+ transform: rotate(1deg);
+ }
+ 50% {
+ bottom: 20%;
+ transform: rotate(0deg);
+ }
+ 75% {
+ bottom: 17.5%;
+ transform: rotate(-1deg);
+ }
+ 100% {
+ bottom: 16%;
+ transform: rotate(0deg);
+ }
+}
+
+.stars-animation {
+ animation: moveStars 30s ease-in-out infinite;
+}
+
+.planet-animation {
+ animation: movePlanet 30s ease-in-out infinite;
+}
diff --git a/src/app/storybook/blank_page.mdx b/src/app/storybook/blank_page.mdx
new file mode 100644
index 00000000..52883e35
--- /dev/null
+++ b/src/app/storybook/blank_page.mdx
@@ -0,0 +1,7 @@
+import { DocContent } from "@/components/mdx/docs";
+
+export const metadata = {
+ title: "Template Title",
+};
+
+
diff --git a/src/app/storybook/board/page.tsx b/src/app/storybook/board/page.tsx
new file mode 100644
index 00000000..df14a4a0
--- /dev/null
+++ b/src/app/storybook/board/page.tsx
@@ -0,0 +1,32 @@
+"use client";
+import Board from "@/components/board";
+import { BoardFilterProps } from "@/components/board/board-filter-dialog";
+import { useState } from "react";
+import ReactGridLayout from "react-grid-layout";
+
+interface DashboardProps {
+ layout: ReactGridLayout.Layout[];
+ data: {
+ filters: BoardFilterProps[];
+ };
+}
+
+export default function StorybookBoardPage() {
+ const [value, setValue] = useState({
+ layout: [
+ { x: 0, y: 0, w: 1, h: 1, i: "0" },
+ { x: 1, y: 0, w: 1, h: 1, i: "1" },
+ { x: 2, y: 0, w: 1, h: 1, i: "2" },
+ { x: 3, y: 0, w: 1, h: 1, i: "3" },
+ ],
+ data: { filters: [] },
+ });
+
+ console.log(value);
+
+ return (
+
+
+
+ );
+}
diff --git a/src/app/storybook/chart/page.tsx b/src/app/storybook/chart/page.tsx
new file mode 100644
index 00000000..4c2e5917
--- /dev/null
+++ b/src/app/storybook/chart/page.tsx
@@ -0,0 +1,222 @@
+"use client";
+
+import Chart, { ChartData, ChartValue } from "@/components/chart";
+import { Button } from "@/components/ui/button";
+import { useState } from "react";
+
+const data: ChartData[] = [
+ {
+ package_year: 2021,
+ package_count: 33,
+ },
+ {
+ package_year: 2020,
+ package_count: 62103,
+ },
+ {
+ package_year: 2021,
+ package_count: 360881,
+ },
+ {
+ package_year: 2022,
+ package_count: 426727,
+ },
+ {
+ package_year: 2023,
+ package_count: 370008,
+ },
+ {
+ package_year: 2024,
+ package_count: 1074,
+ },
+ {
+ package_year: 2025,
+ package_count: 43,
+ },
+];
+
+const lineChartValue: ChartValue = {
+ connection_id: null,
+ created_at: "2025-01-30T02:57:34.579Z",
+ id: "2bdc1dc4-21de-4b5f-90d2-384e40288769",
+ model: "chart",
+ name: "Line Chart",
+ params: {
+ id: "2bdc1dc4-21de-4b5f-90d2-384e40288769",
+ name: "Line Chart",
+ type: "scatter",
+ model: "chart",
+ apiKey: "",
+ layers: [
+ {
+ sql: "SELECT YEAR(created_at) AS package_year, COUNT(*) AS package_count FROM package GROUP BY package_year",
+ type: "line",
+ },
+ ],
+ options: {
+ xAxisKey: "package_year",
+ yAxisKeys: ["package_count"],
+ yAxisKeyColors: {
+ "1 + 1": "#fafafa",
+ package_year: "#fafafa",
+ },
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ created_at: "2025-01-30T02:57:34.579Z",
+ updated_at: "2025-01-30T02:57:34.579Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+ connection_id: null,
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ type: "line",
+ updated_at: "2025-01-30T09:06:35.249Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+};
+
+const barChartValue: ChartValue = {
+ connection_id: null,
+ created_at: "2025-01-30T02:56:40.851Z",
+ id: "fed7cd59-6be9-4f43-af6d-b730d14be984",
+ model: "chart",
+ name: "Bar chart",
+ params: {
+ id: "fed7cd59-6be9-4f43-af6d-b730d14be984",
+ name: "Bar chart",
+ type: "column",
+ model: "chart",
+ apiKey: "",
+ layers: [
+ {
+ sql: "SELECT YEAR(created_at) AS package_year, COUNT(*) AS package_count FROM package GROUP BY package_year",
+ type: "bar",
+ },
+ ],
+ options: {
+ xAxisKey: "package_year",
+ yAxisKeys: ["package_count"],
+ yAxisKeyColors: {
+ avg_value: "#e5e5e5",
+ sum_value: "#a3a3a3",
+ ValueGroup: "#fafafa",
+ PackageCount: "#fafafa",
+ package_count: "#525252",
+ },
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ created_at: "2025-01-30T02:56:40.851Z",
+ updated_at: "2025-01-30T02:56:40.851Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+ connection_id: null,
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ type: "bar",
+ updated_at: "2025-01-30T09:25:05.373Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+};
+
+const pieChartValue: ChartValue = {
+ connection_id: null,
+ created_at: "2025-01-30T03:00:40.963Z",
+ id: "602f5960-e2db-4359-8008-c87629c1d169",
+ model: "chart",
+ name: "Pie chart teset",
+ params: {
+ id: "602f5960-e2db-4359-8008-c87629c1d169",
+ name: "Pie chart teset",
+ type: "line",
+ model: "chart",
+ apiKey: "",
+ layers: [
+ {
+ sql: "SELECT YEAR(created_at) AS package_year, COUNT(*) AS package_count FROM package GROUP BY package_year",
+ type: "pie",
+ },
+ ],
+ options: {
+ theme: "afterburn",
+ xAxisKey: "package_year",
+ yAxisKeys: ["package_count"],
+ yAxisKeyColors: {
+ package_count: "#E75F98",
+ },
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ created_at: "2025-01-30T03:00:40.963Z",
+ updated_at: "2025-01-30T03:00:40.963Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+ connection_id: null,
+ },
+ source_id: "856a1855-2bee-4d87-9756-a783088c0568",
+ type: "pie",
+ updated_at: "2025-01-30T07:19:15.167Z",
+ workspace_id: "3db2e96f-ee43-412d-be09-25fc02d3a463",
+};
+
+export default function StorybookChartPage() {
+ const [chartValue, setChartValue] = useState(lineChartValue);
+ const [items, setItems] = useState(data);
+ const [modifier, setModifier] = useState({});
+
+ return (
+
+
+ {
+ setChartValue(lineChartValue);
+ setItems(items);
+ setModifier({
+ title: {
+ text: "Line Chart",
+ left: "center",
+ },
+ tooltip: {
+ trigger: "item",
+ },
+ });
+ }}
+ >
+ Line
+
+ {
+ setChartValue(barChartValue);
+ setItems(items);
+ setModifier({
+ title: {
+ text: "Bar Chart",
+ left: "center",
+ },
+ tooltip: {
+ trigger: "item",
+ },
+ });
+ }}
+ >
+ Bar
+
+ {
+ setChartValue(pieChartValue);
+ setItems(items);
+ setModifier({
+ title: {
+ text: "Pie Chart",
+ left: "center",
+ },
+ tooltip: {
+ trigger: "item",
+ },
+ });
+ }}
+ >
+ Pie
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/storybook/layout.tsx b/src/app/storybook/layout.tsx
index 2f24c7b5..1cc1c998 100644
--- a/src/app/storybook/layout.tsx
+++ b/src/app/storybook/layout.tsx
@@ -1,33 +1,72 @@
import { SidebarMenuHeader, SidebarMenuItem } from "@/components/sidebar-menu";
+import ThemeToggle from "@/components/theme-toggle";
import { Separator } from "@/components/ui/separator";
+import { TooltipProvider } from "@/components/ui/tooltip";
import { Component, Layers2 } from "lucide-react";
+import ThemeLayout from "../(theme)/theme_layout";
-export default async function StorybookRootLayout({
+export default function StorybookRootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
-
-
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
-
+
+
+
+
+ {children}
-
{children}
-
+
);
}
diff --git a/src/app/storybook/listview/page.tsx b/src/app/storybook/listview/example1.tsx
similarity index 78%
rename from src/app/storybook/listview/page.tsx
rename to src/app/storybook/listview/example1.tsx
index 19e9c10a..447df9e8 100644
--- a/src/app/storybook/listview/page.tsx
+++ b/src/app/storybook/listview/example1.tsx
@@ -4,7 +4,7 @@ import { Input } from "@/components/ui/input";
import { LucideDatabase, LucideTable } from "lucide-react";
import { useCallback, useMemo, useState } from "react";
-function Demo1() {
+export function StorybookListviewExample() {
const [filter, setFilter] = useState("");
const [selected, setSelected] = useState("");
const [collapsed, setCollapsed] = useState(new Set
(["movies"]));
@@ -104,42 +104,3 @@ function Demo1() {
);
}
-
-function Demo2() {
- return (
-
-
-
- );
-}
-
-export default function ListViewStorybookPage() {
- return (
- <>
-
-
-
- >
- );
-}
diff --git a/src/app/storybook/listview/page.mdx b/src/app/storybook/listview/page.mdx
new file mode 100644
index 00000000..7805e5b6
--- /dev/null
+++ b/src/app/storybook/listview/page.mdx
@@ -0,0 +1,12 @@
+import { StorybookListviewExample } from "@/app/storybook/listview/example1";
+import { DocContent } from "@/components/mdx/docs";
+
+export const metadata = {
+ title: "Listview",
+};
+
+
+
+
+
+You can write more markdown here
diff --git a/src/app/storybook/page.mdx b/src/app/storybook/page.mdx
new file mode 100644
index 00000000..3525df96
--- /dev/null
+++ b/src/app/storybook/page.mdx
@@ -0,0 +1,55 @@
+import { DocContent } from "@/components/mdx/docs";
+
+export const metadata = {
+ title: "Standard Component Guideline",
+};
+
+
+
+Please prioritize using these standard components whenever possible. Our goal is to maintain consistency and **limit excessive customization**.
+
+### Example 1
+
+â›” Developers may have their own preferences for styling
+
+```tsx
+
+
+
Username
+
+
Your username does not exist
+
+
+
+ Password
+
+
+
+```
+
+✅ Developers must follow a single prescribed method.
+
+```tsx
+
+
+
+
+```
+
+## New Components Guideline
+
+- ✅ All standard components should be placed in the `src/app/components` folder.
+- ✅ Try to make your component friendly for copy-pasting to other project.
+- â›” Avoid including business logic that is tightly coupled with the application in standard components.
+- â›” Do not place one-off components in the `src/app/components` folder.
+ - If it is an extension component, place it in the extension folder.
+ - If it is a page-specific component, place it in the corresponding app router folder near the page.
+- â›” Minimize the use of color-specific class names whenever possible.
+ Instead, use theme-based colors to ensure easier customization and future support for custom themes.
+ - Example 1: `border` instead of `border-gray-100 dark:border-gray-600`
+ - Example 2: `bg-secondary` instead of `bg-gray-100 dark:bg-gray-600`
+
+
diff --git a/src/app/storybook/page.tsx b/src/app/storybook/page.tsx
deleted file mode 100644
index 97cd70a0..00000000
--- a/src/app/storybook/page.tsx
+++ /dev/null
@@ -1,3 +0,0 @@
-export default function Storybook() {
- return
;
-}
diff --git a/src/app/storybook/toolbar/example.tsx b/src/app/storybook/toolbar/example.tsx
new file mode 100644
index 00000000..192d9abf
--- /dev/null
+++ b/src/app/storybook/toolbar/example.tsx
@@ -0,0 +1,34 @@
+import {
+ Toolbar,
+ ToolbarButton,
+ ToolbarFiller,
+ ToolbarSeparator,
+} from "@/components/gui/toolbar";
+import { LucideCopy, LucideFolder, LucideSave } from "lucide-react";
+
+export function StorybookToolbarExample() {
+ return (
+
+
+
+ }
+ />
+ }
+ />
+
+ }
+ />
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/storybook/toolbar/page.mdx b/src/app/storybook/toolbar/page.mdx
new file mode 100644
index 00000000..58d311f2
--- /dev/null
+++ b/src/app/storybook/toolbar/page.mdx
@@ -0,0 +1,10 @@
+import { DocContent } from "@/components/mdx/docs";
+import { StorybookToolbarExample } from "./example";
+
+export const metadata = {
+ title: "Toolbar",
+};
+
+
+
+
diff --git a/src/app/styles/markdown.css b/src/app/styles/markdown.css
new file mode 100644
index 00000000..8aa1bde6
--- /dev/null
+++ b/src/app/styles/markdown.css
@@ -0,0 +1,47 @@
+.mdx-content {
+ @apply leading-7;
+}
+
+.mdx-content h1 {
+ @apply text-3xl;
+}
+
+.mdx-content h2 {
+ @apply my-4 border-b pb-2 text-2xl font-bold;
+}
+
+.mdx-content h3 {
+ @apply my-4 border-b pb-2 text-xl font-bold;
+}
+
+.mdx-content ul {
+ @apply my-4 list-disc pl-8;
+}
+
+.mdx-content li ul {
+ @apply my-0 list-disc pl-8;
+}
+
+.mdx-content ol {
+ @apply my-4 list-decimal pl-8;
+}
+
+.mdx-content pre {
+ @apply bg-secondary m-0 overflow-x-auto rounded p-4 leading-5;
+}
+
+.mdx-content p {
+ @apply my-2;
+}
+
+.mdx-content a {
+ @apply text-blue-500 underline;
+}
+
+.mdx-content code {
+ @apply bg-secondary rounded p-1;
+}
+
+.mdx-content pre code {
+ @apply rounded-none bg-inherit p-0;
+}
diff --git a/src/components/board/board-canvas.tsx b/src/components/board/board-canvas.tsx
new file mode 100644
index 00000000..388a7fca
--- /dev/null
+++ b/src/components/board/board-canvas.tsx
@@ -0,0 +1,104 @@
+"use client";
+import { cn } from "@/lib/utils";
+import { RectangleHorizontal, Square } from "lucide-react";
+import { useCallback } from "react";
+import RGL, { WidthProvider } from "react-grid-layout";
+import { buttonVariants } from "../ui/button";
+import "./board-style.css";
+
+export interface BoardChartLayout {
+ x: number;
+ y: number;
+ w: number;
+ h: number;
+ i: number;
+}
+
+interface BoardProps {
+ layout: ReactGridLayout.Layout[];
+ onChange: (v: ReactGridLayout.Layout[]) => void;
+ editMode?: "ADD_CHART" | "REARRANGING_CHART" | null;
+}
+
+const ReactGridLayout = WidthProvider(RGL);
+
+export function BoardCanvas(props: BoardProps) {
+ const sizes = [
+ { w: 1, h: 1, name: "1", icon: },
+ {
+ w: 2,
+ h: 1,
+ name: "2",
+ icon: ,
+ },
+ { w: 2, h: 2, name: "3", icon: },
+ {
+ w: 4,
+ h: 2,
+ name: "4",
+ icon: ,
+ },
+ ];
+
+ const handleClickResize = useCallback(
+ (w: number, h: number, index: number) => {
+ const dummy = structuredClone(props.layout);
+ dummy[index].w = w;
+ dummy[index].h = h;
+ props.onChange(dummy);
+ },
+ [props]
+ );
+
+ const mapItem: JSX.Element[] = [...Array(props.layout.length)].map((_, i) => {
+ return (
+
+ {props.editMode === "REARRANGING_CHART" && (
+
+ {sizes.map((x, index) => {
+ return (
+
+ handleClickResize(x.w as number, x.h as number, i)
+ }
+ onMouseDown={(e) => e.stopPropagation()}
+ onTouchStart={(e) => e.stopPropagation()}
+ key={index}
+ >
+ {x.icon}
+
+ );
+ })}
+
+ )}
+
{i}
+
+ );
+ });
+
+ return (
+
+
+ {mapItem}
+
+
+ );
+}
diff --git a/src/components/board/board-filter-dialog.tsx b/src/components/board/board-filter-dialog.tsx
new file mode 100644
index 00000000..de3efcce
--- /dev/null
+++ b/src/components/board/board-filter-dialog.tsx
@@ -0,0 +1,192 @@
+import { useCallback } from "react";
+import { Button } from "../ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "../ui/dialog";
+import { Input } from "../ui/input";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "../ui/select";
+
+export interface BoardFilterProps {
+ type: string;
+ name: string;
+ default_value: string;
+ value: string;
+ new?: boolean;
+}
+
+interface Props {
+ onClose?: () => void;
+ filter: BoardFilterProps;
+ onFilter: (v: BoardFilterProps) => void;
+ onAddFilter?: (v: BoardFilterProps) => void;
+}
+
+const DEFAULT_EMPTY = {
+ type: "search",
+ name: "",
+ default_value: "",
+ value: "",
+};
+
+export const DEFAULT_DATE_FILTER = [
+ "Not timeframe override",
+ "Custom date range",
+ "Last 24 hours",
+ "Today",
+ "Yesterday",
+ "This week",
+ "This month",
+ "Last 7 days",
+ "Last 30 days",
+ "Last 90 days",
+];
+
+export function BoardFilterDialog(props: Props) {
+ let default_value = [...DEFAULT_DATE_FILTER];
+
+ if (props.filter.type === "enum" && !!props.filter.value) {
+ default_value = [...props.filter.value.split(",")];
+ }
+
+ let allowAddFilter = !!props.filter.name;
+
+ if (props.filter.type === "enum") {
+ allowAddFilter = !!props.filter.name && !!props.filter.value;
+ }
+
+ const onAddFilter = useCallback(() => {
+ props.onAddFilter && props.onAddFilter(props.filter);
+ }, [props]);
+
+ return (
+
+
+
+ New Filter
+
+
+
+
Select filter type
+
+ props.onFilter({ ...DEFAULT_EMPTY, type: v })
+ }
+ >
+
+
+
+
+ Search
+ Multi-select ENUM
+ Date Rang
+
+
+
+
+
Filter name*
+
+ props.onFilter({ ...props.filter, name: v.target.value })
+ }
+ />
+
+ {props.filter.type === "enum" && (
+
+
+ Values*
+
+
+ Enter values separated by comma
+
+
+
+
+ props.onFilter({ ...props.filter, value: v.target.value })
+ }
+ />
+
+ )}
+
+
+ Default value (optional)
+
+
+ If this field is left empty, no filter will be applied by
+ default
+
+
+
+ {props.filter.type === "search" ? (
+
+ props.onFilter({
+ ...props.filter,
+ default_value: v.target.value,
+ })
+ }
+ />
+ ) : (
+
+ props.onFilter({ ...props.filter, default_value: v })
+ }
+ >
+
+
+
+
+ {default_value
+ .filter((x) => !!x)
+ .map((x) => {
+ return (
+
+ {x}
+
+ );
+ })}
+
+
+ )}
+
+ {props.filter.type !== "date" && !!props.filter.name && (
+
+ {`Use the variable {{ ${props.filter.name} }} in your charts SQL queries.`}
+
+ )}
+
+
+
+ Add Filter
+
+
+ Cancel
+
+
+
+
+ );
+}
diff --git a/src/components/board/board-filter.tsx b/src/components/board/board-filter.tsx
new file mode 100644
index 00000000..368f2f96
--- /dev/null
+++ b/src/components/board/board-filter.tsx
@@ -0,0 +1,246 @@
+"use client";
+import {
+ CalendarDays,
+ Check,
+ Ellipsis,
+ ListFilter,
+ ListOrdered,
+ Search,
+} from "lucide-react";
+import { useCallback, useState } from "react";
+import { buttonVariants } from "../ui/button";
+import { Checkbox } from "../ui/checkbox";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "../ui/dropdown-menu";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
+import {
+ BoardFilterDialog,
+ BoardFilterProps,
+ DEFAULT_DATE_FILTER,
+} from "./board-filter-dialog";
+import { BoardTool } from "./board-tool";
+
+interface Props {
+ filters: BoardFilterProps[];
+ onFilters: (f: BoardFilterProps[]) => void;
+ editMode: "ADD_CHART" | "REARRANGING_CHART" | null;
+ setEditMode: (v: "ADD_CHART" | "REARRANGING_CHART") => void;
+}
+
+export function BoardFilter(props: Props) {
+ const [open, setOpen] = useState(false);
+ const [selectIndex, setSelectIndex] = useState(undefined);
+
+ const onFilter = useCallback(() => {
+ const data = [
+ ...props.filters,
+ {
+ type: "search",
+ default_value: "",
+ value: "",
+ name: "",
+ new: true,
+ },
+ ];
+ props.onFilters(data);
+ setSelectIndex(data.length - 1);
+ setOpen(true);
+ }, [props]);
+
+ const mapFilterItem = props.filters.map((x, i) => {
+ const icon =
+ x.type === "search" ? (
+
+ ) : x.type === "enum" ? (
+
+ ) : (
+
+ );
+ const input =
+ x.type === "search" ? (
+ {
+ const data = structuredClone(props.filters);
+ data[i].default_value = v.target.value;
+ props.onFilters(data);
+ }}
+ className="max-w-14 outline-0"
+ />
+ ) : x.type === "enum" ? (
+
+
+
+
+
{x.default_value || `Select ${x.name}`}
+
+
+
+ {x.value.split(",").map((v, idx) => {
+ return (
+
+ {
+ const value = x.default_value.split(",");
+ const data = structuredClone(props.filters);
+
+ if (checked) {
+ data[i].default_value = [...value, v]
+ .filter((f) => !!f)
+ .join(",");
+ } else {
+ value.filter((f) => f !== v);
+ data[i].default_value = value
+ .filter((f) => f !== v)
+ .join(",");
+ }
+ props.onFilters(data);
+ }}
+ />
+ {v}
+
+ );
+ })}
+
+
+
+ ) : (
+
+
+
+
{x.default_value || `Select ${x.name}`}
+
+
+
+ {DEFAULT_DATE_FILTER.map((date) => {
+ return (
+ {
+ const data = structuredClone(props.filters);
+ data[i].default_value = date;
+ props.onFilters(data);
+ }}
+ >
+
+ {date}
+ {date === x.default_value && }
+
+
+ );
+ })}
+
+
+ );
+ return (
+
+
+ {icon}
+ {x.name}
+
+
+ {input}
+
+
+
+
+
+
+
+
+ {
+ setSelectIndex(i);
+ setOpen(true);
+ }}
+ >
+ Edit filter
+
+ {
+ props.onFilters([
+ ...props.filters.filter((_, idx) => idx !== i),
+ ]);
+ }}
+ >
+ Remove
+
+
+
+
+ );
+ });
+
+ return (
+ <>
+
+
+ {open && selectIndex !== undefined && (
+
{
+ setOpen(false);
+ if (props.filters[selectIndex].new === true) {
+ props.onFilters([
+ ...props.filters.filter((_, i) => i !== selectIndex),
+ ]);
+ setSelectIndex(undefined);
+ }
+ }}
+ filter={props.filters[selectIndex]}
+ onFilter={(v) => {
+ const data = structuredClone(props.filters);
+ data[selectIndex] = v;
+ props.onFilters(data);
+ }}
+ onAddFilter={() => {
+ const data = structuredClone(props.filters);
+ data[selectIndex].new = false;
+ setOpen(false);
+ props.onFilters(data);
+ }}
+ />
+ )}
+
+ {mapFilterItem}
+
+
+
+
+
+ >
+ );
+}
diff --git a/src/components/board/board-provider.tsx b/src/components/board/board-provider.tsx
new file mode 100644
index 00000000..4267af36
--- /dev/null
+++ b/src/components/board/board-provider.tsx
@@ -0,0 +1,19 @@
+import { BoardSourceDriver } from "@/drivers/board-source/base-source";
+import { createContext, PropsWithChildren } from "react";
+
+interface BoardContextProps {
+ sources?: BoardSourceDriver;
+}
+
+const BoardContext = createContext({});
+
+export function BoardProvider({
+ children,
+ sources,
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/board/board-style.css b/src/components/board/board-style.css
new file mode 100644
index 00000000..8fa084e3
--- /dev/null
+++ b/src/components/board/board-style.css
@@ -0,0 +1,139 @@
+.react-grid-layout {
+ position: relative;
+ transition: height 200ms ease;
+}
+.react-grid-item {
+ transition: all 200ms ease;
+ transition-property: left, top, width, height;
+}
+.react-grid-item img {
+ pointer-events: none;
+ user-select: none;
+}
+.react-grid-item.cssTransforms {
+ transition-property: transform, width, height;
+}
+.react-grid-item.resizing {
+ transition: none;
+ z-index: 1;
+ will-change: width, height;
+}
+
+.react-grid-item.react-draggable-dragging {
+ transition: none;
+ z-index: 3;
+ will-change: transform;
+}
+
+.react-grid-item.dropping {
+ visibility: hidden;
+}
+
+.react-grid-item.react-grid-placeholder {
+ background: #c9c9c9;
+ opacity: 0.2;
+ transition-duration: 100ms;
+ z-index: 2;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.react-grid-item.react-grid-placeholder.placeholder-resizing {
+ transition: none;
+}
+
+.react-grid-item > .react-resizable-handle {
+ position: absolute;
+ width: 20px;
+ height: 20px;
+}
+
+.react-grid-item > .react-resizable-handle::after {
+ content: "";
+ position: absolute;
+ right: 3px;
+ bottom: 3px;
+ width: 5px;
+ height: 5px;
+ border-right: 2px solid rgba(0, 0, 0, 0.4);
+ border-bottom: 2px solid rgba(0, 0, 0, 0.4);
+}
+
+.react-resizable-hide > .react-resizable-handle {
+ display: none;
+}
+
+.react-grid-item > .react-resizable-handle.react-resizable-handle-sw {
+ bottom: 0;
+ left: 0;
+ cursor: sw-resize;
+ transform: rotate(90deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-se {
+ bottom: 0;
+ right: 0;
+ cursor: se-resize;
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-nw {
+ top: 0;
+ left: 0;
+ cursor: nw-resize;
+ transform: rotate(180deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-ne {
+ top: 0;
+ right: 0;
+ cursor: ne-resize;
+ transform: rotate(270deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-w,
+.react-grid-item > .react-resizable-handle.react-resizable-handle-e {
+ top: 50%;
+ margin-top: -10px;
+ cursor: ew-resize;
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-w {
+ left: 0;
+ transform: rotate(135deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-e {
+ right: 0;
+ transform: rotate(315deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-n,
+.react-grid-item > .react-resizable-handle.react-resizable-handle-s {
+ left: 50%;
+ margin-left: -10px;
+ cursor: ns-resize;
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-n {
+ top: 0;
+ transform: rotate(225deg);
+}
+.react-grid-item > .react-resizable-handle.react-resizable-handle-s {
+ bottom: 0;
+ transform: rotate(45deg);
+}
+
+.reveal {
+ opacity: 0;
+ transform: translateY(20px);
+}
+
+.animate-revealMenu {
+ animation: revealMenu 1s cubic-bezier(0.7, 0, 0, 1.2) forwards;
+}
+
+@keyframes revealMenu {
+ 0% {
+ opacity: 100%;
+ transform: translateY(100px) translateX(-40%);
+ }
+ 100% {
+ opacity: 100%;
+ transform: translateY(0) translateX(-40%);
+ }
+}
diff --git a/src/components/board/board-tool.tsx b/src/components/board/board-tool.tsx
new file mode 100644
index 00000000..8c1a4ec6
--- /dev/null
+++ b/src/components/board/board-tool.tsx
@@ -0,0 +1,38 @@
+import { ChartLine, ImageUpscale } from "lucide-react";
+import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
+
+interface Props {
+ editMode: "ADD_CHART" | "REARRANGING_CHART" | null;
+ setEditMode: (v: "ADD_CHART" | "REARRANGING_CHART") => void;
+}
+
+export function BoardTool(props: Props) {
+ return (
+
+
+
+
+
+
+
+ Add Chart
+
+
+
+
+
+
+
+ Rearranging charts
+
+
+
+
+ );
+}
diff --git a/src/components/board/index.tsx b/src/components/board/index.tsx
new file mode 100644
index 00000000..654870ad
--- /dev/null
+++ b/src/components/board/index.tsx
@@ -0,0 +1,56 @@
+import { BoardSourceDriver } from "@/drivers/board-source/base-source";
+import { useState } from "react";
+import { BoardCanvas } from "./board-canvas";
+import { BoardFilter } from "./board-filter";
+import { BoardFilterProps } from "./board-filter-dialog";
+import { BoardProvider } from "./board-provider";
+
+interface DashboardProps {
+ layout: ReactGridLayout.Layout[];
+ data: {
+ filters: BoardFilterProps[];
+ };
+}
+
+interface Props {
+ value: DashboardProps;
+ sources?: BoardSourceDriver;
+ setValue: (value: DashboardProps) => void;
+}
+
+export default function Board({ value, setValue, sources }: Props) {
+ const [editMode, setEditMode] = useState<
+ "ADD_CHART" | "REARRANGING_CHART" | null
+ >(null);
+
+ return (
+
+
+
+ setValue({
+ ...value,
+ data: {
+ ...value.data,
+ filters: v,
+ },
+ })
+ }
+ editMode={editMode}
+ setEditMode={setEditMode}
+ />
+ {
+ setValue({
+ ...value,
+ layout: v,
+ });
+ }}
+ editMode={editMode}
+ />
+
+
+ );
+}
diff --git a/src/components/button-group.tsx b/src/components/button-group.tsx
new file mode 100644
index 00000000..d5cb22cb
--- /dev/null
+++ b/src/components/button-group.tsx
@@ -0,0 +1,40 @@
+import { cn } from "@/lib/utils";
+import { PropsWithChildren } from "react";
+
+export function ButtonGroup({
+ children,
+}: PropsWithChildren<{ suppressHydrationWarning?: boolean }>) {
+ return (
+
+ {children}
+
+ );
+}
+
+interface ButtonGroupItemProps {
+ onClick?: () => void;
+ selected?: boolean;
+ suppressHydrationWarning?: boolean;
+}
+
+export function ButtonGroupItem({
+ children,
+ selected,
+ onClick,
+}: PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/components/chart/index.tsx b/src/components/chart/index.tsx
new file mode 100644
index 00000000..d379da30
--- /dev/null
+++ b/src/components/chart/index.tsx
@@ -0,0 +1,171 @@
+"use client";
+import * as echarts from "echarts";
+import { EChartsOption } from "echarts";
+import { useEffect, useRef } from "react";
+
+interface ChartLayer {
+ sql: string;
+ type: string;
+}
+
+interface ChartOptions {
+ theme?: string;
+ xAxisKey: string;
+ yAxisKeys: string[];
+ yAxisKeyColors: {
+ [key: string]: string;
+ };
+}
+
+interface ChartParams {
+ id: string;
+ name: string;
+ type: ChartType;
+ model: string;
+ apiKey: string;
+ layers: ChartLayer[];
+ options: ChartOptions;
+ source_id: string;
+ created_at: string;
+ updated_at: string;
+ workspace_id: string;
+ connection_id: string | null;
+}
+
+export interface ChartValue {
+ connection_id: string | null;
+ created_at: string;
+ id: string;
+ model: string;
+ name: string;
+ params: ChartParams;
+ source_id: string;
+ type: ChartType;
+ updated_at: string;
+ workspace_id: string;
+}
+
+export interface ChartData {
+ [key: string]: any;
+}
+
+export type ChartType = "line" | "bar" | "pie" | "column" | "scatter";
+
+interface OuterbaseChartProps {
+ data: ChartData[];
+ value: ChartValue;
+ modifier?: EChartsOption;
+ className?: string;
+}
+
+export default function Chart({
+ value,
+ data,
+ modifier,
+ className,
+}: OuterbaseChartProps) {
+ const domRef = useRef(null);
+
+ useEffect(() => {
+ const props = transfromOutbaseChartData({
+ value,
+ data,
+ modifier,
+ });
+ if (domRef.current) {
+ let chartInstance = echarts.getInstanceByDom(domRef.current);
+ if (chartInstance) {
+ chartInstance.dispose();
+ }
+ chartInstance = echarts.init(domRef.current);
+ chartInstance.clear();
+ chartInstance.setOption(props);
+ }
+ }, [domRef, value, data, modifier]);
+
+ return (
+
+ );
+}
+
+export function transfromOutbaseChartData({
+ value,
+ data,
+ modifier,
+}: OuterbaseChartProps) {
+ const xAxisData = data.map(
+ (item) => item[value?.params?.options?.xAxisKey] ?? ""
+ );
+ const seriesData = value?.params?.options?.yAxisKeys.map((key) => {
+ const color = value?.params?.options?.yAxisKeyColors?.[key] ?? "";
+ const chartType = value?.type;
+ const baseSeries = {
+ name: key,
+ type: value.type,
+ data: data.map((item) => {
+ if (chartType === "pie") {
+ return {
+ value: item[key],
+ name: item[value?.params?.options?.xAxisKey] ?? "",
+ };
+ } else {
+ return item[key];
+ }
+ }),
+ itemStyle: {
+ color: color,
+ },
+ };
+ if (chartType === "pie") {
+ return pieChartDecoration(baseSeries);
+ }
+ return baseSeries;
+ });
+
+ if (value.type === "pie") {
+ return {
+ series: seriesData,
+ ...modifier,
+ };
+ }
+
+ return {
+ xAxis: {
+ type: "category",
+ data: xAxisData,
+ },
+ yAxis: {
+ type: "value",
+ },
+ series: seriesData,
+ ...modifier,
+ };
+}
+
+function pieChartDecoration(baseSeries: any) {
+ return {
+ ...baseSeries,
+ type: "pie",
+ radius: ["40%", "70%"],
+ avoidLabelOverlap: false,
+ itemStyle: {
+ borderRadius: 10,
+ borderColor: "#fff",
+ borderWidth: 2,
+ },
+ label: {
+ show: false,
+ position: "center",
+ },
+ emphasis: {
+ label: {
+ show: true,
+ fontSize: 40,
+ fontWeight: "bold",
+ },
+ },
+ labelLine: {
+ show: false,
+ },
+ };
+}
diff --git a/src/components/code-block.tsx b/src/components/code-block.tsx
index 453d5fd3..9de278be 100644
--- a/src/components/code-block.tsx
+++ b/src/components/code-block.tsx
@@ -1,3 +1,4 @@
+import React from "react";
import { createHighlighter } from "shiki";
import type { BundledLanguage } from "shiki/bundle/full";
@@ -55,9 +56,19 @@ interface MDXCodeBlockProps {
async function CodeBlock(props: MDXCodeBlockProps) {
if (typeof props.children === "string") {
+ console.log("here");
return ;
}
+ const codeElement = React.Children.toArray(props.children).find(
+ (child) =>
+ React.isValidElement(child) && (child as any).type().type === "code"
+ ) as React.ReactElement | undefined;
+
+ if (codeElement && typeof codeElement.props.children === "string") {
+ return ;
+ }
+
return (
diff --git a/src/components/gui/json-editor/index.tsx b/src/components/gui/json-editor/index.tsx
index 60d87391..4efae351 100644
--- a/src/components/gui/json-editor/index.tsx
+++ b/src/components/gui/json-editor/index.tsx
@@ -1,10 +1,10 @@
-import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { json } from "@codemirror/lang-json";
-import { forwardRef, useMemo } from "react";
import { tags as t } from "@lezer/highlight";
-import createTheme from "@uiw/codemirror-themes";
-import { useTheme } from "@/context/theme-provider";
import { indentationMarkers } from "@replit/codemirror-indentation-markers";
+import createTheme from "@uiw/codemirror-themes";
+import CodeMirror, { ReactCodeMirrorRef } from "@uiw/react-codemirror";
+import { useTheme } from "next-themes";
+import { forwardRef, useMemo } from "react";
interface JsonEditorProps {
value: string;
@@ -13,10 +13,10 @@ interface JsonEditorProps {
}
function useJsonTheme() {
- const { theme } = useTheme();
+ const { resolvedTheme } = useTheme();
return useMemo(() => {
- if (theme === "light") {
+ if (resolvedTheme === "light") {
return createTheme({
theme: "light",
settings: {
@@ -69,7 +69,7 @@ function useJsonTheme() {
],
});
}
- }, [theme]);
+ }, [resolvedTheme]);
}
const JsonEditor = forwardRef(
diff --git a/src/components/gui/query-result-table.tsx b/src/components/gui/query-result-table.tsx
index e45e389a..0f4f716f 100644
--- a/src/components/gui/query-result-table.tsx
+++ b/src/components/gui/query-result-table.tsx
@@ -256,6 +256,12 @@ export default function ResultTable({
const data = pasteValue.split("\r\n").map((row) => row.split("\t"));
for (let row = 0; row < data.length; row++) {
+ //filter out the additional enter from excel copy
+ if (row === data.length - 1) {
+ if (data[row].length === 1 && data[row][0] === "") {
+ break;
+ }
+ }
for (let col = 0; col < data[row].length; col++) {
state.changeValue(
y + row,
diff --git a/src/components/gui/sidebar-tab.tsx b/src/components/gui/sidebar-tab.tsx
index 7378d649..b8ca106e 100644
--- a/src/components/gui/sidebar-tab.tsx
+++ b/src/components/gui/sidebar-tab.tsx
@@ -1,9 +1,11 @@
import { useConfig } from "@/context/config-provider";
-import { useTheme } from "@/context/theme-provider";
import { cn } from "@/lib/utils";
-import { ReactElement, useMemo, useState } from "react";
-import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
+import { ArrowLeft } from "@phosphor-icons/react";
+import { useTheme } from "next-themes";
import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import { ReactElement, useMemo, useState } from "react";
+import ThemeToggle from "../theme-toggle";
import {
DropdownMenu,
DropdownMenuContent,
@@ -11,7 +13,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
-import { ArrowLeft, MoonStars, Sun } from "@phosphor-icons/react";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export interface SidebarTabItem {
key: string;
@@ -26,14 +28,18 @@ interface SidebarTabProps {
}
export default function SidebarTab({ tabs }: Readonly) {
+ const { forcedTheme } = useTheme();
const [selectedIndex, setSelectedIndex] = useState(0);
- const { theme, toggleTheme, disableToggle } = useTheme();
const [loadedIndex, setLoadedIndex] = useState(() => {
const a: boolean[] = new Array(tabs.length).fill(false);
a[0] = true;
return a;
});
+ const searchParams = useSearchParams();
+ const disableToggle =
+ searchParams.get("disableThemeToggle") === "1" || forcedTheme;
+
const config = useConfig();
/**
@@ -103,26 +109,18 @@ export default function SidebarTab({ tabs }: Readonly) {
{!disableToggle && (
-
{
- toggleTheme();
- }}
- >
- {theme === "dark" ? (
-
- ) : (
-
- )}
- Switch to {theme === "dark" ? "light mode" : "dark mode"}
-
+
+
+
)}
+
{config.onBack && (
Back to bases
)}
- {config.onBack && !disableToggle &&
}
+ {config.onBack &&
}
{
- if (theme === "light") {
+ if (resolvedTheme === "light") {
return createTheme({
theme: "light",
settings: {
@@ -73,5 +73,5 @@ export default function useCodeEditorTheme({
],
});
}
- }, [theme, fontSize]);
+ }, [resolvedTheme, fontSize]);
}
diff --git a/src/components/gui/studio.tsx b/src/components/gui/studio.tsx
index 17586f32..6090d7d9 100644
--- a/src/components/gui/studio.tsx
+++ b/src/components/gui/studio.tsx
@@ -2,14 +2,14 @@
import MainScreen from "@/components/gui/main-connection";
import { ConfigProvider } from "@/context/config-provider";
import { DriverProvider } from "@/context/driver-provider";
+import { StudioExtensionManager } from "@/core/extension-manager";
+import { BeforeQueryPipeline } from "@/core/query-pipeline";
import type { BaseDriver } from "@/drivers/base-driver";
-import { useEffect, useMemo, useRef } from "react";
import { CollaborationBaseDriver } from "@/drivers/collaboration-driver-base";
import { SavedDocDriver } from "@/drivers/saved-doc/saved-doc-driver";
-import { FullEditorProvider } from "./providers/full-editor-provider";
+import { useEffect, useMemo, useRef } from "react";
import { CommonDialogProvider } from "../common-dialog";
-import { StudioExtensionManager } from "@/core/extension-manager";
-import { BeforeQueryPipeline } from "@/core/query-pipeline";
+import { FullEditorProvider } from "./providers/full-editor-provider";
interface StudioProps {
driver: BaseDriver;
@@ -82,7 +82,6 @@ export function Studio({
}, [extensions]);
useEffect(() => {
- finalExtensionManager.init();
return () => finalExtensionManager.cleanup();
}, [finalExtensionManager]);
diff --git a/src/components/gui/table-optimized/OptimizeTableState.tsx b/src/components/gui/table-optimized/OptimizeTableState.tsx
index 91ed47a0..22c6b1f1 100644
--- a/src/components/gui/table-optimized/OptimizeTableState.tsx
+++ b/src/components/gui/table-optimized/OptimizeTableState.tsx
@@ -1,11 +1,11 @@
-import { OptimizeTableHeaderProps } from ".";
-import deepEqual from "deep-equal";
-import { formatNumber } from "@/lib/convertNumber";
-import { selectArrayFromIndexList } from "@/lib/export-helper";
import {
buildTableResultHeader,
BuildTableResultProps,
} from "@/lib/build-table-result";
+import { formatNumber } from "@/lib/convertNumber";
+import { selectArrayFromIndexList } from "@/lib/export-helper";
+import deepEqual from "deep-equal";
+import { OptimizeTableHeaderProps } from ".";
export interface OptimizeTableRowValue {
raw: Record;
@@ -321,6 +321,8 @@ export default class OptimizeTableState {
if (removedRows.length > 0) {
this.data = this.data.filter((row) => !removedRows.includes(row));
+ // after rows were removed, we need to deselect them
+ this.selectionRanges = [];
}
this.changeLogs = {};
diff --git a/src/components/gui/tabs/relational-diagram-tab/erd-table-column.tsx b/src/components/gui/tabs/relational-diagram-tab/erd-table-column.tsx
index 257bad71..b747512a 100644
--- a/src/components/gui/tabs/relational-diagram-tab/erd-table-column.tsx
+++ b/src/components/gui/tabs/relational-diagram-tab/erd-table-column.tsx
@@ -31,11 +31,11 @@ export default function ERDTableColumn({
{column.title}
-
+
-
- {column.type}
+
{column.type}
diff --git a/src/components/gui/toolbar.tsx b/src/components/gui/toolbar.tsx
index af2c71d4..564e4514 100644
--- a/src/components/gui/toolbar.tsx
+++ b/src/components/gui/toolbar.tsx
@@ -1,12 +1,12 @@
+import { cn } from "@/lib/utils";
+import { LucideLoader } from "lucide-react";
import { PropsWithChildren, ReactElement } from "react";
import { buttonVariants } from "../ui/button";
-import { LucideLoader } from "lucide-react";
-import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
import { Separator } from "../ui/separator";
-import { cn } from "@/lib/utils";
+import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip";
export function Toolbar({ children }: PropsWithChildren) {
- return
{children}
;
+ return
{children}
;
}
export function ToolbarSeparator() {
@@ -17,6 +17,10 @@ export function ToolbarSeparator() {
);
}
+export function ToolbarFiller() {
+ return
;
+}
+
export function ToolbarButton({
disabled,
loading,
@@ -46,12 +50,12 @@ export function ToolbarButton({
disabled={disabled}
onClick={onClick}
>
- {loading ?
: icon}
-
{text}
+ {loading ?
: icon}
+ {text &&
{text} }
{badge && (
diff --git a/src/components/label-input.tsx b/src/components/label-input.tsx
new file mode 100644
index 00000000..aa96e147
--- /dev/null
+++ b/src/components/label-input.tsx
@@ -0,0 +1,11 @@
+import { Input, InputProps } from "./ui/input";
+import { Label } from "./ui/label";
+
+export default function LabelInput(props: InputProps & { label: string }) {
+ return (
+
+ {props.label}
+
+
+ );
+}
diff --git a/src/components/listview/index.tsx b/src/components/listview/index.tsx
index 17206b45..3e2be101 100644
--- a/src/components/listview/index.tsx
+++ b/src/components/listview/index.tsx
@@ -19,6 +19,7 @@ import React, {
useRef,
useState,
} from "react";
+import HighlightText from "../ui/highlight-text";
export interface ListViewItem {
key: string;
@@ -57,31 +58,6 @@ interface ListViewRendererProps extends ListViewProps {
contextOpen: boolean;
}
-function Highlight({ text, highlight }: { text: string; highlight?: string }) {
- if (!highlight) return {text} ;
-
- const regex = new RegExp(
- "(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")",
- "i"
- );
-
- const splitedText = text.split(regex);
-
- return (
-
- {splitedText.map((text, idx) => {
- return text.toLowerCase() === (highlight ?? "").toLowerCase() ? (
-
- {text}
-
- ) : (
- {text}
- );
- })}
-
- );
-}
-
function Indentation({ depth }: { depth: number }) {
if (depth <= 0) return null;
@@ -219,7 +195,7 @@ function renderList(props: ListViewRendererProps): React.ReactElement {
{item.iconBadgeColor && (
@@ -228,7 +204,7 @@ function renderList(props: ListViewRendererProps): React.ReactElement {
)}
-
+
{item.badgeContent && (
(props: ListViewRendererProps): React.ReactElement {
{item.progressBarValue && item.progressBarMax && (
-
+
) {
+}: PropsWithChildren<{ title?: string; group?: string }>) {
return (
<>
-
-
- {group &&
{group} }
-
{title}
+ {title && (
+
+
+ {group && {group} }
+
{title}
+
-
-
{children}
+ )}
+
+ {children}
+
>
);
}
@@ -36,7 +41,7 @@ export function DocLayout({
<>
-
{children}
+
{children}
>
);
diff --git a/src/components/resource-card/icon.tsx b/src/components/resource-card/icon.tsx
new file mode 100644
index 00000000..5e1d3987
--- /dev/null
+++ b/src/components/resource-card/icon.tsx
@@ -0,0 +1,184 @@
+type IconProps = {
+ className?: string;
+};
+
+export const CloudflareIcon = ({ className }: IconProps) => (
+
+
+
+
+);
+
+export const DigitalOceanIcon = ({ className }: IconProps) => (
+
+
+
+
+
+
+);
+
+export const NeonIcon = ({ className }: IconProps) => (
+
+
+
+);
+
+export const RQLiteIcon = ({ className }: IconProps) => (
+
+
+
+
+
+
+
+
+);
+
+export const StarbaseIcon = ({ className }: IconProps) => (
+
+
+
+);
+
+export const TursoIcon = ({ className }: IconProps) => (
+
+
+
+);
+
+export const SupabaseIcon = ({ className }: IconProps) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export const ValTownIcon = ({ className }: IconProps) => (
+
+
+
+);
diff --git a/src/components/resource-card/index.tsx b/src/components/resource-card/index.tsx
new file mode 100644
index 00000000..427c783b
--- /dev/null
+++ b/src/components/resource-card/index.tsx
@@ -0,0 +1,152 @@
+import { cn } from "@/lib/utils";
+import {
+ Circle,
+ Database,
+ DotsThreeVertical,
+ Triangle,
+} from "@phosphor-icons/react";
+import Link from "next/link";
+import { PropsWithChildren, useState } from "react";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+} from "../ui/dropdown-menu";
+import { BoardVisual } from "./visual";
+
+interface DatabaseCardProps {
+ className?: string;
+ href: string;
+ status?: string;
+ statusType?: "error" | "info" | "success";
+ title?: string;
+ subtitle?: string;
+ color?: string;
+ visual?: React.FC
>;
+ icon?: React.FC>;
+}
+
+export default function ResourceCard({
+ className,
+ color,
+ href,
+ status,
+ title,
+ subtitle,
+ icon: IconComponent = Database,
+ visual: VisualComponent = BoardVisual,
+ children,
+}: PropsWithChildren) {
+ const [open, setOpen] = useState(false);
+
+ return (
+
+ {/* content */}
+
+
+ {IconComponent && }
+
+
+
+ {title}
+
+ {subtitle && (
+
{subtitle}
+ )}
+
+
+
+ {/* status */}
+
+ {status === "success" ? (
+
+ ) : status === "error" ? (
+
+ ) : null}
+
+
{status}
+
+
+ {VisualComponent && }
+
+
+
+ {children && (
+
+
+ {
+ e.stopPropagation();
+ e.preventDefault();
+ }}
+ className={cn(
+ "absolute top-2 right-2 z-10 flex size-8 cursor-pointer items-center justify-center rounded border border-neutral-200 bg-white opacity-0 duration-100 group-hover:opacity-100 hover:bg-neutral-50 focus:opacity-100 focus:outline-none focus-visible:ring focus-visible:ring-blue-600 dark:border-neutral-700 dark:bg-neutral-900 dark:hover:bg-neutral-800",
+ { "opacity-100": open }
+ )}
+ >
+
+
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+ {children}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/resource-card/utils.tsx b/src/components/resource-card/utils.tsx
new file mode 100644
index 00000000..8bf06634
--- /dev/null
+++ b/src/components/resource-card/utils.tsx
@@ -0,0 +1,48 @@
+import { Database } from "@phosphor-icons/react";
+import {
+ MySQLIcon,
+ PostgreIcon,
+ RqliteIcon,
+ SQLiteIcon,
+} from "../icons/outerbase-icon";
+import { CloudflareIcon, StarbaseIcon, ValTownIcon } from "./icon";
+import { GeneralVisual, MySQLVisual, SQLiteVisual } from "./visual";
+
+export function getDatabaseFriendlyName(type: string) {
+ if (type === "sqlite") return "SQLite";
+ if (type === "mysql") return "MySQL";
+ if (type === "postgres") return "PostgreSQL";
+ if (type === "rqlite") return "rqlite";
+ if (type === "turso") return "Turso";
+ if (type === "libsql") return "LibSQL";
+ if (type === "mssql") return "Microsoft SQL";
+ if (type === "snowflake") return "Snowflake";
+ if (type === "motherduck") return "Motherduck";
+ if (type === "duckdb") return "DuckDB";
+ if (type === "cloudflare" || type === "cloudflare-d1") return "Cloudflare";
+ if (type === "starbasedb") return "StarbaseDB";
+ if (type === "starbase") return "StarbaseDB";
+ if (type === "bigquery") return "BigQuery";
+
+ return type;
+}
+
+export function getDatabaseIcon(type: string) {
+ if (type === "mysql") return MySQLIcon;
+ if (type === "postgres") return PostgreIcon;
+ if (type === "cloudflare" || type === "cloudflare-d1") return CloudflareIcon;
+ if (type === "valtown") return ValTownIcon;
+ if (type === "starbasedb" || type === "starbase") return StarbaseIcon;
+ if (type === "libsql" || type === "turso") return SQLiteIcon;
+ if (type === "rqlite") return RqliteIcon;
+
+ return Database;
+}
+
+export function getDatabaseVisual(type: string) {
+ if (type === "mysql") return MySQLVisual;
+ if (type === "sqlite") return SQLiteVisual;
+ if (type === "postgres") return GeneralVisual;
+
+ return GeneralVisual;
+}
diff --git a/src/components/resource-card/visual.tsx b/src/components/resource-card/visual.tsx
new file mode 100644
index 00000000..9cdd7d19
--- /dev/null
+++ b/src/components/resource-card/visual.tsx
@@ -0,0 +1,119 @@
+export const PostgresVisual = () => (
+
+
+
+
+);
+
+export const MySQLVisual = () => (
+
+
+
+
+);
+
+export const SQLiteVisual = () => (
+
+
+
+
+);
+
+export const BoardVisual = () => (
+
+
+
+
+);
+
+export const GeneralVisual = () => (
+
+
+
+
+);
diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx
index 0e460dc3..80ba03b7 100644
--- a/src/components/sidebar-menu.tsx
+++ b/src/components/sidebar-menu.tsx
@@ -33,6 +33,14 @@ export function SidebarMenuItem({
);
if (href) {
+ if (href.startsWith("https://")) {
+ return (
+
+ {body}
+
+ );
+ }
+
return (
{body}
diff --git a/src/components/theme-toggle.tsx b/src/components/theme-toggle.tsx
new file mode 100644
index 00000000..ecfc4f0c
--- /dev/null
+++ b/src/components/theme-toggle.tsx
@@ -0,0 +1,61 @@
+"use client";
+import { Gear, Moon, Sun } from "@phosphor-icons/react";
+import { useTheme } from "next-themes";
+import { useEffect, useState } from "react";
+import { ButtonGroup, ButtonGroupItem } from "./button-group";
+
+export default function ThemeToggle() {
+ const { theme, setTheme } = useTheme();
+ const [mounted, setMounted] = useState(false);
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // This is to prevent mismatched render on the server
+ // This is just a placeholder to avoid flicking as well
+ // https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
+ if (!mounted)
+ return (
+
+
+
+ System
+
+
+
+ Dark
+
+
+
+ Light
+
+
+ );
+
+ return (
+
+ setTheme("system")}
+ selected={theme === "system"}
+ >
+
+ System
+
+ setTheme("dark")}
+ selected={theme === "dark"}
+ >
+
+ Dark
+
+ setTheme("light")}
+ selected={theme === "light"}
+ >
+
+ Light
+
+
+ );
+}
diff --git a/src/components/ui/highlight-text.tsx b/src/components/ui/highlight-text.tsx
new file mode 100644
index 00000000..8097a1cc
--- /dev/null
+++ b/src/components/ui/highlight-text.tsx
@@ -0,0 +1,29 @@
+"use client";
+interface Props {
+ text: string;
+ highlight?: string;
+}
+export default function HighlightText({ text, highlight }: Props) {
+ if (!highlight) return {text} ;
+
+ const regex = new RegExp(
+ "(" + (highlight ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + ")",
+ "i"
+ );
+
+ const splitedText = text.split(regex);
+
+ return (
+
+ {splitedText.map((text, idx) => {
+ return text.toLowerCase() === (highlight ?? "").toLowerCase() ? (
+
+ {text}
+
+ ) : (
+ {text}
+ );
+ })}
+
+ );
+}
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index 90b82c8f..b7046af1 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -1,16 +1,16 @@
"use client";
-import { useTheme } from "@/context/theme-provider";
+import { useTheme } from "next-themes";
import { Toaster as Sonner } from "sonner";
type ToasterProps = React.ComponentProps;
const Toaster = ({ ...props }: ToasterProps) => {
- const { theme } = useTheme();
+ const { resolvedTheme } = useTheme();
return (
void;
-}>({
- theme: "dark",
- disableToggle: false,
- toggleTheme: () => {
- throw new Error("Not implemented");
- },
-});
-
-export function useTheme() {
- return useContext(ThemeContext);
-}
-
-export default function ThemeProvider({
- children,
- defaultTheme,
- disableToggle,
-}: PropsWithChildren<{ defaultTheme: ThemeType; disableToggle?: boolean }>) {
- const [theme, setTheme] = useState(defaultTheme);
-
- const toggleTheme = useCallback(
- (assignedTheme?: string) => {
- setTheme((prevTheme) => {
- if (assignedTheme) {
- return assignedTheme === "dark" ? "dark" : "light";
- }
-
- const newTheme = prevTheme === "dark" ? "light" : "dark";
- setCookie("theme", newTheme);
- return newTheme;
- });
- },
- [setTheme]
- );
-
- useEffect(() => {
- if (theme === "light") {
- window.document.body.classList.remove("dark");
- } else {
- window.document.body.classList.add("dark");
- }
- }, [theme]);
-
- const value = useMemo(
- () => ({ toggleTheme, theme, disableToggle: disableToggle ?? false }),
- [toggleTheme, theme, disableToggle]
- );
-
- return (
- {children}
- );
-}
diff --git a/src/core/extension-manager.tsx b/src/core/extension-manager.tsx
index 6cb9e44d..5071e8bb 100644
--- a/src/core/extension-manager.tsx
+++ b/src/core/extension-manager.tsx
@@ -46,7 +46,9 @@ export class StudioExtensionContext {
protected resourceContextMenu: Record =
{};
- constructor(protected extensions: IStudioExtension[]) {}
+ constructor(protected extensions: IStudioExtension[]) {
+ this.extensions.forEach((ext) => ext.init(this));
+ }
registerBeforeQuery(handler: BeforeQueryHandler) {
this.beforeQueryHandlers.push(handler);
@@ -82,12 +84,14 @@ export class StudioExtensionContext {
registerQueryCellContextMenu(handler: QueryResultCellMenuHandler) {
this.queryResultCellContextMenu.push(handler);
}
+
+ getExtension(name: string): T | undefined {
+ return this.extensions.find((ext) => ext.extensionName === name) as
+ | T
+ | undefined;
+ }
}
export class StudioExtensionManager extends StudioExtensionContext {
- init() {
- this.extensions.forEach((ext) => ext.init(this));
- }
-
cleanup() {
this.extensions.forEach((ext) => ext.cleanup());
}
diff --git a/src/core/standard-extension.tsx b/src/core/standard-extension.tsx
index 9aa23416..3923b6f2 100644
--- a/src/core/standard-extension.tsx
+++ b/src/core/standard-extension.tsx
@@ -2,10 +2,10 @@
* This contains the standard extensions as a base for all databases.
*/
+import ColumnDescriptorExtension from "@/extensions/column-descriptor";
import QueryHistoryConsoleLogExtension from "@/extensions/query-console-log";
-import ViewEditorExtension from "@/extensions/view-editor";
import TriggerEditorExtension from "@/extensions/trigger-editor";
-import ColumnDescriptorExtension from "@/extensions/column-descriptor";
+import ViewEditorExtension from "@/extensions/view-editor";
export function createStandardExtensions() {
return [
diff --git a/src/drivers/board-source/README b/src/drivers/board-source/README
new file mode 100644
index 00000000..6c1e4fea
--- /dev/null
+++ b/src/drivers/board-source/README
@@ -0,0 +1 @@
+This is multiple database driver that will be used for board
\ No newline at end of file
diff --git a/src/drivers/board-source/base-source.ts b/src/drivers/board-source/base-source.ts
new file mode 100644
index 00000000..a8aaadc6
--- /dev/null
+++ b/src/drivers/board-source/base-source.ts
@@ -0,0 +1,12 @@
+
+export interface BoardSource {
+ id: string;
+ name: string;
+ type: string;
+}
+
+export abstract class BoardSourceDriver {
+ abstract sourceList(): BoardSource[];
+ abstract query(sourceId: string, statement: string): Promise[]>
+ abstract cleanup(): void;
+}
\ No newline at end of file
diff --git a/src/extensions/data-catalog/data-model-tab.tsx b/src/extensions/data-catalog/data-model-tab.tsx
new file mode 100644
index 00000000..3e1d85eb
--- /dev/null
+++ b/src/extensions/data-catalog/data-model-tab.tsx
@@ -0,0 +1,392 @@
+import SchemaNameSelect from "@/components/gui/schema-editor/schema-name-select";
+import { Toolbar, ToolbarFiller } from "@/components/gui/toolbar";
+import { Button } from "@/components/ui/button";
+import { Checkbox } from "@/components/ui/checkbox";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
+import HighlightText from "@/components/ui/highlight-text";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+import { useConfig } from "@/context/config-provider";
+import { useDatabaseDriver } from "@/context/driver-provider";
+import { useSchema } from "@/context/schema-provider";
+import {
+ DatabaseTableColumn,
+ DatabaseTableSchema,
+} from "@/drivers/base-driver";
+import { MagicWand } from "@phosphor-icons/react";
+import { LucideLoader, LucideMoreHorizontal } from "lucide-react";
+import { useCallback, useMemo, useState } from "react";
+import DataCatalogExtension from ".";
+import DataCatalogDriver from "./driver";
+
+interface DataCatalogTableColumnModalProps {
+ driver: DataCatalogDriver;
+ schemaName: string;
+ tableName: string;
+ columnName: string;
+ onClose: () => void;
+}
+
+function DataCatalogTableColumnModal({
+ driver,
+ schemaName,
+ tableName,
+ columnName,
+ onClose,
+}: DataCatalogTableColumnModalProps) {
+ const modelColumn = driver.getColumn(schemaName, tableName, columnName);
+ const { databaseDriver } = useDatabaseDriver();
+
+ const [definition, setDefinition] = useState(modelColumn?.definition ?? "");
+ const [samples, setSamples] = useState(
+ (modelColumn?.samples ?? []).join(",")
+ );
+
+ const [loading, setLoading] = useState(false);
+ const [sampleLoading, setSampleLoading] = useState(false);
+
+ const onAutomaticSampleData = useCallback(() => {
+ setSampleLoading(true);
+ databaseDriver
+ .query(
+ `SELECT DISTINCT ${databaseDriver.escapeId(columnName)} FROM ${databaseDriver.escapeId(schemaName)}.${databaseDriver.escapeId(tableName)} LIMIT 10`
+ )
+ .then((r) => {
+ setSamples(r.rows.map((row) => row[columnName]).join(", "));
+ })
+ .finally(() => setSampleLoading(false));
+ }, [databaseDriver, columnName, schemaName, tableName]);
+
+ const onSaveUpdateColumn = useCallback(() => {
+ setLoading(true);
+
+ driver
+ .updateColumn(schemaName, tableName, columnName, {
+ definition,
+ samples:
+ samples && samples.trim()
+ ? samples.split(",").map((s) => s.trim())
+ : [],
+ hideFromEzql: modelColumn?.hideFromEzql ?? false,
+ })
+ .then()
+ .finally(() => {
+ setLoading(false);
+ onClose();
+ });
+ }, [
+ driver,
+ modelColumn,
+ samples,
+ columnName,
+ onClose,
+ schemaName,
+ tableName,
+ definition,
+ ]);
+
+ return (
+ <>
+
+ Column Metadata
+
+ Add metadata to this column to help your team and AI understand its
+ purpose. Include detailed descriptions and examples of sample data to
+ make the data more clear and easier to use.
+
+
+
+
+
+ Column Description
+
+
+
+
+
Sample Data
+
+
+ {sampleLoading ? (
+
+ ) : (
+
+ )}
+ Automatically Generate Sample Data
+
+
+
+
+
+
+
+
+ {loading && }
+ Save
+
+
+ >
+ );
+}
+
+interface DataCatalogTableColumnProps {
+ table: DatabaseTableSchema;
+ column: DatabaseTableColumn;
+ driver: DataCatalogDriver;
+ search?: string;
+ hasDefinitionOnly?: boolean;
+}
+
+function DataCatalogTableColumn({
+ column,
+ table,
+ driver,
+ search,
+ hasDefinitionOnly,
+}: DataCatalogTableColumnProps) {
+ const modelColumn = driver.getColumn(
+ table.schemaName,
+ table.tableName!,
+ column.name
+ );
+
+ const definition = modelColumn?.definition;
+ const sampleData = modelColumn?.samples ?? [];
+
+ const [open, setOpen] = useState(false);
+
+ if (hasDefinitionOnly && !definition) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {definition || "No description"}
+
+
+ {sampleData.length > 0 && (
+
+ {sampleData.length} sample data
+
+ )}
+
+
+
+
+
+
+
+ {open && (
+ setOpen(false)}
+ />
+ )}
+
+
+
+
+ );
+}
+
+interface DataCatalogTableAccordionProps {
+ table: DatabaseTableSchema;
+ driver: DataCatalogDriver;
+ search?: string;
+ columnName?: string;
+ hasDefinitionOnly?: boolean;
+}
+
+function DataCatalogTableAccordion({
+ table,
+ driver,
+ search,
+ hasDefinitionOnly,
+}: DataCatalogTableAccordionProps) {
+ const modelTable = driver.getTable(table.schemaName, table.tableName!);
+
+ const [definition, setDefinition] = useState(modelTable?.definition || "");
+
+ const onUpdateTable = useCallback(() => {
+ if (
+ definition &&
+ definition.trim() &&
+ definition !== modelTable?.definition
+ ) {
+ driver.updateTable(table?.schemaName, table.tableName!, {
+ definition,
+ });
+ }
+ }, [driver, table, definition, modelTable]);
+
+ // Check if any of the column match?
+ const matchColumns = useMemo(() => {
+ if (!search || search.toLowerCase() === table.tableName!.toLowerCase()) {
+ return table.columns;
+ }
+ return table.columns.filter((column) =>
+ column.name.toLowerCase().includes(search.toLowerCase())
+ );
+ }, [search, table]);
+
+ const matchedTableName = useMemo(() => {
+ if (search) {
+ return table.tableName!.toLowerCase().includes(search?.toLowerCase());
+ }
+ return true;
+ }, [search, table]);
+
+ // this will work only toggle check box
+ if (hasDefinitionOnly) {
+ const columnsDefinition = table.columns
+ .map((col) => {
+ const modelColumn = driver.getColumn(
+ table.schemaName,
+ table.tableName!,
+ col.name
+ );
+ return !!modelColumn?.definition;
+ })
+ .filter(Boolean);
+
+ if (columnsDefinition.length === 0) {
+ return null;
+ }
+ }
+
+ if (!matchedTableName && matchColumns.length === 0 && search) {
+ return null;
+ }
+
+ return (
+
+
+
{table.tableName}
+
{
+ e.preventDefault();
+ setDefinition(e.currentTarget.value);
+ }}
+ className="h-[30px] w-[150px] p-0 text-[13px] focus-visible:outline-none"
+ />
+
+ {matchColumns.map((column) => {
+ return (
+
+ );
+ })}
+
+ );
+}
+
+export default function DataCatalogModelTab() {
+ const { currentSchemaName, schema } = useSchema();
+ const [search, setSearch] = useState("");
+ const [hasDefinitionOnly, setHasDefinitionOnly] = useState(false);
+ const [selectedSchema, setSelectedSchema] = useState(currentSchemaName);
+
+ const { extensions } = useConfig();
+
+ const dataCatalogExtension =
+ extensions.getExtension("data-catalog");
+
+ const driver = dataCatalogExtension?.driver;
+
+ const currentSchema = useMemo(() => {
+ if (!selectedSchema) return [];
+ const result = (schema[selectedSchema] || [])
+ .filter((table) => table.type === "table")
+ .map((table) => table.tableSchema)
+ .filter(Boolean) as DatabaseTableSchema[];
+
+ result.sort((a, b) => a.tableName!.localeCompare(b.tableName!));
+ return result;
+ }, [schema, selectedSchema]);
+
+ if (!driver) {
+ return Missing driver
;
+ }
+
+ return (
+
+
+
+
+
+ setHasDefinitionOnly(!hasDefinitionOnly)}
+ />
+ Definition only?
+
+
+
+ {
+ setSearch(e.currentTarget.value);
+ }}
+ placeholder="Search tables, columns"
+ />
+
+
+
+
+
+ {currentSchema.map((table) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/extensions/data-catalog/driver-inmemory.ts b/src/extensions/data-catalog/driver-inmemory.ts
new file mode 100644
index 00000000..23d156e6
--- /dev/null
+++ b/src/extensions/data-catalog/driver-inmemory.ts
@@ -0,0 +1,141 @@
+import DataCatalogDriver, {
+ DataCatalogModelColumn,
+ DataCatalogModelColumnInput,
+ DataCatalogModelTable,
+ DataCatalogModelTableInput,
+ DataCatalogSchemas,
+} from "./driver";
+
+interface DataCatalogInmemoryDriverOptions {
+ delay?: number;
+}
+
+export default class DataCatalogInmemoryDriver implements DataCatalogDriver {
+ protected schemas: DataCatalogSchemas;
+ protected options: DataCatalogInmemoryDriverOptions;
+
+ constructor(
+ schemas: DataCatalogSchemas,
+ options: DataCatalogInmemoryDriverOptions
+ ) {
+ this.schemas = schemas;
+ this.options = options;
+ }
+
+ async load(): Promise {
+ if (this.options.delay) {
+ await new Promise((resolve) => setTimeout(resolve, this.options.delay));
+ }
+
+ return this.schemas;
+ }
+
+ async updateColumn(
+ schemaName: string,
+ tableName: string,
+ columnName: string,
+ data: DataCatalogModelColumnInput
+ ): Promise {
+ if (this.options.delay) {
+ await new Promise((resolve) => setTimeout(resolve, this.options.delay));
+ }
+
+ const normalizedSchemaName = schemaName.toLowerCase();
+ const normalizedTableName = tableName.toLowerCase();
+ const normalizedColumnName = columnName.toLowerCase();
+
+ if (!this.schemas[normalizedSchemaName]) {
+ this.schemas[normalizedSchemaName] = {};
+ }
+
+ const schemas = this.schemas[normalizedSchemaName];
+
+ if (!schemas[normalizedTableName]) {
+ schemas[normalizedTableName] = {
+ schemaName: normalizedSchemaName,
+ tableName: normalizedTableName,
+ columns: {},
+ definition: "",
+ };
+ }
+
+ const table = schemas[normalizedTableName];
+ if (!table.columns[normalizedColumnName]) {
+ table.columns[normalizedColumnName] = {
+ name: normalizedColumnName,
+ definition: "",
+ samples: [],
+ hideFromEzql: false,
+ };
+ }
+
+ const column = table.columns[normalizedColumnName];
+ table.columns[normalizedColumnName] = { ...column, ...data };
+ return table.columns[normalizedColumnName];
+ }
+
+ async updateTable(
+ schemaName: string,
+ tableName: string,
+ data: DataCatalogModelTableInput
+ ): Promise {
+ if (this.options.delay) {
+ await new Promise((resolve) => setTimeout(resolve, this.options.delay));
+ }
+
+ const normalizedSchemaName = schemaName.toLowerCase();
+ const normalizedTableName = tableName.toLowerCase();
+
+ if (!this.schemas[normalizedSchemaName]) {
+ this.schemas[normalizedSchemaName] = {};
+ }
+
+ const schemas = this.schemas[normalizedSchemaName];
+
+ if (!schemas[normalizedTableName]) {
+ schemas[normalizedTableName] = {
+ schemaName: normalizedSchemaName,
+ tableName: normalizedTableName,
+ columns: {},
+ definition: "",
+ };
+ }
+
+ const table = schemas[normalizedTableName];
+ schemas[normalizedTableName] = { ...table, ...data };
+
+ return schemas[normalizedTableName];
+ }
+
+ getColumn(
+ schemaName: string,
+ tableName: string,
+ columnName: string
+ ): DataCatalogModelColumn | undefined {
+ const normalizedColumnName = columnName.toLowerCase();
+
+ const table = this.getTable(schemaName, tableName);
+ return table?.columns[normalizedColumnName];
+ }
+
+ getTable(
+ schemaName: string,
+ tableName: string
+ ): DataCatalogModelTable | undefined {
+ const normalizedSchemaName = schemaName.toLowerCase();
+ const normalizedTableName = tableName.toLowerCase();
+
+ if (!this.schemas[normalizedSchemaName]) {
+ return;
+ }
+
+ const schemas = this.schemas[normalizedSchemaName];
+
+ if (!schemas[normalizedTableName]) {
+ return;
+ }
+
+ const table = schemas[normalizedTableName];
+ return table;
+ }
+}
diff --git a/src/extensions/data-catalog/driver.tsx b/src/extensions/data-catalog/driver.tsx
new file mode 100644
index 00000000..e0c1631e
--- /dev/null
+++ b/src/extensions/data-catalog/driver.tsx
@@ -0,0 +1,56 @@
+export interface DataCatalogModelColumnInput {
+ hideFromEzql: boolean;
+ definition: string;
+ samples: string[];
+ virtualJoin?: {
+ tableName: string;
+ columnName: string;
+ };
+}
+
+export interface DataCatalogModelColumn extends DataCatalogModelColumnInput {
+ name: string;
+}
+
+export interface DataCatalogModelTableInput {
+ definition: string;
+}
+
+export interface DataCatalogModelTable extends DataCatalogModelTableInput {
+ schemaName: string;
+ tableName: string;
+ columns: Record;
+}
+
+export type DataCatalogSchemas = Record<
+ string,
+ Record
+>;
+
+export default abstract class DataCatalogDriver {
+ abstract load(): Promise;
+
+ abstract updateColumn(
+ schemaName: string,
+ tableName: string,
+ columnName: string,
+ data: DataCatalogModelColumnInput
+ ): Promise;
+
+ abstract updateTable(
+ schemaName: string,
+ tableName: string,
+ data: DataCatalogModelTableInput
+ ): Promise;
+
+ abstract getColumn(
+ schemaName: string,
+ tableName: string,
+ columnName: string
+ ): DataCatalogModelColumn | undefined;
+
+ abstract getTable(
+ schemaName: string,
+ tableName: string
+ ): DataCatalogModelTable | undefined;
+}
diff --git a/src/extensions/data-catalog/index.tsx b/src/extensions/data-catalog/index.tsx
new file mode 100644
index 00000000..0029a451
--- /dev/null
+++ b/src/extensions/data-catalog/index.tsx
@@ -0,0 +1,54 @@
+import { StudioExtension } from "@/core/extension-base";
+import { StudioExtensionContext } from "@/core/extension-manager";
+import { createTabExtension } from "@/core/extension-tab";
+import { Book } from "@phosphor-icons/react";
+import DataCatalogModelTab from "./data-model-tab";
+import DataCatalogDriver from "./driver";
+import DataCatalogSidebar from "./sidebar";
+import DataCatalogResultHeader from "./table-result-header";
+
+export const dataCatalogModelTab = createTabExtension({
+ key: () => "data-catalog-model",
+ name: "Data Model",
+ generate: () => ({
+ icon: Book,
+ title: "Data Model",
+ component: ,
+ }),
+});
+export default class DataCatalogExtension extends StudioExtension {
+ extensionName = "data-catalog";
+
+ constructor(public readonly driver: DataCatalogDriver) {
+ super();
+ }
+
+ init(studio: StudioExtensionContext): void {
+ this.driver.load().then().catch();
+
+ studio.registerSidebar({
+ key: "data-catalog",
+ name: "Data Catalog",
+ icon: ,
+ content: ,
+ });
+
+ studio.registerQueryHeaderContextMenu((header) => {
+ const from = header.metadata.from;
+ if (!from) return;
+
+ return {
+ key: "data-catalog",
+ title: "",
+ component: (
+
+ ),
+ };
+ });
+ }
+}
diff --git a/src/extensions/data-catalog/sidebar.tsx b/src/extensions/data-catalog/sidebar.tsx
new file mode 100644
index 00000000..f7182144
--- /dev/null
+++ b/src/extensions/data-catalog/sidebar.tsx
@@ -0,0 +1,16 @@
+import { SidebarMenuHeader, SidebarMenuItem } from "@/components/sidebar-menu";
+import { Database } from "@phosphor-icons/react";
+import { dataCatalogModelTab } from ".";
+
+export default function DataCatalogSidebar() {
+ return (
+
+
+ dataCatalogModelTab.open({})}
+ icon={Database}
+ />
+
+ );
+}
diff --git a/src/extensions/data-catalog/table-result-header.tsx b/src/extensions/data-catalog/table-result-header.tsx
new file mode 100644
index 00000000..858c54ba
--- /dev/null
+++ b/src/extensions/data-catalog/table-result-header.tsx
@@ -0,0 +1,59 @@
+import { Textarea } from "@/components/ui/textarea";
+import { LucideLoader } from "lucide-react";
+import { useCallback, useState } from "react";
+import DataCatalogDriver from "./driver";
+
+interface DataCatalogResultHeaderProps {
+ schemaName: string;
+ tableName: string;
+ columnName: string;
+ driver: DataCatalogDriver;
+}
+
+export default function DataCatalogResultHeader({
+ schemaName,
+ tableName,
+ columnName,
+ driver,
+}: DataCatalogResultHeaderProps) {
+ const column = driver.getColumn(schemaName, tableName, columnName);
+ const [definition, setDefinition] = useState(column?.definition ?? "");
+ const [loading, setLoading] = useState(false);
+
+ const onSaveClicked = useCallback(() => {
+ setLoading(true);
+ driver
+ .updateColumn(schemaName, tableName, columnName, {
+ definition,
+ hideFromEzql: column?.hideFromEzql ?? false,
+ samples: column?.samples ?? [],
+ })
+ .then()
+ .catch()
+ .finally(() => setLoading(false));
+ }, [driver, definition, column, schemaName, tableName, columnName]);
+
+ return (
+
+
+
Data Catalog
+
+ {loading && }
+ Save
+
+
+
+
+ );
+}
diff --git a/src/mdx-components.tsx b/src/mdx-components.tsx
index 04a5abc1..b399794b 100644
--- a/src/mdx-components.tsx
+++ b/src/mdx-components.tsx
@@ -1,11 +1,12 @@
import type { MDXComponents } from "mdx/types";
import CodeBlock from "./components/code-block";
-import { Fragment } from "react";
export function useMDXComponents(components: MDXComponents): MDXComponents {
return {
...components,
- code: CodeBlock,
- pre: Fragment,
+ code: (props) => {
+ return ;
+ },
+ pre: CodeBlock,
};
}
diff --git a/src/outerbase-cloud/api-type.ts b/src/outerbase-cloud/api-type.ts
index 784ced60..e94e857f 100644
--- a/src/outerbase-cloud/api-type.ts
+++ b/src/outerbase-cloud/api-type.ts
@@ -1,18 +1,38 @@
export interface OuterbaseDatabaseConfig {
- token: string;
workspaceId: string;
- baseId: string;
sourceId: string;
}
+export class OuterbaseAPIError extends Error {
+ public readonly description: string;
+ public readonly code: string;
+ public readonly title: string;
+
+ constructor(error: OuterbaseAPIErrorResponse) {
+ super(error.description);
+
+ this.description = error.description;
+ this.code = error.code;
+ this.message = error.description;
+ this.title = error.title;
+ }
+}
+
+export interface OuterbaseAPIErrorResponse {
+ code: string;
+ description: string;
+ title: string;
+}
+
export interface OuterbaseAPIResponse {
success: boolean;
response: T;
+ error?: OuterbaseAPIErrorResponse;
}
-export type OuterbaseAPIQueryRawResponse = OuterbaseAPIResponse<{
+export interface OuterbaseAPIQueryRaw {
items: Record[];
-}>;
+}
export interface OuterbaseAPIAnalyticEvent {
created_at: string;
@@ -40,6 +60,90 @@ export interface OuterbaseAPIWorkspace {
bases: OuterbaseAPIBase[];
}
+export interface OuterbaseAPIDashboard {
+ base_id: string | null;
+ chart_ids: string[];
+ created_at: string;
+ id: string;
+ model: "dashboard";
+ type: "dashboard";
+ name: string;
+ workspace_id: string;
+ layout: {
+ h: number;
+ i: string;
+ w: number;
+ x: number;
+ y: number;
+ max_h: number;
+ max_w: number;
+ }[];
+}
+
+export interface OuterbaseAPIQuery {
+ base_id: string;
+ id: string;
+ name: string;
+ query: string;
+ source_id: string;
+}
+export interface OuterbaseAPIDashboardChart {
+ connection_id: string | null;
+ created_at: string;
+ id: string;
+ model: "chart";
+ name: string;
+ params: {
+ id: string;
+ name: string;
+ type: string;
+ model: string;
+ apiKey: string;
+ layers: {
+ sql: string;
+ type: string;
+ }[];
+ options: {
+ xAxisKey: string;
+ };
+ source_id: string;
+ created_at: string;
+ updated_at: string;
+ workspace_id: string;
+ connection_id: string | null;
+ };
+ result?: OuterbaseAPIQueryRaw;
+ source_id: string;
+ type: string;
+ updated_at: string;
+ workspace_id: string;
+}
+
+export interface OuterbaseAPISession {
+ created_at: string;
+ user_id: string;
+ phone_verified_at: string | null;
+ password_verified_at: string | null;
+ otp_verified_at: string | null;
+ oauth_verified_at: string | null;
+ expires_at: string | null;
+ token: string;
+}
+
+export interface OuterbaseAPIUser {
+ avatar: string | null;
+ google_user_id: string;
+ initials: string;
+ id: string;
+ email: string;
+ last_name: string;
+ first_name: string;
+}
+
+export interface OuterbaseAPIDashboardDetail extends OuterbaseAPIDashboard {
+ charts: OuterbaseAPIDashboardChart[];
+}
+
export interface OuterbaseAPIWorkspaceResponse {
items: OuterbaseAPIWorkspace[];
}
@@ -47,3 +151,11 @@ export interface OuterbaseAPIWorkspaceResponse {
export interface OuterbaseAPIBaseResponse {
items: OuterbaseAPIBase[];
}
+
+export interface OuterbaseAPIDashboardListResponse {
+ items: OuterbaseAPIDashboard[];
+}
+
+export interface OuterbaseAPIQueryListResponse {
+ items: OuterbaseAPIQuery[];
+}
diff --git a/src/outerbase-cloud/api.ts b/src/outerbase-cloud/api.ts
index 3689c3e1..85b817a2 100644
--- a/src/outerbase-cloud/api.ts
+++ b/src/outerbase-cloud/api.ts
@@ -1,12 +1,21 @@
import {
OuterbaseAPIBaseResponse,
+ OuterbaseAPIDashboardChart,
+ OuterbaseAPIDashboardDetail,
+ OuterbaseAPIDashboardListResponse,
+ OuterbaseAPIError,
+ OuterbaseAPIQuery,
+ OuterbaseAPIQueryListResponse,
+ OuterbaseAPIQueryRaw,
OuterbaseAPIResponse,
+ OuterbaseAPISession,
+ OuterbaseAPIUser,
OuterbaseAPIWorkspaceResponse,
} from "./api-type";
export async function requestOuterbase(
url: string,
- method: "GET" | "POST" | "DELETE" = "GET",
+ method: "GET" | "POST" | "DELETE" | "PUT" = "GET",
body?: unknown
) {
const raw = await fetch(url, {
@@ -19,6 +28,11 @@ export async function requestOuterbase(
});
const json = (await raw.json()) as OuterbaseAPIResponse;
+
+ if (json.error) {
+ throw new OuterbaseAPIError(json.error);
+ }
+
return json.response;
}
@@ -36,3 +50,108 @@ export async function getOuterbaseBase(workspaceId: string, baseId: string) {
return baseList.items[0];
}
+
+export async function getOuterbaseDashboardList(workspaceId: string) {
+ return requestOuterbase(
+ `/api/v1/workspace/${workspaceId}/dashboard`
+ );
+}
+
+export async function getOuterbaseDashboard(
+ workspaceId: string,
+ dashboardId: string
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${workspaceId}/dashboard/${dashboardId}`
+ );
+}
+
+export async function runOuterbaseQueryRaw(
+ workspaceId: string,
+ sourceId: string,
+ query: string
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${workspaceId}/source/${sourceId}/query/raw`,
+ "POST",
+ { query }
+ );
+}
+
+export async function getOuterbaseQueryList(
+ workspaceId: string,
+ baseId: string
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${workspaceId}/query?${new URLSearchParams({ baseId })}`
+ );
+}
+
+export async function createOuterbaseQuery(
+ workspaceId: string,
+ baseId: string,
+ options: { source_id: string; name: string; baseId: string; query: string }
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${workspaceId}/query?${new URLSearchParams({ baseId })}`,
+ "POST",
+ options
+ );
+}
+
+export async function deleteOuterbaseQuery(
+ worksaceId: string,
+ queryId: string
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${worksaceId}/query/${queryId}`,
+ "DELETE"
+ );
+}
+
+export async function updateOuterbaseQuery(
+ worksaceId: string,
+ queryId: string,
+ options: { name: string; query: string }
+) {
+ return requestOuterbase(
+ `/api/v1/workspace/${worksaceId}/query/${queryId}`,
+ "PUT",
+ options
+ );
+}
+
+export async function getOuterbaseSession() {
+ return requestOuterbase<{
+ session: OuterbaseAPISession;
+ user: OuterbaseAPIUser;
+ }>("/api/v1/auth/session");
+}
+
+export async function loginOuterbaseByPassword(
+ email: string,
+ password: string
+) {
+ return requestOuterbase("/api/v1/auth/login", "POST", {
+ email,
+ password,
+ });
+}
+
+export async function getOuterbaseEmbedChart(
+ chartId: string,
+ apiKey: string
+): Promise> {
+ const result = await fetch(
+ `${process.env.NEXT_PUBLIC_OB_API}/chart/${chartId}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "x-chart-api-key": apiKey,
+ },
+ }
+ );
+
+ return await result.json();
+}
diff --git a/src/outerbase-cloud/database-source/index.tsx b/src/outerbase-cloud/database-source/index.tsx
new file mode 100644
index 00000000..353877c7
--- /dev/null
+++ b/src/outerbase-cloud/database-source/index.tsx
@@ -0,0 +1,64 @@
+import { OuterbaseAPIWorkspace } from "@/outerbase-cloud/api-type";
+import { createOuterbaseDatabaseDriver } from "@/outerbase-cloud/database/utils";
+import { BaseDriver } from "../../drivers/base-driver";
+import {
+ BoardSource,
+ BoardSourceDriver,
+} from "../../drivers/board-source/base-source";
+
+export default class OuterbaseBoardSourceDriver implements BoardSourceDriver {
+ protected workspace: OuterbaseAPIWorkspace;
+ protected sourceDrivers: Record = {};
+
+ constructor(workspace: OuterbaseAPIWorkspace) {
+ this.workspace = workspace;
+ }
+
+ sourceList(): BoardSource[] {
+ return this.workspace.bases
+ .filter((base) => base.sources && base.sources.length > 0)
+ .map((base) => {
+ return {
+ id: base.sources[0].id!,
+ name: base.name,
+ type: base.sources[0].type!,
+ };
+ });
+ }
+
+ async query(
+ sourceId: string,
+ statement: string
+ ): Promise[]> {
+ const source = this.workspace.bases.find((base) => {
+ return (
+ base.sources &&
+ base.sources.length > 0 &&
+ base.sources[0].id === sourceId
+ );
+ })?.sources[0];
+
+ if (!source) {
+ throw new Error("Source does not exist");
+ }
+
+ if (!this.sourceDrivers[sourceId]) {
+ this.sourceDrivers[sourceId] = createOuterbaseDatabaseDriver(
+ source.type,
+ {
+ workspaceId: this.workspace.id!,
+ sourceId,
+ }
+ );
+ }
+
+ const driver = this.sourceDrivers[sourceId]!;
+ const result = await driver.query(statement);
+
+ return result.rows;
+ }
+
+ cleanup(): void {
+ // do nothing
+ }
+}
diff --git a/src/outerbase-cloud/database/mysql.ts b/src/outerbase-cloud/database/mysql.ts
index 9e5ef018..5e38361f 100644
--- a/src/outerbase-cloud/database/mysql.ts
+++ b/src/outerbase-cloud/database/mysql.ts
@@ -1,9 +1,7 @@
import { DatabaseHeader, DatabaseResultSet } from "@/drivers/base-driver";
-import {
- OuterbaseAPIQueryRawResponse,
- OuterbaseDatabaseConfig,
-} from "../api-type";
import MySQLLikeDriver from "@/drivers/mysql/mysql-driver";
+import { runOuterbaseQueryRaw } from "../api";
+import { OuterbaseDatabaseConfig } from "../api-type";
function transformObjectBasedResult(arr: Record[]) {
const usedColumnName = new Set();
@@ -31,41 +29,24 @@ function transformObjectBasedResult(arr: Record[]) {
}
export class OuterbaseMySQLDriver extends MySQLLikeDriver {
- protected token: string;
protected workspaceId: string;
protected sourceId: string;
- constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
+ constructor({ workspaceId, sourceId }: OuterbaseDatabaseConfig) {
super();
this.workspaceId = workspaceId;
this.sourceId = sourceId;
- this.token = token;
}
async query(stmt: string): Promise {
- const response = await fetch(
- `/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
- {
- method: "POST",
- headers: {
- "x-auth-token": this.token,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: stmt,
- }),
- }
+ const jsonResponse = await runOuterbaseQueryRaw(
+ this.workspaceId,
+ this.sourceId,
+ stmt
);
- const jsonResponse =
- (await response.json()) as OuterbaseAPIQueryRawResponse;
-
- if (!jsonResponse.success) {
- throw new Error("Query failed");
- }
-
- const result = transformObjectBasedResult(jsonResponse.response.items);
+ const result = transformObjectBasedResult(jsonResponse.items);
return {
rows: result.data,
diff --git a/src/outerbase-cloud/database/postgresql.ts b/src/outerbase-cloud/database/postgresql.ts
index 432949ae..685ae351 100644
--- a/src/outerbase-cloud/database/postgresql.ts
+++ b/src/outerbase-cloud/database/postgresql.ts
@@ -1,13 +1,11 @@
import {
DatabaseHeader,
- DriverFlags,
DatabaseResultSet,
+ DriverFlags,
} from "@/drivers/base-driver";
-import {
- OuterbaseAPIQueryRawResponse,
- OuterbaseDatabaseConfig,
-} from "../api-type";
import PostgresLikeDriver from "@/drivers/postgres/postgres-driver";
+import { runOuterbaseQueryRaw } from "../api";
+import { OuterbaseDatabaseConfig } from "../api-type";
function transformObjectBasedResult(arr: Record[]) {
const usedColumnName = new Set();
@@ -37,7 +35,6 @@ function transformObjectBasedResult(arr: Record[]) {
export class OuterbasePostgresDriver extends PostgresLikeDriver {
supportPragmaList = false;
- protected token: string;
protected workspaceId: string;
protected sourceId: string;
@@ -48,37 +45,21 @@ export class OuterbasePostgresDriver extends PostgresLikeDriver {
};
}
- constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
+ constructor({ workspaceId, sourceId }: OuterbaseDatabaseConfig) {
super();
this.workspaceId = workspaceId;
this.sourceId = sourceId;
- this.token = token;
}
async query(stmt: string): Promise {
- const response = await fetch(
- `/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
- {
- method: "POST",
- headers: {
- "x-auth-token": this.token,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: stmt,
- }),
- }
+ const jsonResponse = await runOuterbaseQueryRaw(
+ this.workspaceId,
+ this.sourceId,
+ stmt
);
- const jsonResponse =
- (await response.json()) as OuterbaseAPIQueryRawResponse;
-
- if (!jsonResponse.success) {
- throw new Error("Query failed");
- }
-
- const result = transformObjectBasedResult(jsonResponse.response.items);
+ const result = transformObjectBasedResult(jsonResponse.items);
return {
rows: result.data,
diff --git a/src/outerbase-cloud/database/sqlite.ts b/src/outerbase-cloud/database/sqlite.ts
index 232c1443..343be436 100644
--- a/src/outerbase-cloud/database/sqlite.ts
+++ b/src/outerbase-cloud/database/sqlite.ts
@@ -1,13 +1,11 @@
import {
DatabaseHeader,
- DriverFlags,
DatabaseResultSet,
+ DriverFlags,
} from "@/drivers/base-driver";
import { SqliteLikeBaseDriver } from "@/drivers/sqlite-base-driver";
-import {
- OuterbaseAPIQueryRawResponse,
- OuterbaseDatabaseConfig,
-} from "../api-type";
+import { runOuterbaseQueryRaw } from "../api";
+import { OuterbaseDatabaseConfig } from "../api-type";
function transformObjectBasedResult(arr: Record[]) {
const usedColumnName = new Set();
@@ -37,7 +35,6 @@ function transformObjectBasedResult(arr: Record[]) {
export class OuterbaseSqliteDriver extends SqliteLikeBaseDriver {
supportPragmaList = false;
- protected token: string;
protected workspaceId: string;
protected sourceId: string;
@@ -48,37 +45,21 @@ export class OuterbaseSqliteDriver extends SqliteLikeBaseDriver {
};
}
- constructor({ workspaceId, sourceId, token }: OuterbaseDatabaseConfig) {
+ constructor({ workspaceId, sourceId }: OuterbaseDatabaseConfig) {
super();
this.workspaceId = workspaceId;
this.sourceId = sourceId;
- this.token = token;
}
async query(stmt: string): Promise {
- const response = await fetch(
- `/api/v1/workspace/${this.workspaceId}/source/${this.sourceId}/query/raw`,
- {
- method: "POST",
- headers: {
- "x-auth-token": this.token,
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- query: stmt,
- }),
- }
+ const jsonResponse = await runOuterbaseQueryRaw(
+ this.workspaceId,
+ this.sourceId,
+ stmt
);
- const jsonResponse =
- (await response.json()) as OuterbaseAPIQueryRawResponse;
-
- if (!jsonResponse.success) {
- throw new Error("Query failed");
- }
-
- const result = transformObjectBasedResult(jsonResponse.response.items);
+ const result = transformObjectBasedResult(jsonResponse.items);
return {
rows: result.data,
diff --git a/src/outerbase-cloud/database/utils.ts b/src/outerbase-cloud/database/utils.ts
new file mode 100644
index 00000000..9a83840a
--- /dev/null
+++ b/src/outerbase-cloud/database/utils.ts
@@ -0,0 +1,14 @@
+import { OuterbaseDatabaseConfig } from "../api-type";
+import { OuterbaseMySQLDriver } from "./mysql";
+import { OuterbasePostgresDriver } from "./postgresql";
+import { OuterbaseSqliteDriver } from "./sqlite";
+
+export function createOuterbaseDatabaseDriver(type: string, config: OuterbaseDatabaseConfig) {
+ if (type === "postgres") {
+ return new OuterbasePostgresDriver(config)
+ } else if (type === 'mysql') {
+ return new OuterbaseMySQLDriver(config);
+ }
+
+ return new OuterbaseSqliteDriver(config);
+}
\ No newline at end of file
diff --git a/src/outerbase-cloud/query-driver.ts b/src/outerbase-cloud/query-driver.ts
new file mode 100644
index 00000000..6c4a1c8e
--- /dev/null
+++ b/src/outerbase-cloud/query-driver.ts
@@ -0,0 +1,178 @@
+import {
+ SavedDocData,
+ SavedDocDriver,
+ SavedDocGroupByNamespace,
+ SavedDocInput,
+ SavedDocNamespace,
+ SavedDocType,
+} from "@/drivers/saved-doc/saved-doc-driver";
+import {
+ createOuterbaseQuery,
+ deleteOuterbaseQuery,
+ getOuterbaseQueryList,
+ updateOuterbaseQuery,
+} from "./api";
+
+export default class OuterbaseQueryDriver implements SavedDocDriver {
+ protected cb: (() => void)[] = [];
+ protected cacheNamespaceList: SavedDocNamespace[] | null = null;
+ protected cacheDocs: Record = {};
+
+ constructor(
+ protected workspaceId: string,
+ protected baseId: string,
+ protected sourceId: string
+ ) {}
+
+ async getNamespaces(): Promise {
+ if (this.cacheNamespaceList) {
+ return this.cacheNamespaceList;
+ }
+
+ const queries = await getOuterbaseQueryList(this.workspaceId, this.baseId);
+
+ this.cacheNamespaceList = [
+ {
+ id: "default",
+ name: "Workspace",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ },
+ ];
+
+ this.cacheDocs = {
+ default: queries.items.map((q) => ({
+ id: q.id,
+ namespace: {
+ id: "default",
+ name: "Workspace",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ },
+ name: q.name,
+ content: q.query,
+ type: "sql",
+ data: q,
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ })),
+ };
+
+ return this.cacheNamespaceList;
+ }
+
+ async createNamespace(): Promise {
+ throw new Error("Not implemented");
+ }
+
+ async updateNamespace(): Promise {
+ throw new Error("Not implemented");
+ }
+
+ async removeNamespace(): Promise {
+ throw new Error("Not implemented");
+ }
+
+ async createDoc(
+ _: SavedDocType,
+ __: string,
+ data: SavedDocInput
+ ): Promise {
+ await this.getNamespaces();
+
+ const r = await createOuterbaseQuery(this.workspaceId, this.baseId, {
+ baseId: this.baseId,
+ name: data.name,
+ query: data.content,
+ source_id: this.sourceId,
+ });
+
+ const doc: SavedDocData = {
+ id: r.id,
+ namespace: {
+ id: "default",
+ name: "Workspace",
+ },
+ name: data.name,
+ content: data.content,
+ type: "sql",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ };
+
+ if (this.cacheDocs["default"]) {
+ this.cacheDocs["default"].unshift(doc);
+ }
+
+ this.triggerChange();
+ return doc;
+ }
+
+ async getDocs(): Promise {
+ const ns = await this.getNamespaces();
+
+ return ns.map((n) => {
+ return {
+ namespace: n,
+ docs: this.cacheDocs[n.id] ?? [],
+ };
+ });
+ }
+
+ async updateDoc(id: string, data: SavedDocInput): Promise {
+ await this.getNamespaces();
+
+ const r = await updateOuterbaseQuery(this.workspaceId, id, {
+ name: data.name,
+ query: data.content,
+ });
+
+ const doc: SavedDocData = {
+ id: r.id,
+ namespace: {
+ id: "default",
+ name: "Workspace",
+ },
+ name: r.name,
+ content: r.query,
+ type: "sql",
+ createdAt: Date.now(),
+ updatedAt: Date.now(),
+ };
+
+ if (this.cacheDocs["default"]) {
+ this.cacheDocs["default"] = this.cacheDocs["default"].map((d) => {
+ if (d.id === r.id) return doc;
+ return d;
+ });
+ }
+
+ this.triggerChange();
+ return doc;
+ }
+
+ async removeDoc(id: string): Promise {
+ await this.getNamespaces();
+ await deleteOuterbaseQuery(this.workspaceId, id);
+
+ for (const namespaceId of Object.keys(this.cacheDocs)) {
+ this.cacheDocs[namespaceId] = this.cacheDocs[namespaceId].filter(
+ (d) => d.id !== id
+ );
+ }
+
+ this.triggerChange();
+ }
+
+ addChangeListener(cb: () => void): void {
+ this.cb.push(cb);
+ }
+
+ removeChangeListener(cb: () => void): void {
+ this.cb = this.cb.filter((c) => c !== cb);
+ }
+
+ protected triggerChange() {
+ this.cb.forEach((c) => c());
+ }
+}