From ff0f23b08f2740d2d3ac9e8c0ee3e7c6ea6ba125 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Sun, 26 Jan 2025 17:58:53 +0700 Subject: [PATCH 01/21] refactor embed page to single page (#268) * refactor embed page to single page * starbase does not support pragma list --- .../(theme)/embed/[driver]/page-client.tsx | 90 +++++++++++++++++++ src/app/(theme)/embed/[driver]/page.tsx | 48 ++++++++++ src/app/(theme)/embed/dolt/page-client.tsx | 41 --------- src/app/(theme)/embed/dolt/page.tsx | 4 - src/app/(theme)/embed/embed-page.tsx | 44 --------- src/app/(theme)/embed/mysql/page-client.tsx | 37 -------- src/app/(theme)/embed/mysql/page.tsx | 4 - .../(theme)/embed/postgres/page-client.tsx | 37 -------- src/app/(theme)/embed/postgres/page.tsx | 4 - src/app/(theme)/embed/sqlite/page-client.tsx | 37 -------- src/app/(theme)/embed/sqlite/page.tsx | 4 - .../(theme)/embed/starbase/page-client.tsx | 40 --------- src/app/(theme)/embed/starbase/page.tsx | 4 - src/app/(theme)/embed/turso/page-client.tsx | 45 ---------- src/app/(theme)/embed/turso/page.tsx | 4 - 15 files changed, 138 insertions(+), 305 deletions(-) create mode 100644 src/app/(theme)/embed/[driver]/page-client.tsx create mode 100644 src/app/(theme)/embed/[driver]/page.tsx delete mode 100644 src/app/(theme)/embed/dolt/page-client.tsx delete mode 100644 src/app/(theme)/embed/dolt/page.tsx delete mode 100644 src/app/(theme)/embed/embed-page.tsx delete mode 100644 src/app/(theme)/embed/mysql/page-client.tsx delete mode 100644 src/app/(theme)/embed/mysql/page.tsx delete mode 100644 src/app/(theme)/embed/postgres/page-client.tsx delete mode 100644 src/app/(theme)/embed/postgres/page.tsx delete mode 100644 src/app/(theme)/embed/sqlite/page-client.tsx delete mode 100644 src/app/(theme)/embed/sqlite/page.tsx delete mode 100644 src/app/(theme)/embed/starbase/page-client.tsx delete mode 100644 src/app/(theme)/embed/starbase/page.tsx delete mode 100644 src/app/(theme)/embed/turso/page-client.tsx delete mode 100644 src/app/(theme)/embed/turso/page.tsx 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..ff9d078e --- /dev/null +++ b/src/app/(theme)/embed/[driver]/page.tsx @@ -0,0 +1,48 @@ +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; + 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 ( + + + + + + ); +} 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(() => ); From d1ec52ec64e05fb6843d0b0636c45ba79d8f2310 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 27 Jan 2025 12:07:35 +0700 Subject: [PATCH 02/21] add outerbase workspace api --- .../board/[boardId]/page-client.tsx | 15 ++++ .../w/[workspaceId]/board/[boardId]/page.tsx | 22 ++++++ .../(theme)/w/[workspaceId]/page-client.tsx | 72 +++++++++++++++++++ src/app/(theme)/w/[workspaceId]/page.tsx | 19 +++++ src/outerbase-cloud/api-type.ts | 63 +++++++++++++++- src/outerbase-cloud/api.ts | 30 ++++++++ src/outerbase-cloud/database/mysql.ts | 31 ++------ src/outerbase-cloud/database/postgresql.ts | 33 +++------ src/outerbase-cloud/database/sqlite.ts | 33 +++------ 9 files changed, 242 insertions(+), 76 deletions(-) create mode 100644 src/app/(theme)/w/[workspaceId]/board/[boardId]/page-client.tsx create mode 100644 src/app/(theme)/w/[workspaceId]/board/[boardId]/page.tsx create mode 100644 src/app/(theme)/w/[workspaceId]/page-client.tsx create mode 100644 src/app/(theme)/w/[workspaceId]/page.tsx diff --git a/src/app/(theme)/w/[workspaceId]/board/[boardId]/page-client.tsx b/src/app/(theme)/w/[workspaceId]/board/[boardId]/page-client.tsx new file mode 100644 index 00000000..78b65fde --- /dev/null +++ b/src/app/(theme)/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/(theme)/w/[workspaceId]/board/[boardId]/page.tsx b/src/app/(theme)/w/[workspaceId]/board/[boardId]/page.tsx new file mode 100644 index 00000000..8bf28879 --- /dev/null +++ b/src/app/(theme)/w/[workspaceId]/board/[boardId]/page.tsx @@ -0,0 +1,22 @@ +import ClientOnly from "@/components/client-only"; +import ThemeLayout from "../../../../theme_layout"; +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/(theme)/w/[workspaceId]/page-client.tsx b/src/app/(theme)/w/[workspaceId]/page-client.tsx new file mode 100644 index 00000000..70a68c8d --- /dev/null +++ b/src/app/(theme)/w/[workspaceId]/page-client.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { + getOuterbaseDashboardList, + getOuterbaseWorkspace, +} from "@/outerbase-cloud/api"; +import { + OuterbaseAPIBase, + OuterbaseAPIDashboard, +} from "@/outerbase-cloud/api-type"; +import Link from "next/link"; +import { useEffect, useState } from "react"; + +export default function WorkspaceListPageClient({ + workspaceId, +}: { + workspaceId: string; +}) { + const [loading, setLoading] = useState(true); + const [bases, setBases] = useState([]); + const [boards, setBoards] = useState([]); + + useEffect(() => { + Promise.all([ + getOuterbaseWorkspace(), + getOuterbaseDashboardList(workspaceId), + ]) + .then(([workspace, boards]) => { + setBases( + workspace.items.find((w) => w.short_name === workspaceId)?.bases ?? [] + ); + setBoards((boards.items ?? []).filter((b) => b.base_id === null)); + }) + .finally(() => { + setLoading(false); + }); + }, [workspaceId]); + + if (loading) { + return
Loading...
; + } + + return ( +
+

Board

+
+ {boards.map((board) => ( + + {board.name} + + ))} +
+ +

Base

+
+ {bases.map((base) => ( + + {base.name} + + ))} +
+
+ ); +} diff --git a/src/app/(theme)/w/[workspaceId]/page.tsx b/src/app/(theme)/w/[workspaceId]/page.tsx new file mode 100644 index 00000000..3b744f95 --- /dev/null +++ b/src/app/(theme)/w/[workspaceId]/page.tsx @@ -0,0 +1,19 @@ +import ClientOnly from "@/components/client-only"; +import ThemeLayout from "../../theme_layout"; +import WorkspaceListPageClient from "./page-client"; + +interface WorkspaceListPageProps { + params: Promise<{ workspaceId: string }>; +} + +export default async function WorkspaceListPage(props: WorkspaceListPageProps) { + const params = await props.params; + + return ( + + + + + + ); +} diff --git a/src/outerbase-cloud/api-type.ts b/src/outerbase-cloud/api-type.ts index 784ced60..de25aebd 100644 --- a/src/outerbase-cloud/api-type.ts +++ b/src/outerbase-cloud/api-type.ts @@ -10,9 +10,9 @@ export interface OuterbaseAPIResponse { response: T; } -export type OuterbaseAPIQueryRawResponse = OuterbaseAPIResponse<{ +export interface OuterbaseAPIQueryRaw { items: Record[]; -}>; +} export interface OuterbaseAPIAnalyticEvent { created_at: string; @@ -40,6 +40,61 @@ 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 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; + }; + source_id: string; + type: string; + updated_at: string; + workspace_id: string; +} + +export interface OuterbaseAPIDashboardDetail extends OuterbaseAPIDashboard { + charts: OuterbaseAPIDashboardChart[]; +} + export interface OuterbaseAPIWorkspaceResponse { items: OuterbaseAPIWorkspace[]; } @@ -47,3 +102,7 @@ export interface OuterbaseAPIWorkspaceResponse { export interface OuterbaseAPIBaseResponse { items: OuterbaseAPIBase[]; } + +export interface OuterbaseAPIDashboardListResponse { + items: OuterbaseAPIDashboard[]; +} diff --git a/src/outerbase-cloud/api.ts b/src/outerbase-cloud/api.ts index 3689c3e1..f6e9bb33 100644 --- a/src/outerbase-cloud/api.ts +++ b/src/outerbase-cloud/api.ts @@ -1,5 +1,8 @@ import { OuterbaseAPIBaseResponse, + OuterbaseAPIDashboardDetail, + OuterbaseAPIDashboardListResponse, + OuterbaseAPIQueryRaw, OuterbaseAPIResponse, OuterbaseAPIWorkspaceResponse, } from "./api-type"; @@ -36,3 +39,30 @@ 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 } + ); +} diff --git a/src/outerbase-cloud/database/mysql.ts b/src/outerbase-cloud/database/mysql.ts index 9e5ef018..36006a91 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(); @@ -44,28 +42,13 @@ export class OuterbaseMySQLDriver extends MySQLLikeDriver { } 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..837a0025 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(); @@ -57,28 +55,13 @@ export class OuterbasePostgresDriver extends PostgresLikeDriver { } 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..ea06d4ef 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(); @@ -57,28 +55,13 @@ export class OuterbaseSqliteDriver extends SqliteLikeBaseDriver { } 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, From aa8a76d1277319d7a686530bae6418e1f4898776 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Mon, 27 Jan 2025 19:12:40 +0700 Subject: [PATCH 03/21] add outerbase query integration --- .../w/[workspaceId]/[baseId]/page-client.tsx | 15 +- src/outerbase-cloud/api-type.ts | 11 ++ src/outerbase-cloud/api.ts | 47 ++++- src/outerbase-cloud/query-driver.ts | 178 ++++++++++++++++++ 4 files changed, 248 insertions(+), 3 deletions(-) create mode 100644 src/outerbase-cloud/query-driver.ts diff --git a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx b/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx index 2b2ffeab..a6d93e7b 100644 --- a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx +++ b/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx @@ -7,6 +7,7 @@ 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,6 +29,11 @@ export default function OuterbaseSourcePageClient({ }); }, [workspaceId, baseId]); + const savedDocDriver = useMemo(() => { + if (!workspaceId || !source?.id || !baseId) return null; + return new OuterbaseQueryDriver(workspaceId, baseId, source.id); + }, [workspaceId, baseId, source?.id]); + const outerbaseDriver = useMemo(() => { if (!workspaceId || !source) return null; @@ -48,11 +54,16 @@ export default function OuterbaseSourcePageClient({ return new OuterbaseSqliteDriver(outerbaseConfig); }, [workspaceId, source]); - if (!outerbaseDriver) { + if (!outerbaseDriver || !savedDocDriver) { return ; } return ( - + ); } diff --git a/src/outerbase-cloud/api-type.ts b/src/outerbase-cloud/api-type.ts index de25aebd..0655cfe6 100644 --- a/src/outerbase-cloud/api-type.ts +++ b/src/outerbase-cloud/api-type.ts @@ -60,6 +60,13 @@ export interface OuterbaseAPIDashboard { }[]; } +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; @@ -106,3 +113,7 @@ export interface OuterbaseAPIBaseResponse { 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 f6e9bb33..9a3ad62b 100644 --- a/src/outerbase-cloud/api.ts +++ b/src/outerbase-cloud/api.ts @@ -2,6 +2,8 @@ import { OuterbaseAPIBaseResponse, OuterbaseAPIDashboardDetail, OuterbaseAPIDashboardListResponse, + OuterbaseAPIQuery, + OuterbaseAPIQueryListResponse, OuterbaseAPIQueryRaw, OuterbaseAPIResponse, OuterbaseAPIWorkspaceResponse, @@ -9,7 +11,7 @@ import { export async function requestOuterbase( url: string, - method: "GET" | "POST" | "DELETE" = "GET", + method: "GET" | "POST" | "DELETE" | "PUT" = "GET", body?: unknown ) { const raw = await fetch(url, { @@ -66,3 +68,46 @@ export async function runOuterbaseQueryRaw( { 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 + ); +} 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()); + } +} From 35c5b1db23c22e7141cbbd3f8f967d7f7f4f3c76 Mon Sep 17 00:00:00 2001 From: keppere <50957820+keppere@users.noreply.github.com> Date: Wed, 29 Jan 2025 09:44:43 +0700 Subject: [PATCH 04/21] Handle deselect removed rows (#283) * fix: deselect removed rows #277 * fix: prevent to paste clipboard with extra enter from excel --- src/components/gui/query-result-table.tsx | 6 ++++++ .../gui/table-optimized/OptimizeTableState.tsx | 10 ++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) 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/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 = {}; From 1a6a95eb1f18e5d0fa913058b2f8c1756f56e1e4 Mon Sep 17 00:00:00 2001 From: "Visal .In" Date: Wed, 29 Jan 2025 10:17:26 +0700 Subject: [PATCH 05/21] data catalog extension prototype (#285) * add data catalog extension * add data model tab and drivers * add data catalog first prototype * add extension for outerbase cloud * remove the data catalog extension --- .../w/[workspaceId]/[baseId]/page-client.tsx | 26 +- src/components/gui/studio.tsx | 9 +- src/core/extension-manager.tsx | 14 +- src/core/standard-extension.tsx | 4 +- .../data-catalog/data-model-tab.tsx | 276 ++++++++++++++++++ .../data-catalog/driver-inmemory.ts | 123 ++++++++ src/extensions/data-catalog/driver.tsx | 51 ++++ src/extensions/data-catalog/index.tsx | 54 ++++ src/extensions/data-catalog/sidebar.tsx | 13 + .../data-catalog/table-result-header.tsx | 59 ++++ 10 files changed, 612 insertions(+), 17 deletions(-) create mode 100644 src/extensions/data-catalog/data-model-tab.tsx create mode 100644 src/extensions/data-catalog/driver-inmemory.ts create mode 100644 src/extensions/data-catalog/driver.tsx create mode 100644 src/extensions/data-catalog/index.tsx create mode 100644 src/extensions/data-catalog/sidebar.tsx create mode 100644 src/extensions/data-catalog/table-result-header.tsx diff --git a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx b/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx index a6d93e7b..f1d4d8b8 100644 --- a/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx +++ b/src/app/(theme)/w/[workspaceId]/[baseId]/page-client.tsx @@ -2,6 +2,12 @@ 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"; @@ -34,8 +40,8 @@ export default function OuterbaseSourcePageClient({ return new OuterbaseQueryDriver(workspaceId, baseId, source.id); }, [workspaceId, baseId, source?.id]); - const outerbaseDriver = useMemo(() => { - if (!workspaceId || !source) return null; + const [outerbaseDriver, extensions] = useMemo(() => { + if (!workspaceId || !source) return [null, null]; const dialect = source.type; const outerbaseConfig = { @@ -46,12 +52,21 @@ 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 || !savedDocDriver) { @@ -63,6 +78,7 @@ export default function OuterbaseSourcePageClient({ color="gray" driver={outerbaseDriver} docDriver={savedDocDriver} + extensions={extensions} name="Storybook Testing" /> ); 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/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/extensions/data-catalog/data-model-tab.tsx b/src/extensions/data-catalog/data-model-tab.tsx new file mode 100644 index 00000000..79bd05df --- /dev/null +++ b/src/extensions/data-catalog/data-model-tab.tsx @@ -0,0 +1,276 @@ +import SchemaNameSelect from "@/components/gui/schema-editor/schema-name-select"; +import { Toolbar } from "@/components/gui/toolbar"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +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]); + + 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. + + + +
+
+ +