diff --git a/public/extension/chart-dark.png b/public/extension/chart-dark.png new file mode 100644 index 00000000..4dfe8a08 Binary files /dev/null and b/public/extension/chart-dark.png differ diff --git a/public/extension/chart-light.png b/public/extension/chart-light.png new file mode 100644 index 00000000..b9a368b7 Binary files /dev/null and b/public/extension/chart-light.png differ diff --git a/public/extension/definition-dark.png b/public/extension/definition-dark.png new file mode 100644 index 00000000..642e4468 Binary files /dev/null and b/public/extension/definition-dark.png differ diff --git a/public/extension/definition-light.png b/public/extension/definition-light.png new file mode 100644 index 00000000..65be8505 Binary files /dev/null and b/public/extension/definition-light.png differ diff --git a/public/extension/term-dark.png b/public/extension/term-dark.png new file mode 100644 index 00000000..3a1c3739 Binary files /dev/null and b/public/extension/term-dark.png differ diff --git a/public/extension/term-light.png b/public/extension/term-light.png new file mode 100644 index 00000000..72106aad Binary files /dev/null and b/public/extension/term-light.png differ diff --git a/src/app/(outerbase)/navigation.tsx b/src/app/(outerbase)/navigation.tsx index f9d2cc71..bc69f167 100644 --- a/src/app/(outerbase)/navigation.tsx +++ b/src/app/(outerbase)/navigation.tsx @@ -1,3 +1,4 @@ +import { OuterbaseIcon } from "@/components/icons/outerbase"; import { getDatabaseIcon } from "@/components/resource-card/utils"; import { Button, buttonVariants } from "@/components/ui/button"; import { @@ -6,13 +7,14 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { cn } from "@/lib/utils"; +import { CaretUpDown } from "@phosphor-icons/react"; 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 { workspaces, currentWorkspace } = useWorkspaces(); const { workspaceId } = useParams<{ workspaceId: string }>(); const [selectedWorkspaceId, setSelectedWorkspaceId] = useState(workspaceId); @@ -38,7 +40,11 @@ function WorkspaceSelector() { key={workspace.id} onMouseEnter={() => setSelectedWorkspaceId(workspace.short_name)} className={cn( - buttonVariants({ variant: "ghost", size: "sm" }), + buttonVariants({ + variant: + currentWorkspace?.id === workspace.id ? "secondary" : "ghost", + size: "sm", + }), "cursor-pointer justify-start py-0.5" )} > @@ -78,13 +84,22 @@ function WorkspaceSelector() { } export function NavigationBar() { + const { currentWorkspace } = useWorkspaces(); + + if (!currentWorkspace) { + return null; + } + return ( -
- +
+ + - + - + diff --git a/src/app/(outerbase)/session-provider.tsx b/src/app/(outerbase)/session-provider.tsx index 05159f47..4e4309c4 100644 --- a/src/app/(outerbase)/session-provider.tsx +++ b/src/app/(outerbase)/session-provider.tsx @@ -25,10 +25,11 @@ export function useSession() { export function OuterbaseSessionProvider({ children }: PropsWithChildren) { const router = useRouter(); const pathname = usePathname(); - const token = localStorage.getItem("ob-token"); + const token = + typeof window !== "undefined" ? localStorage.getItem("ob-token") : ""; const { data, isLoading } = useSWR( - token ? "session-" + localStorage.getItem("ob-token") : undefined, + token ? "session-" + token : undefined, () => { return getOuterbaseSession(); }, diff --git a/src/app/(outerbase)/w/[workspaceId]/layout.tsx b/src/app/(outerbase)/w/[workspaceId]/layout.tsx index f9fe8102..02b2a905 100644 --- a/src/app/(outerbase)/w/[workspaceId]/layout.tsx +++ b/src/app/(outerbase)/w/[workspaceId]/layout.tsx @@ -1,4 +1,5 @@ import { OuterbaseSessionProvider } from "@/app/(outerbase)/session-provider"; +import ClientOnly from "@/components/client-only"; import ThemeLayout from "../../../(theme)/theme_layout"; import { WorkspaceProvider } from "../../workspace-provider"; @@ -9,9 +10,11 @@ export default function RootLayout({ }) { return ( - - {children} - + + + {children} + + ); } diff --git a/src/app/(outerbase)/w/[workspaceId]/page-client.tsx b/src/app/(outerbase)/w/[workspaceId]/page-client.tsx index bf96afac..2d822a2d 100644 --- a/src/app/(outerbase)/w/[workspaceId]/page-client.tsx +++ b/src/app/(outerbase)/w/[workspaceId]/page-client.tsx @@ -8,13 +8,17 @@ import { getDatabaseIcon, getDatabaseVisual, } from "@/components/resource-card/utils"; +import { BoardVisual } from "@/components/resource-card/visual"; import { DropdownMenuItem } from "@/components/ui/dropdown-menu"; +import { getOuterbaseDashboardList } from "@/outerbase-cloud/api"; import { CalendarDots, + ChartBar, SortAscending, SortDescending, } from "@phosphor-icons/react"; import { useParams } from "next/navigation"; +import useSWR from "swr"; import { NavigationBar } from "../../navigation"; import { useWorkspaces } from "../../workspace-provider"; @@ -22,7 +26,18 @@ export default function WorkspaceListPageClient() { const { workspaces } = useWorkspaces(); const { workspaceId } = useParams<{ workspaceId: string }>(); - // const boards = data?.boards ?? []; + const { data: boards } = useSWR( + `/workspace/${workspaceId}/boards`, + () => { + return getOuterbaseDashboardList(workspaceId); + }, + { + revalidateIfStale: false, + revalidateOnFocus: false, + revalidateOnReconnect: false, + } + ); + const bases = workspaces.find( (workspace) => @@ -31,20 +46,6 @@ export default function WorkspaceListPageClient() { return (
- {/*

Board

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

Base

*/}
@@ -75,6 +76,26 @@ export default function WorkspaceListPageClient() {
+

Board

+
+ {(boards?.items ?? []) + .filter((board) => board.base_id === null) + .map((board) => ( + + ))} +
+ +

Base

+
{bases.map((base) => ( ({ workspaces: [], loading: true }); @@ -26,9 +28,27 @@ export function WorkspaceProvider({ children }: PropsWithChildren) { } ); + const { workspaceId } = useParams<{ workspaceId?: string }>(); + + const currentWorkspace = useMemo(() => { + if (!workspaceId) return; + if (!data) return; + + return data?.items.find( + (workspace) => + workspace.short_name === workspaceId || workspace.id === workspaceId + ); + }, [workspaceId, data]); + + console.log(data, workspaceId, currentWorkspace); + return ( {children} diff --git a/src/components/gui/sql-editor/index.tsx b/src/components/gui/sql-editor/index.tsx index 936e27ae..0024c87d 100644 --- a/src/components/gui/sql-editor/index.tsx +++ b/src/components/gui/sql-editor/index.tsx @@ -1,28 +1,33 @@ -import CodeMirror, { - EditorView, - Extension, - ReactCodeMirrorRef, -} from "@uiw/react-codemirror"; -import { indentUnit, LanguageSupport } from "@codemirror/language"; import { acceptCompletion, completionStatus, startCompletion, } from "@codemirror/autocomplete"; -import { sql, SQLNamespace, MySQL as MySQLDialect } from "@codemirror/lang-sql"; +import { + MySQL as MySQLDialect, + PostgreSQL as PostgresDialect, + sql, + SQLNamespace, +} from "@codemirror/lang-sql"; +import { indentUnit, LanguageSupport } from "@codemirror/language"; +import CodeMirror, { + EditorView, + Extension, + ReactCodeMirrorRef, +} from "@uiw/react-codemirror"; import { forwardRef, KeyboardEventHandler, useMemo } from "react"; +import { SupportedDialect } from "@/drivers/base-driver"; +import sqliteFunctionList from "@/drivers/sqlite/function-tooltip.json"; +import { sqliteDialect } from "@/drivers/sqlite/sqlite-dialect"; +import { KEY_BINDING } from "@/lib/key-matcher"; import { defaultKeymap, insertTab } from "@codemirror/commands"; import { keymap } from "@codemirror/view"; -import { KEY_BINDING } from "@/lib/key-matcher"; -import useCodeEditorTheme from "./use-editor-theme"; -import createSQLTableNameHighlightPlugin from "./sql-tablename-highlight"; -import { sqliteDialect } from "@/drivers/sqlite/sqlite-dialect"; -import { functionTooltip } from "./function-tooltips"; -import sqliteFunctionList from "@/drivers/sqlite/function-tooltip.json"; import { toast } from "sonner"; +import { functionTooltip } from "./function-tooltips"; +import createSQLTableNameHighlightPlugin from "./sql-tablename-highlight"; import SqlStatementHighlightPlugin from "./statement-highlight"; -import { SupportedDialect } from "@/drivers/base-driver"; +import useCodeEditorTheme from "./use-editor-theme"; interface SqlEditorProps { value: string; @@ -135,6 +140,11 @@ const SqlEditor = forwardRef( schema, }); tooltipExtension = functionTooltip(sqliteFunctionList); + } else if (dialect === "postgres") { + sqlDialect = sql({ + dialect: PostgresDialect, + schema, + }); } else { sqlDialect = sql({ dialect: MySQLDialect, diff --git a/src/components/icons/outerbase.tsx b/src/components/icons/outerbase.tsx new file mode 100644 index 00000000..3c693aa7 --- /dev/null +++ b/src/components/icons/outerbase.tsx @@ -0,0 +1,11 @@ +export function OuterbaseIcon(props: { className?: string }) { + return ( + + + + ); +} diff --git a/src/components/sidebar-menu.tsx b/src/components/sidebar-menu.tsx index 80ba03b7..16e202f3 100644 --- a/src/components/sidebar-menu.tsx +++ b/src/components/sidebar-menu.tsx @@ -1,3 +1,4 @@ +import { cn } from "@/lib/utils"; import Link from "next/link"; interface SidebarMenuItemProps { @@ -17,6 +18,7 @@ export function SidebarMenuItem({ onClick, icon: IconComponent, href, + selected, }: SidebarMenuItemProps) { const className = "flex p-2 pl-4 text-xs hover:cursor-pointer hover:bg-secondary"; @@ -49,7 +51,10 @@ export function SidebarMenuItem({ } return ( - ); diff --git a/src/extensions/data-catalog/data-catalog-entry-modal.tsx b/src/extensions/data-catalog/data-catalog-entry-modal.tsx new file mode 100644 index 00000000..7e63027b --- /dev/null +++ b/src/extensions/data-catalog/data-catalog-entry-modal.tsx @@ -0,0 +1,172 @@ +import { ToolbarFiller } from "@/components/gui/toolbar"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { LucideLoader } from "lucide-react"; +import { useCallback, useEffect, useState } from "react"; +import DataCatalogDriver, { DataCatalogTermDefinition } from "./driver"; + +interface Props { + driver?: DataCatalogDriver; + open: boolean; + onSuccess: () => void; + onClose: (open: boolean) => void; + selectedTermDefinition?: DataCatalogTermDefinition; +} + +export function DataCatalogEntryModal({ + open, + onClose, + driver, + onSuccess, + selectedTermDefinition, +}: Props) { + const [deleting, setDeleting] = useState(false); + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + id: "", + name: "", + otherName: "", + definition: "", + }); + + const clear = useCallback(() => { + setLoading(false); + setDeleting(false); + onClose(false); + setFormData({ + id: "", + name: "", + otherName: "", + definition: "", + }); + }, [onClose]); + + useEffect(() => { + if (selectedTermDefinition) { + setFormData(selectedTermDefinition); + } else { + clear(); + } + }, [selectedTermDefinition, clear]); + + const saveTermDefinition = useCallback(() => { + setLoading(true); + const data = { + ...formData, + id: selectedTermDefinition?.id || String(Date.now() * 1000), // Use existing ID if editing + }; + + driver + ?.updateTermDefinition(data) + .then(() => onSuccess()) + .finally(() => clear()); + }, [formData, driver, onSuccess, clear, selectedTermDefinition]); + + function onDelete() { + if (!selectedTermDefinition) return; + + setDeleting(true); + driver + ?.deleteTermDefinition(selectedTermDefinition.id) + .then(() => onSuccess()) + .finally(() => clear()); + } + + const onChangeValue = useCallback( + (value: string, key: keyof DataCatalogTermDefinition) => { + setFormData((prev) => ({ + ...prev, + [key]: value, + })); + }, + [] + ); + + return ( + + + + + {selectedTermDefinition ? "Edit Term" : "Add Term"} + + + {selectedTermDefinition + ? "Modify the existing term definition." + : "Add terms to your Data Dictionary to help your team and AI understand important business terminology."} + + +
+
+ + onChangeValue(e.currentTarget.value, "name")} + /> +
+
+ + + onChangeValue(e.currentTarget.value, "otherName") + } + /> +
+
+ +