Improved Performence , Re-Order Diagram whene import , Fix Database History Bug (stuck in undo or redo , foriegn keys constraint error)

This commit is contained in:
KarimTamani
2025-07-03 22:01:12 +01:00
parent 815845e1e9
commit 9ecc2f9d56
50 changed files with 982 additions and 549 deletions
+1
View File
@@ -49,6 +49,7 @@
"uuid": "^11.1.0"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"@types/node": "20.5.7",
"@types/object-hash": "^3.0.6",
"@types/react": "18.3.3",
@@ -1,3 +1,4 @@
import { CardinalityStyle } from "@/lib/database";
import { cn } from "@heroui/react";
import React from "react";
@@ -6,12 +7,12 @@ export interface CardinalityMarkerProps {
selected?: boolean,
direction?: "start" | "end",
cardinality: "one" | "many",
type?: "symbole" | "numeric"
style?: CardinalityStyle
}
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", type = "symbole" }) => {
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", style = CardinalityStyle.SYMBOLIC }) => {
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
const renderMarker = () => {
@@ -37,7 +38,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
}
if (type == "symbole")
if (style == CardinalityStyle.SYMBOLIC)
return (
<>
<marker
@@ -63,22 +64,22 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
</marker>
</>
)
else if (type == "numeric") {
else if (style == CardinalityStyle.NUMERIC) {
return (
<marker
id={id}
viewBox="0 0 24 24"
markerWidth="24"
markerHeight="24"
refX={direction == "start" ? "4" : "20"}
refX={direction == "start" ? "2" : "22"}
refY="12"
orient="auto"
>
<circle
cx="12"
cy="12"
r="6"
stroke-width="1"
r="8"
strokeWidth="1"
className={
cn("dark:fill-default fill-default",
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
@@ -88,11 +89,11 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
<text
x="12"
y="13"
text-anchor="middle"
dominant-baseline="middle"
font-size="7"
textAnchor="middle"
dominantBaseline="middle"
fontSize="8"
className={
cn("fill-font/90 dark:fill-font/90" ,
cn("fill-font/90 dark:fill-font/90 font-semibold" ,
selected ? "fill-primary dark:fill-primary" : "" ,
)
}
@@ -16,8 +16,8 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
value={database.dialect}
className="database-checkbox"
classNames={{
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative",
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default" ,
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative data-[selected=true]:border-1 data-[selected=true]:border-primary data-[selected=true]:bg-primary/5",
wrapper: "absolute top-2 left-2 before:border-divider group-data-[hover=true]:before:bg-default " ,
label : "flex items-center justify-center w-full h-full p-2 "
}}
@@ -26,7 +26,7 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
<div className="min-w-full h-full flex items-center justify-center ">
<Image
src={database.logo}
className="w-full"
className="w-full rounded-none"
/>
</div>
</Checkbox>
+14 -13
View File
@@ -1,5 +1,7 @@
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
import { useTheme } from "next-themes";
import React from "react";
interface OptionCheckboxProps {
value: string;
@@ -13,15 +15,15 @@ interface OptionCheckboxProps {
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
const { value, icon, logo, label, isSelected } = props;
const variant = isSelected ? {
variant: "flat",
color: "primary"
let variant : any = isSelected ? {
variant: "solid",
color: "default"
} : {
variant: "bordered",
variant: "borderd",
color: "default"
}
return (
return (
<Checkbox
aria-label={value}
value={value}
@@ -32,16 +34,15 @@ const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
label: "flex items-center justify-center w-full h-full "
}}
>
<Chip radius="sm" {...variant as any} className={cn("option-span px-3 h-8 border-1 transition-all duration-300 border-divider ",
!isSelected ? "dark:bg-default" : undefined
<Chip radius="sm" {...variant as any} className={cn("dark:bg-background px-3 h-9 border-1 transition-all duration-300 border-divider text-font/90 ",
!isSelected ? "dark:bg-default" : undefined
)}
avatar={logo ? <img src={logo} /> : undefined}
avatar={logo ? <img src={logo} height={12} /> : undefined}
startContent={
icon
}
>
<span className="text-sm font-medium">
<span className="text-xs font-medium ">
{label}
</span>
</Chip>
@@ -50,4 +51,4 @@ const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
}
export default OptionCheckbox;
export default React.memo(OptionCheckbox);
+1 -1
View File
@@ -40,7 +40,7 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
size="sm"
isIconOnly
variant="bordered"
className="text-font/90 border-divider"
className="text-font/90 border-1 border-divider"
onPressEnd={copyToClipboard}
>
{
+30 -29
View File
@@ -1,6 +1,7 @@
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button } from "@heroui/react";
import { useMemo } from "react";
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, Popover, PopoverTrigger, PopoverContent, cn } from "@heroui/react";
import { useCallback, useMemo } from "react";
import SubmenuDropdown from "./submenu-dropdown";
import { Check } from "lucide-react";
@@ -12,78 +13,78 @@ import SubmenuDropdown from "./submenu-dropdown";
export interface MenuDropdownProps {
id: string;
title?: string;
children?: MenuDropdownProps[];
theme?: "default" | "danger",
isDisabled?: boolean,
divide?: boolean,
shortcut?: string,
isOpen?: boolean ,
clickHandler? : () => void
isOpen?: boolean,
selected?: boolean;
clickHandler?: () => void
}
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler }) => {
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHandler, selected = false }) => {
const disabledChilds: string[] = useMemo(() => {
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled && child.title).map((child: MenuDropdownProps) => child.title as string) : []
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled).map((child: MenuDropdownProps) => child.id as string) : []
}, [children]);
return <Dropdown radius="sm" shadow="sm" showArrow >
return <Dropdown radius="sm" shadow="sm" showArrow>
{
title &&
<DropdownTrigger onPressEnd={clickHandler}>
<Button size="sm" variant="light" className="min-w-[42px]" >
<DropdownTrigger onPressEnd={clickHandler}>
<Button size="sm" variant="light" className="min-w-[42px]" aria-label={title} >
<span className=" text-left flex justify-between text-small font-semibold text-font/90">
{title}
</span>
</Button>
</DropdownTrigger>
}
<HeroDropdownMenu disabledKeys={disabledChilds} >
<HeroDropdownMenu
disabledKeys={disabledChilds}
>
{
children ? children?.map((child: MenuDropdownProps) => (
<DropdownItem
key={child.title as string}
key={child.id as string}
color={child.theme}
shortcut={child.shortcut}
classNames={{
shortcut : "dark:border-font/10"
shortcut: "dark:border-font/10",
base: child.children ? "p-0" : "p-2"
}}
className="text-font/90"
onPressEnd={ child.clickHandler }
>
textValue={child.title}
endContent={
child.selected ? <Check className="text-icon size-4" /> : undefined
}
className={child.theme == "danger" ? "text-danger" : "text-font/90"}
onPressEnd={child.clickHandler}
>
{
!child.children ?
<div >
{child.title}
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
}
</div>
:
<div >
<SubmenuDropdown {...child} />
<SubmenuDropdown {...child} />
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider"></div>
}
</div>
}
</DropdownItem>
)) : []
}
</HeroDropdownMenu>
</Dropdown>
}
+77 -24
View File
@@ -5,34 +5,44 @@ import React, { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { useDatabaseHistory } from "@/providers/database-history/database-history-provider";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { TableType } from "@/lib/schemas/table-schema";
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
import { CardinalityStyle } from "@/lib/database";
const Menu: React.FC<any > = ({ }) => {
const Menu: React.FC = ({ }) => {
const { setTheme } = useTheme()
const { setTheme, resolvedTheme } = useTheme()
const { t } = useTranslation();
const { open } = useModal();
const { canRedo, canUndo, undo, redo } = useDatabaseHistory();
const { deleteMultiTables } = useDatabaseOperations();
const { database } = useDatabase();
const { setShowController, showController, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
const menu: MenuDropdownProps[] = useMemo(() => [
{
id: "menu.file",
title: t("menu.file"),
children: [
{
id: "menu.new",
title: t("menu.new"),
clickHandler: () => {
open(Modals.CREATE_DATABASE)
}
},
{
id: "menu.open",
title: t("menu.open"), shortcut: "Ctnl + O", clickHandler: () => {
open(Modals.OPEN_DATABASE)
}
},
{ title: t("menu.save"), shortcut: "Ctnl + S", divide: true },
{
id: "menu.import",
title: t("menu.import"),
divide: true,
clickHandler: () => {
@@ -40,15 +50,15 @@ const Menu: React.FC<any > = ({ }) => {
}
},
{
id: "menu.export_sql",
title: t("menu.export_sql"),
children: [
{ title: t("menu.generic") },
{ title: t("menu.mysql") },
{ title: t("menu.postgresql") },
],
clickHandler: () => {
open(Modals.EXPORT_SQL)
}
},
{ title: t("menu.export_orm_models"), divide: true },
//{ title: t("menu.export_orm_models"), divide: true },
{
id: "menu.delete_project",
title: t("menu.delete_project"), theme: "danger", clickHandler: () => {
open(Modals.DELETE_DATABASE)
}
@@ -56,54 +66,97 @@ const Menu: React.FC<any > = ({ }) => {
],
},
{
id: "menu.edit",
title: t("menu.edit"),
clickHandler: () => console.log("hello wo"),
children: [
{ title: t("menu.undo"), isDisabled: true },
{ title: t("menu.redo"), isDisabled: true },
{ title: t("menu.clear") },
{ id: "menu.undo", title: t("menu.undo"), isDisabled: !canUndo, clickHandler: undo },
{ id: "menu.redo", title: t("menu.redo"), isDisabled: !canRedo, clickHandler: redo },
{
id: "menu.clear",
title: t("menu.clear"), clickHandler: () => {
const tables = database ? database.tables : [];
deleteMultiTables(tables.map((table: TableType) => table.id))
}
},
],
},
{
id: "menu.view",
title: t("menu.view"),
children: [
{ title: t("menu.hide_controller"), shortcut: "Ctnl + B", divide: true },
{
title: t("menu.zoom_on_scroll"),
id: "menu.cardinality_style",
title: t("menu.cardinality_style"),
divide: true,
children: [
{ title: t("menu.on") },
{ title: t("menu.off") },
{
id: "menu.symbolic",
selected: cardinalityStyle == CardinalityStyle.SYMBOLIC,
title: t("menu.symbolic"), clickHandler: () => {
setCardinalityStyle(CardinalityStyle.SYMBOLIC)
}
},
{
id: "menu.numeric",
selected: cardinalityStyle == CardinalityStyle.NUMERIC,
title: t("menu.numeric"), clickHandler: () => {
setCardinalityStyle(CardinalityStyle.NUMERIC)
}
},
{
id: "menu.hidden",
selected: cardinalityStyle == CardinalityStyle.HIDDEN,
title: t("menu.hidden"), clickHandler: () => {
setCardinalityStyle(CardinalityStyle.HIDDEN)
}
},
],
},
{
id: "menu.controller_visibility",
title: showController ? t("menu.hide_controller") : t("menu.show_controller"), shortcut: "Ctnl + B", divide: true, clickHandler: () => {
setShowController(!showController)
}
},
{
id: "menu.theme",
title: t("menu.theme"),
clickHandler: () => console.log("hello wo"),
children: [
{
id: "light",
title: t("menu.light"),
clickHandler: () => setTheme("light"),
selected: resolvedTheme == "light",
},
{
id: "dark",
title: t("menu.dark"),
clickHandler: () => setTheme("dark"),
selected: resolvedTheme == "dark"
},
],
},
],
},
{
id: "menu.help",
title: t("menu.help"),
children: [
{ title: t("menu.show_docs") },
{ title: t("menu.join_discord") },
{ id: "menu.show_docs", title: t("menu.show_docs") },
{ id: "menu.join_discord", title: t("menu.join_discord") },
],
},
], [t]);
], [t, canRedo, canUndo, undo, redo, deleteMultiTables, database, showController, setShowController, cardinalityStyle, setCardinalityStyle, resolvedTheme]);
return <div className="gap-1 flex">
{
menu.map((menuItem, index) => (
<DropdownMenu {...menuItem} key={index} />
<DropdownMenu {...menuItem} key={menuItem.id} />
))
}
</div>
+18 -13
View File
@@ -1,31 +1,36 @@
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
import { MenuDropdownProps } from "./menu-dropdown";
import { ChevronRight } from "lucide-react";
import { Check, ChevronRight } from "lucide-react";
import DropdownMenu from "./menu-dropdown";
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ title, children }) => {
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ id , title, children }) => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<div onMouseEnter={onOpen} onMouseLeave={onClose} >
<Popover placement="right" isOpen={isOpen} radius="sm" offset={90} shadow="sm">
<PopoverTrigger>
<Button className="w-full h-6 bg-transparent p-0 text-font/90" size="sm" value={"light"}>
return (
<div onMouseEnter={onOpen} onMouseLeave={onClose} className="p-2">
<Popover placement="right" isOpen={isOpen} radius="sm" shadow="sm" >
<PopoverTrigger >
<Button className="w-full h-6 bg-transparent p-0 text-font/90 " size="sm" value={"light"}>
<span className="w-full text-left flex justify-between text-small font-normal">
{title}
<ChevronRight className="size-4 text-icon" />
</span>
</Button>
</PopoverTrigger>
<PopoverContent>
<DropdownMenu
<PopoverContent className="p-0 border-0 shadow-none block w-[200px] ">
<div className="h-8">
<DropdownMenu
id = { id }
children={children}
/>
children={children}
/>
</div>
</PopoverContent>
</Popover>
</div>
+24 -11
View File
@@ -39,8 +39,14 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
const handleAction = async (onClose: () => void) => {
setIsLoading(true)
actionHandler && await actionHandler();
setIsLoading(false);
try {
actionHandler && await actionHandler();
} catch (error) {
setIsLoading(false);
} finally {
setIsLoading(false);
}
}
return (
<HeroUiModal
@@ -53,7 +59,7 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
classNames={{
base: "dark:bg-background-100",
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md" , !closable ? "hidden" : "")
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md", !closable ? "hidden" : "")
}}
>
@@ -84,12 +90,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
{t("modals.close")}
</Button>
}
{
{
!closable && <div></div>
}
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
{
actionHandler &&
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
{
@@ -101,12 +110,16 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
{t("modals.close")}
</Button>
}
{
{
!closable && <div></div>
}
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
{
actionHandler &&
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
}
</>
}
+2 -2
View File
@@ -49,13 +49,13 @@ const ConnectionStatus: React.FC = () => {
useEffect(() => {
if (syncStatus.downloadProgress?.downloadedOperations && syncStatus.downloadProgress?.totalOperations) {
const progress: number = Math.round(syncStatus.downloadProgress.downloadedOperations / syncStatus.downloadProgress.totalOperations * 100);
if (progress <= 100) {
setDownloadProgress(progress);
}
if (downloadProgress == 100 && !clearProgressHandler.current) {
clearProgressHandler.current = setTimeout(() => {
setDownloadProgress(undefined);
clearProgressHandler.current = undefined;
+1 -3
View File
@@ -20,8 +20,6 @@ const Navbar: React.FC<Props> = ({ }) => {
src={`/stackrender.png`}
width={22}
alt="logo"
/>
<h3 className="font-semibold ml-2 text-slate-900 text-sm dark:text-white">StackRender</h3>
<div className="ml-4 w-full">
@@ -31,8 +29,8 @@ const Navbar: React.FC<Props> = ({ }) => {
<div className=" w-full h-full flex items-center justify-center">
{database && <RenameDatabase database={database} />}
</div>
<ConnectionStatus />
</div>
</div>
</nav>
+9 -8
View File
@@ -4,15 +4,14 @@ import { Node, useReactFlow } from "@xyflow/react";
import { useEffect } from "react";
import { getDefaultTableOverlapping } from "@/utils/tables";
import hash from "object-hash";
import { useDatabase } from "@/providers/database-provider/database-provider";
export const useTableToNode = (tables: TableType[]): void => {
const { setNodes } = useReactFlow();
useEffect(() => {
useEffect(() => {
const tableNodes = tables.map((table: TableType) => {
return {
id: table.id,
@@ -31,21 +30,23 @@ export const useTableToNode = (tables: TableType[]): void => {
width: 224
}
} as Node
})
}) ;
setNodes((nodes) => {
return tableNodes.map((tableNode) => {
const node: Node | undefined = nodes.find((node: Node) => node.id == tableNode.id);
if (!node)
return tableNode;
else {
const hashNode: string = hash(node.data.table as TableType);
const hashTableNode: string = hash(tableNode.data.table as TableType);
return hashNode == hashTableNode ? node : tableNode;
}
})
});
});
}, [tables])
}
+14 -6
View File
@@ -38,7 +38,7 @@ export const en = {
foreign_key: "Foreign Key",
source_table: "Source Table",
target_table: "Target Table",
referenced_table: "Referenced Table",
select_table: "Select table",
select_field: "Select field",
@@ -154,9 +154,11 @@ export const en = {
clear: "Clear",
view: "View",
hide_controller: "Hide Controller",
zoom_on_scroll: "Zoom on scroll",
on: "On",
off: "Off",
show_controller : "Show Controller" ,
cardinality_style: "Cardinality style",
hidden: "Hidden",
numeric: "Numeric",
symbolic : "Symbolic" ,
theme: "Theme",
light: "Light",
dark: "Dark",
@@ -185,8 +187,14 @@ export const en = {
import_database: {
title: "Import your Database",
import: "Import",
import_options: "Would you like to import using :"
}
import_options: "Would you like to import using :" ,
import_error : "SQL Parsing Error" ,
import_error_description : "We couldn't import your SQL because it contains invalid syntax." ,
import_warning : "SQL Parsing Warning" ,
import_warning_description : "Some elements couldn't be processed due to unsupported or incomplete declarations." ,
} ,
export_sql : "Export SQL" ,
export_sql_header : "Export your database diagram in SQL Code"
},
clipboard: {
copy: "Copy",
+9 -1
View File
@@ -84,4 +84,12 @@ export const MARIADB_DUMP_EXAMPLE = "mariadb-dump -u root -p --no-data example_d
export const SQLITE_DUMP_INSRUCTION = "sqlite3 [daabase_path] .schema > [output_file.sql]";
export const SQLITE_DUMP_EXAMPLE = "sqlite3 example_db.sqlite .schema > example_db.sql";
export const SQLITE_DUMP_EXAMPLE = "sqlite3 example_db.sqlite .schema > example_db.sql";
export enum CardinalityStyle {
HIDDEN = "HIDDEN" ,
NUMERIC = "NUMERIC" ,
SYMBOLIC = "SYMBOLIC"
}
+28 -34
View File
@@ -43,6 +43,8 @@ import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { Loading } from "@/components/modal/loading-modal";
import { usePowerSync, usePowerSyncStatus } from "@powersync/react";
import { CardinalityStyle } from "@/lib/database";
import debounce from 'lodash.debounce';
@@ -50,10 +52,6 @@ const DatabasePage: React.FC = () => {
const { open } = useModal();
const syncStatus = usePowerSync();
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
// Extract database state and operations
@@ -66,7 +64,7 @@ const DatabasePage: React.FC = () => {
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
// Diagram-related state (e.g. connection in progress)
const { setIsConnectionInProgress } = useDiagramOps();
const { setIsConnectionInProgress, cardinalityStyle, setCardinalityStyle } = useDiagramOps();
// Destructure tables and relationships from database
const { tables, relationships } = database || { tables: [], relationships: [] };
@@ -78,11 +76,11 @@ const DatabasePage: React.FC = () => {
const nodeTypes = useMemo(() => ({ table: Table }), []);
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
useEffect(() => {
useEffect(() => {
if (isLoading || isFetching || !(syncStatus.currentStatus as any).options.hasSynced)
return;
// no database selected , open (open database) Modal
if (!database && databases?.length > 0) {
open(Modals.OPEN_DATABASE, {
@@ -95,13 +93,9 @@ const DatabasePage: React.FC = () => {
open(Modals.CREATE_DATABASE, {
closable: false
});
}
}, [database?.id, isLoading, isFetching , (syncStatus.currentStatus as any).options.hasSynced])
}
}, [database?.id, isLoading, isFetching, (syncStatus.currentStatus as any).options.hasSynced])
useEffect(() => {
if (isSwitchingDatabase) {
setNodes([]);
@@ -121,7 +115,6 @@ const DatabasePage: React.FC = () => {
const sourceField: FieldType = getField(connection.source, sourceId as string) as FieldType;
const targetField: FieldType = getField(connection.target, targetId) as FieldType;
const { sourceTableId, targetTableId, sourceFieldId, targetFieldId } = getRelationshipSourceAndTarget(connection.source, sourceField, connection.target, targetField);
// Check if both fields have the same type (valid relationship)
@@ -135,8 +128,6 @@ const DatabasePage: React.FC = () => {
} as RelationshipInsertType);
// Add edge to the diagram
//setEdges((eds) => addEdge(connection, eds));
} else {
// Show error toast if invalid relationship
addToast({
@@ -158,10 +149,11 @@ const DatabasePage: React.FC = () => {
) as NodePositionChange[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
await updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
id: change.id,
posX: change.position?.x,
posY: change.position?.y
@@ -177,7 +169,6 @@ const DatabasePage: React.FC = () => {
// Called when edges (relationships) change
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
const edgeRemoveChanges: EdgeRemoveChange[] = changes.filter((change: EdgeChange) => change.type == "remove") as EdgeRemoveChange[];
// Delete relationships from database
if (edgeRemoveChanges.length > 0) {
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
@@ -215,7 +206,6 @@ const DatabasePage: React.FC = () => {
const { isOverlapping, puls } = useOverlappingTables(tables);
return (
<div className="w-full h-screen flex relative overflow-hidden">
@@ -239,8 +229,8 @@ const DatabasePage: React.FC = () => {
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
animated: false
}}
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
@@ -249,6 +239,7 @@ const DatabasePage: React.FC = () => {
minZoom={0.10}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<Controls
position="bottom-center"
@@ -300,29 +291,32 @@ const DatabasePage: React.FC = () => {
</div>
}
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker type="numeric" cardinality="one" direction="start" />
<CardinalityMarker type="numeric" cardinality="one" direction="start" selected />
{
(cardinalityStyle != CardinalityStyle.HIDDEN) &&
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="start" selected />
<CardinalityMarker type="numeric" cardinality="one" direction="end" />
<CardinalityMarker type="numeric" cardinality="one" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="one" direction="end" selected />
<CardinalityMarker type="numeric" cardinality="many" direction="start" />
<CardinalityMarker type="numeric" cardinality="many" direction="start" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="start" selected />
<CardinalityMarker type="numeric" cardinality="many" direction="end" />
<CardinalityMarker type="numeric" cardinality="many" direction="end" selected />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" />
<CardinalityMarker style={cardinalityStyle} cardinality="many" direction="end" selected />
</defs>
</svg>
</defs>
</svg>
}
</div>
)
}
export default DatabasePage;
export default DatabasePage;
@@ -1,9 +1,11 @@
import React, { useState } from "react"
import React, { useEffect, useState } from "react"
import { Outlet } from "react-router-dom"
import { ResizableBox as ResizableBoxRaw } from 'react-resizable';
import 'react-resizable/css/styles.css';
import { GripVertical } from "lucide-react";
import 'react-resizable/css/styles.css';
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
@@ -14,29 +16,56 @@ interface Props {
const DBController: React.FC<Props> = ({ }) => {
const [width, setWidth] = useState<number>(512);
const { showController } = useDiagramOps()
const [show, setShow] = useState<boolean>(!showController);
const [style, setStyle] = useState<any>(undefined);
const onResize = (event: any, params: any) => {
setWidth(params.size.width);
}
useEffect(() => {
if (!showController) {
setWidth(0);
setStyle({
transition: 'width 0.5s',
})
setTimeout(() => {
setShow(false);
}, 500);
return (
<ResizableBox
width={width}
onResize={onResize} className="min-h-full overflow-visible "
minConstraints={[512]}
axis="x"
handle={
} else if (showController) {
setShow(true);
setTimeout(() => setWidth(512))
;
setTimeout(() => {
setStyle(undefined);
}, 500)
}
}, [showController])
if (show)
return (
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary-300 active:border-primary-400 transition-colors duration-200 ">
<ResizableBox
width={width}
onResize={onResize} className="min-h-full overflow-hidden "
minConstraints={[512]}
axis="x"
style={style}
handle={
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary-300 active:border-primary-400 transition-colors duration-200 ">
</div>
}
>
<div className="min-w-[512px] w-full h-full bg-background dark:bg-background-50 border-r p-2 pt-[52px] border-default-100 dark:border-divider">
<Outlet />
</div>
}
>
<div className="min-w-[512px] w-full h-full bg-background dark:bg-background-50 border-r p-2 pt-[52px] border-default-100 dark:border-divider">
<Outlet />
</div>
</ResizableBox>
)
</ResizableBox>
)
else
return;
}
export default React.memo(DBController)
@@ -70,7 +70,7 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
<div className="w-full space-y-1">
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm ">
<FileMinus2 className="size-4" />
{t(" .target_table")}
{t("db_controller.referenced_table")}
</label>
@@ -10,7 +10,7 @@ import CreateRelationshipForm from "../../modals/create-relationship-modal";
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
import { getDefaultRelationshipName } from "@/utils/relationship";
@@ -39,13 +39,17 @@ const RelationshipController: React.FC = ({ }) => {
useEffect(() => {
if (focusedRelationshipId) {
setSelectedRelationship(new Set([focusedRelationshipId]) as any);
const accordionItem = document.getElementById(focusedRelationshipId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
})
}
}, [focusedRelationshipId]);
const onOpen = useCallback(() => {
open(Modals.CREATE_RELATIONSHIP, {
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
})
}, [])
@@ -66,12 +70,12 @@ const RelationshipController: React.FC = ({ }) => {
}
}, [nameRef, allRelationships]);
const selectedRelationshipId = selectedRelationship.values().next().value;
const selectedRelationshipId = selectedRelationship.values().next().value;
const collapseAll = useCallback(() => {
setSelectedRelationship(new Set([])) ;
} , [])
setSelectedRelationship(new Set([]));
}, [])
return (
<div className="w-full h-full flex flex-col gap-2">
@@ -84,8 +88,8 @@ const RelationshipController: React.FC = ({ }) => {
variant="light"
className="size-8 p-0 text-icon hover:text-font/90"
isIconOnly
onPressEnd={collapseAll}
onPressEnd={collapseAll}
>
<ListCollapse className="size-4" />
@@ -108,7 +112,7 @@ const RelationshipController: React.FC = ({ }) => {
onKeyUp={searchRelationships}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
}}
/>
@@ -138,6 +142,7 @@ const RelationshipController: React.FC = ({ }) => {
{relationships.map((relationship: RelationshipType) => (
<AccordionItem
key={relationship.id}
id={relationship.id}
aria-label={relationship.id}
classNames={{
trigger: "w-full hover:bg-default transition-all duration-200 h-12 dark:hover:bg-background",
@@ -18,38 +18,29 @@ import { RelationshipType } from "@/lib/schemas/relationship-schema";
interface SqlPreviewProps {
tableFilterIds ? : string[]
tableFilterIds?: string[]
}
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
const { database : currentDatabase } = useDatabase();
const database = useMemo(() => {
const { database: currentDatabase } = useDatabase();
const database = useMemo(() => {
if ( !tableFilterIds )
return currentDatabase ;
return {
...currentDatabase ,
tables : currentDatabase?.tables.filter((table : TableType) => tableFilterIds?.includes(table.id)) ,
relationships : currentDatabase?.relationships.filter((relationship : RelationshipType) =>
tableFilterIds?.includes( relationship.sourceTableId) || tableFilterIds?.includes(relationship.targetFieldId)
),
} as DatabaseType ;
} , [currentDatabase ,tableFilterIds ])
...currentDatabase,
tables: currentDatabase?.tables.filter((table: TableType) => tableFilterIds?.includes(table.id)),
relationships: currentDatabase?.relationships.filter((relationship: RelationshipType) =>
tableFilterIds?.includes(relationship.sourceTableId) || tableFilterIds?.includes(relationship.targetFieldId)
),
} as DatabaseType;
}, [currentDatabase, tableFilterIds])
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
const { resolvedTheme } = useTheme();
const { t } = useTranslation();
console.log ( tableFilterIds ) ;
useEffect(() => {
if (circularDependency)
addToast({
@@ -62,26 +53,26 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
if (circularDependency)
return <CircularDependencyAlert error={circularDependency} />
else
return (
<div className="flex w-full h-full relative">
<div className="flex w-full h-full relative ">
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
<Clipboard
text={sqlCode}
/>
</div>
{
<CodeMirror
defaultValue={sqlCode}
value={sqlCode}
className="flex flex-1 w-full "
extensions={[sql()]}
readOnly
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
}
<CodeMirror
defaultValue={sqlCode}
value={sqlCode}
className="flex flex-1 w-full "
extensions={[sql()]}
readOnly
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
</div>
)
@@ -89,4 +80,4 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
export default React.memo(SqlPreview) ;
export default React.memo(SqlPreview);
@@ -11,6 +11,7 @@ import { useDatabaseOperations } from "@/providers/database-provider/database-pr
import Autocomplete from "@/components/auto-complete/auto-complete";
import ToggleButton from "@/components/toggle/toggle";
import FieldSetting from "./field-setting";
import { DataType } from "@/lib/schemas/data-type-schema";
interface Props {
field: FieldType
}
@@ -22,7 +23,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
const [fieldName, setFieldName] = useState<string>(field.name);
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const { grouped_data_types } = useDatabaseOperations();
const { grouped_data_types, data_types } = useDatabaseOperations();
const { editField } = useDatabaseOperations();
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
@@ -50,13 +51,18 @@ const FieldItem: React.FC<Props> = ({ field }) => {
} as FieldType);
}
const updateFieldType = (key: Key | null) => {
const dataType: DataType | undefined = data_types.find((dataTypes: DataType) => dataTypes.id == key);
if (!dataType)
return;
if (key != null) {
editField({
id: field.id,
typeId: key
} as FieldType);
setSelectedType(key as string | undefined);
}
}
@@ -87,7 +87,8 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
id: field.id,
note: note,
} as FieldInsertType)
}, [field])
}, [field , note]) ;
const toggleUnqiue = useCallback((value: boolean) => {
editField({
id: field.id,
@@ -569,7 +570,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
base: "max-w-xs",
input: "resize-y min-h-[60px] max-h-[180px]",
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
label: "text-font/90 group-data-[focus=true]:text-font/70"
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70 "
}} />
<hr className="border-divider" />
<Button
@@ -160,7 +160,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table, keys })
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")}
classNames={{
inputWrapper: "bg-default border-divider dark:bg-background-100 group-hover:border-primary group-data-[focus=true]:border-primary",
label: "text-font/90 group-data-[focus=true]:text-font/70"
label: "text-font/90 group-data-[focus=true]:text-font/70 group-data-[filled-within=true]:text-font/70"
}}
value={note}
onValueChange={setNote}
@@ -21,16 +21,13 @@ export interface TableAccordionHeaderProps {
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
const { editTable, deleteTable, createField, createTable, createIndex , getInteger } = useDatabaseOperations();
const { editTable, deleteTable, createField, createTable, createIndex, getInteger } = useDatabaseOperations();
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
const [tableName, setTableName] = useState<string>(table.name);
const { t } = useTranslation();
const [editMode, setEditMode] = useState<boolean>(false);
const { focusOnTable } = useDiagramOps();
useEffect(() => {
setTableName(table.name);
}, [table.name])
@@ -40,12 +37,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
setEditMode(false);
}, [tableName])
const onDeleteTable = async () => {
const onDeleteTable = useCallback(async () => {
deleteTable(table.id)
setPopOverOpen(false);
}
}, [table]);
const addField = () => {
const addField = useCallback(() => {
setPopOverOpen(false)
createField({
id: v4(),
@@ -53,30 +51,26 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
tableId: table.id,
sequence: getNextSequence(table.fields),
nullable: true,
typeId : getInteger()?.id
typeId: getInteger()?.id
})
}
}, [table, getInteger])
const addIndex = (event: any) => {
const addIndex = useCallback((event: any) => {
setPopOverOpen(false)
createIndex({
id: v4(),
name: `index_${table.indices.length + 1}`,
unique: true,
tableId: table.id
} as IndexInsertType);
}
}, [table])
const duplicate = async () => {
const duplicate = useCallback(async () => {
setPopOverOpen(false)
const clonedTable: TableType = cloneTable(table);
await createTable(clonedTable)
focusOnTable(clonedTable.id);
}
}, [table])
return (
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center border-l-[6px] border-default"
@@ -64,11 +64,10 @@ const TablesController: React.FC = ({ }) => {
useEffect(() => {
if (focusedTableId) {
setSelectedTable(new Set([focusedTableId]) as any);
setShowSqlPreview(false) ;
const accordionItem = document.getElementById(focusedTableId)
if (accordionItem)
accordionItem?.scrollIntoView({
behavior: 'smooth', block: 'center'
})
@@ -81,7 +81,7 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
placeholder={t("modals.db_name")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
}}
/>
</div>
@@ -106,16 +106,14 @@ export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
className="w-full text-font/90 border-1 border-divider"
>
<SquareMenu className="size-4" /> Check examples
<SquareMenu className="size-4 text-icon" /> Check examples
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
className="w-full text-font/90 border-1 border-divider"
>
<span className="underline">
Empty Diagram
@@ -71,9 +71,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
const addRelationship = useCallback(() => {
const id: string = v4();
createRelationship({
...relationship,
id,
@@ -109,7 +107,7 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
<div className="space-y-2">
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4" />
{t("db_controller.target_table")}
{t("db_controller.referenced_table")}
</label>
<Autocomplete
@@ -29,9 +29,8 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
className="min-w-[560px]"
actionHandler={onDelete}
variant="danger"
>
<p className="text-font/90">
<p className="text-font/90 text-sm font-medium">
{t("modals.delete_database_content")}
</p>
</Modal>
@@ -0,0 +1,31 @@
import Modal, { ModalProps } from "@/components/modal/modal";
import React from "react";
import { useTranslation } from "react-i18next";
import SqlPreview from "../db-controller/sql-preview";
const ExportSqlModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { t } = useTranslation();
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.export_sql")}
className="min-w-[960px] h-[90vh]"
header={t("modals.export_sql_header")}
>
<div className="h-full max-h-[72vh]">
<SqlPreview />
</div>
</Modal>
)
};
export default React.memo(ExportSqlModal)
+172 -100
View File
@@ -7,112 +7,120 @@ import { sql } from '@codemirror/lang-sql';
import { useTheme } from "next-themes";
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType, MARIADB_DUMP_EXAMPLE, MARIADB_DUMP_INSTRUCTIONS, MYSQL_DUMP_EXAMPLE, MYSQL_DUMP_INSTRUCTIONS, PG_DUMP_EXAMPLE, PG_DUMP_INSTRUCTIONS, SQLITE_DUMP_EXAMPLE, SQLITE_DUMP_INSRUCTION } from "@/lib/database";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { CheckboxGroup } from "@heroui/react";
import { Alert, CheckboxGroup, cn } from "@heroui/react";
import OptionCheckbox from "@/components/checkbox/option-checkbox";
import { Code } from "lucide-react";
import { Code as CodeSection } from "@heroui/react";
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
import Clipboard from "@/components/clipboard/clipboard";
import { Trans } from 'react-i18next';
import { Parser } from "node-sql-parser";
import { DatabaseInsertType } from "@/lib/schemas/database-schema";
import { Node, useReactFlow } from "@xyflow/react";
import { adjustTablesPositions } from "@/utils/tables";
import { TableInsertType } from "@/lib/schemas/table-schema";
const options: ImportDatabaseOption[] = [{
dialect: DatabaseDialect.POSTGRES,
methods: [{
id: "pg_dump",
name: "pg_dump",
icon: <Code className="size-3.5 " />,
type: ImportMethodType.DUMP,
instruction: PG_DUMP_INSTRUCTIONS,
example: PG_DUMP_EXAMPLE
}, {
id: "pg_admin",
name: "Pg Admin",
logo: "/postgresql_logo_small.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}, {
dialect: DatabaseDialect.MYSQL,
methods: [{
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MYSQL_DUMP_INSTRUCTIONS,
example: MYSQL_DUMP_EXAMPLE,
}, {
id: "workbench",
name: "MySQL Workbench",
logo: "/mysql_logo_small.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}
, {
dialect: DatabaseDialect.MARIADB,
methods: [{
id: "mariadb-dump",
name: "mariadb-dump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MARIADB_DUMP_INSTRUCTIONS,
example: MARIADB_DUMP_EXAMPLE,
}, {
id: "heidisql",
name: "HeidiSQL",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
logo: "/heidisqlL_logo.png",
}, {
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MYSQL_DUMP_INSTRUCTIONS,
example: MYSQL_DUMP_EXAMPLE,
}, {
id: "workbench",
name: "MySQL Workbench",
logo: "/mysql_logo_small.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}],
}, {
dialect: DatabaseDialect.SQLITE,
methods: [{
id: "sqlite3",
name: "Sqlite3",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: SQLITE_DUMP_INSRUCTION,
example: SQLITE_DUMP_EXAMPLE
}, {
id: "dbbrowser",
name: "DB Browser",
logo: "/dbbrowser.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}
];
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { t } = useTranslation();
const options: ImportDatabaseOption[] = useMemo(() => [{
dialect: DatabaseDialect.POSTGRES,
methods: [{
id: "pg_dump",
name: "pg_dump",
icon: <Code className="size-3.5 " />,
type: ImportMethodType.DUMP,
instruction: PG_DUMP_INSTRUCTIONS,
example: PG_DUMP_EXAMPLE
}, {
id: "pg_admin",
name: "Pg Admin",
logo: "/postgresql_logo.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}, {
dialect: DatabaseDialect.MYSQL,
methods: [{
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MYSQL_DUMP_INSTRUCTIONS,
example: MYSQL_DUMP_EXAMPLE,
}, {
id: "workbench",
name: "MySQL Workbench",
logo: "/mysql_logo.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}
, {
dialect: DatabaseDialect.MARIADB,
methods: [{
id: "mariadb-dump",
name: "mariadb-dump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MARIADB_DUMP_INSTRUCTIONS,
example: MARIADB_DUMP_EXAMPLE,
}, {
id: "heidisql",
name: "HeidiSQL",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
logo: "/heidisqlL_logo.png",
}, {
id: "mysql_dump",
name: "mysqldump",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: MYSQL_DUMP_INSTRUCTIONS,
example: MYSQL_DUMP_EXAMPLE,
}, {
id: "workbench",
name: "MySQL Workbench",
logo: "/mysql_logo.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}],
}, {
dialect: DatabaseDialect.SQLITE,
methods: [{
id: "sqlite3",
name: "Sqlite3",
icon: <Code className="size-3.5" />,
type: ImportMethodType.DUMP,
instruction: SQLITE_DUMP_INSRUCTION,
example: SQLITE_DUMP_EXAMPLE
}, {
id: "dbbrowser",
name: "DB Browser",
logo: "/dbbrowser.png",
type: ImportMethodType.DB_CLIENT,
numberOfInstructions: 5,
}]
}
], [t])
const { resolvedTheme } = useTheme();
const { database , isLoading , isSwitchingDatabase } = useDatabase();
const { data_types, importDatabase } = useDatabaseOperations();
const { database, isLoading, isSwitchingDatabase } = useDatabase();
const { data_types, importDatabase } = useDatabaseOperations();
const [sqlCode, setSqlCode] = useState<string>("");
const [parsedDatabase, setParsedDatabase] = useState<any | undefined>(undefined)
const [error, setError] = useState<boolean>(false);
const { fitView } = useReactFlow();
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
}, [database]);
const [selectedMethodId, setSelectedMethodId] = useState<string[]>([]);
useEffect(() => {
setSelectedMethodId(currentOption ? [currentOption.methods[0].id] : [])
}, [currentOption])
@@ -127,17 +135,56 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId?.[0]) as ImportDatabaseMethod;
}, [selectedMethodId, currentOption])
const validateSql = useCallback((code: string) => {
try {
const parsedDatabase = SqlToDatabase(code, data_types, database?.dialect as DatabaseDialect);
setParsedDatabase(parsedDatabase);
setError(false);
} catch (error) {
setError(true);
setParsedDatabase(undefined)
}
setSqlCode(code)
}, [database?.dialect, data_types])
const onImport = useCallback(async () => {
return new Promise(async (res, rej) => {
try {
const nodes: Node[] = parsedDatabase.tables.map((table: TableInsertType) => ({
id: table.id,
data: {
table
}
}))
const adjustedTables = await adjustTablesPositions(nodes, parsedDatabase.relationships);
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indices);
onOpenChange && onOpenChange(false);
setTimeout(() => {
fitView({
duration: 500
});
}, 300);
} catch (error) {
console.log(error)
setError(true);
rej()
}
})
}, [parsedDatabase]);
const { tables, relationships, indices } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect);
await importDatabase(tables, relationships, indices);
onOpenChange && onOpenChange(false);
}, [sqlCode, database?.dialect, data_types]);
if (isLoading || isSwitchingDatabase)
return ;
return;
return (
<Modal
@@ -147,6 +194,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
actionName={t("modals.import_database.import")}
className="min-w-[860px] max-w-[860px]"
actionHandler={onImport}
isDisabled={!parsedDatabase}
>
<div className="flex flex-col gap-4 ">
<div className="w-full ">
@@ -188,7 +236,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
<div>
{t("import.run_command")}
</div>
<CodeSection color="default" radius="md" className="selectable border-1 py-0.5 bg-default w-full text-font/90 flex items-center justify-between dark:border-divider" >
<CodeSection color="default" radius="md" className="selectable border-1 border-divider py-0.5 bg-background w-full text-font/90 flex items-center justify-between dark:border-divider dark:bg-transparent" >
{selectedImportMethod.instruction}
<Clipboard text={selectedImportMethod.instruction} />
</CodeSection>
@@ -196,7 +244,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
<div>
{t("import.example")}
</div>
<CodeSection color="default" radius="md" className="selectable border-1 py-0.5 bg-default w-full text-font/90 flex items-center justify-between dark:border-divider" >
<CodeSection color="default" radius="md" className="selectable border-1 border-divider py-0.5 bg-background w-full text-font/90 flex items-center justify-between dark:border-divider dark:bg-transparent" >
{selectedImportMethod.example}
<Clipboard text={selectedImportMethod.example} />
</CodeSection>
@@ -221,8 +269,8 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
<Trans i18nKey={`import.${selectedImportMethod.id}.step${index + 1}`} components={{
bold: <span className="font-medium" />,
code: <CodeSection
className="border-1 bg-default text-font/90 px-1 h-7 mx-0.5 dark:border-divider"
color="default"
className="border-1 bg-default text-font/90 px-1 h-7 mx-0.5 dark:border-divider "
color="primary"
size="sm"
/>
}} />
@@ -235,16 +283,40 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
}
</div>
<div className="flex flex-1 ">
<div className="flex flex-1 flex-col ">
<ReactCodeMirror
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
className={
cn("flex w-full border-1 rounded-md border-divider overflow-hidden ",
(error || parsedDatabase?.errors?.length > 0) ? "min-h-[360px] max-h-[360px]" : "min-h-[424px] max-h-[424px]"
)
}
extensions={[sql()]}
value={sqlCode}
onChange={setSqlCode}
onChange={validateSql}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
<div className="mt-4">
{
error &&
<Alert
color="danger"
description={t("modals.import_database.import_error_description")}
title={t("modals.import_database.import_error")}
variant={resolvedTheme == "dark" ? "solid" : "faded"}
/>
}
{
(parsedDatabase?.errors && parsedDatabase?.errors?.length > 0) &&
<Alert
color="warning"
description={t("modals.import_database.import_warning_description")}
title={t("modals.import_database.import_warning")}
variant={resolvedTheme == "dark" ? "solid" : "faded"}
/>
}
</div>
</div>
</div>
</Modal>
@@ -4,7 +4,7 @@ import { DatabaseType } from "@/lib/schemas/database-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Image, Selection, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@heroui/react";
import { Image, Pagination, Selection, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@heroui/react";
import { Key, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -16,14 +16,20 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
const { isOpen, onOpenChange } = props;
const { t } = useTranslation();
const { databases, currentDatabaseId } = useDatabase();
const { switchDatabase } = useDatabaseOperations();
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(
(() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined)
);
const openDatabase = () => {
switchDatabase(selectedDatabase.currentKey) ;
onOpenChange && onOpenChange(false) ;
switchDatabase(selectedDatabase.currentKey);
onOpenChange && onOpenChange(false);
}
const onSelectionChange = (selection: Selection) => {
if ((selection as any).currentKey != selectedDatabase?.currentKey)
setSelectedDatabase(selection)
}
return (
@@ -33,26 +39,29 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
onOpenChange={onOpenChange}
title={t("modals.open_database")}
actionName={t("modals.open")}
className="min-w-[860px]"
className="min-w-[820px]"
actionHandler={openDatabase}
header={t("modals.open_database_header")}
isDisabled={!selectedDatabase?.size}
>
<Table
isHeaderSticky
aria-label="Example static collection table"
color={"primary"}
selectionMode="single"
selectedKeys={selectedDatabase}
onSelectionChange={setSelectedDatabase}
onSelectionChange={onSelectionChange}
classNames={{
wrapper: "min-h-[360px] shadow-none border-1 border-divider rounded-sm",
th: "dark:bg-background",
tr: "hover:bg-default-100 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 "
wrapper: "shadow-sm border-1 border-divider dark:bg-background-100 rounded-sm",
th: "dark:bg-background font-bold text-center rounded-sm",
tr: " hover:bg-default-100 h-10 dark:hover:bg-background rounded-lg cursor-pointer text-font/90 transition-colors duration-200 ",
base: "max-h-[520px] overflow-y-scroll ",
td: "items-center text-center" ,
}}
>
<TableHeader className="rounded-sm">
<TableColumn >Dialect</TableColumn>
<TableColumn>Dialect</TableColumn>
<TableColumn>Name</TableColumn>
<TableColumn>Created at</TableColumn>
<TableColumn>Tables</TableColumn>
@@ -62,14 +71,16 @@ const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
databases.map((database: DatabaseType) => (
<TableRow key={database.id}>
<TableCell>
<Image
src={getDatabaseByDialect(database.dialect).small_logo}
width={24}
radius="none"
/>
<div className="flex justify-center">
<Image
src={getDatabaseByDialect(database.dialect).small_logo}
width={24}
radius="none"
/>
</div>
</TableCell>
<TableCell className="font-semibold">{database.name}</TableCell>
<TableCell>{
<TableCell className="font-semibold ">{database.name}</TableCell>
<TableCell className="text-font/70">{
new Date(database.createdAt as string).toLocaleString("en-US")
}</TableCell>
<TableCell>{database.numOfTables}</TableCell>
+23 -23
View File
@@ -123,37 +123,37 @@ const Field: React.FC<Props> = (props) => {
</Button>
</>
}
<div className={
cn(
"absolute w-full left-0 ",
!showHandles ? "invisible" : "visible"
)} >
<Handle
type="source"
position={Position.Left}
id={LEFT_PREFIX + field.id}
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
/>
<Handle
type="source"
position={Position.Right}
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
id={RIGHT_PREFIX + field.id}
/>
</div>
{
<div className={
cn(
"absolute w-full left-0 ",
!isConnectionInProgress ? "invisible" : "visible"
!showHandles ? "invisible" : "visible"
)} >
<Handle
type="source"
position={Position.Left}
id={LEFT_PREFIX + field.id}
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
/>
<Handle
type="source"
position={Position.Right}
className="w-4 h-4 border-4 bg-primary dark:border-background-50"
id={RIGHT_PREFIX + field.id}
/>
</div>
}
{
<div className={
cn(
"absolute w-full left-0 h-full ",
!isConnectionInProgress ? "invisible" : "visible"
)}>
<Handle
id={`${TARGET_PREFIX}${field.id}`}
className={
true
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
: `!invisible`
'absolute left-0 top-0 h-full w-full transform-none rounded-none border-none opacity-0'
}
position={Position.Left}
type="target"
+7 -10
View File
@@ -57,9 +57,10 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
}, [table])
const fields: React.ReactNode[] = useMemo(() => {
return table.fields.map((field: FieldType) => {
const highlight: boolean = highlightedEdges.find((edge: any) =>
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
(edge.data?.relationship as RelationshipType).targetFieldId == field.id) != null;
@@ -78,6 +79,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
setShowMore((previousShowMore) => !previousShowMore);
}, []);
return (
<Card className={cn(
@@ -183,21 +185,16 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
</div>
)}
</Card>
)
};
export default React.memo(Table , (previousState: any, newState: any) => {
export default React.memo(Table , (previousState: any, newState: any) => {
// compare the previous state to the new one just to prevent re-rendering when we drag a table
const previousStateHash: string = hash(previousState.data);
const newStateHash: string = hash(newState.data);
return previousStateHash == newStateHash && previousState.selected == newState.selected;
});
/*
,
*/
@@ -12,19 +12,28 @@ interface Props { children: React.ReactNode };
const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const udpateDbFlag = useRef(false);
const { database } = useDatabase() as { database: DatabaseType };
const { executeDbDiffOps } = useDatabaseOperations();
const { database , isLoading , isFetching } = useDatabase() ;
const { executeDbDiffOps } = useDatabaseOperations();
const [isProcessing, setIsProcessing] = useState<boolean>(false);
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo, reset }] = useUndo<DatabaseType>(database as DatabaseType);
useEffect(() => {
if (datatbaseState.present && database) {
if (database)
reset(database);
}, [database?.id]);
useEffect(() => {
if (database && datatbaseState.present && !isLoading && !isFetching) {
udpateDbFlag.current = false;
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
const presentHash: string | undefined = hash(datatbaseState.present, { algorithm: 'sha1' });
const databaseHash: string = hash(database, { algorithm: 'sha1' });
if (presentHash != databaseHash)
if (presentHash != databaseHash) {
set(database);
}
}
}, [database]);
@@ -50,13 +59,14 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
return;
}
if ( ! database || !datatbaseState.present )
return ;
if (!database || !datatbaseState.present)
return;
const normalizedDatabase = normalizeDatabase(database);
const normalizedPresent = normalizeDatabase(datatbaseState.present);
const differences = compare(normalizedDatabase, normalizedPresent);
//console.log ( differences )
if (differences && differences.length > 0) {
setIsProcessing(true);
@@ -65,11 +75,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
(async () => {
try {
// console.log ( operations) ;
await executeDbDiffOps(operations)
setIsProcessing(false);
}
catch (error) {
set(database);
console.log ( error ) ;
/// set(database);
setIsProcessing(false);
}
})()
@@ -22,9 +22,7 @@ interface DatabaseDataContextType {
isFetching : boolean ,
isSwitchingDatabase: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
}
@@ -42,7 +40,7 @@ interface DatabaseOperationsContextType {
createTable: (table: TableInsertType) => Promise<void>,
editTable: (table: TableInsertType) => Promise<QueryResult>,
deleteTable: (id: string) => Promise<void>,
updateTablePositions: (tables: TableInsertType[]) => Promise<void>,
updateTablePositions: (tables: TableInsertType[] ) => Promise<void>,
deleteMultiTables: (ids: string[]) => Promise<void>
// field operations
createField: (field: FieldInsertType) => Promise<QueryResult>,
@@ -64,7 +62,7 @@ interface DatabaseOperationsContextType {
executeDbDiffOps: (operations: DBDiffOperation[]) => void,
// insert databse tables , relationships , indices in one operation
importDatabase : (tables : TableInsertType[] , relationships : RelationshipInsertType[] , indices : IndexInsertType[]) => Promise<void> ;
importDatabase : (tables : TableInsertType[] , relationships : RelationshipInsertType[] , indices : IndexInsertType[]) => Promise<any> ;
}
@@ -5,7 +5,7 @@ import { TableInsertType, tables, TableType } from "@/lib/schemas/table-schema";
import { useQuery } from "@powersync/react";
import { toCompilableQuery } from "@powersync/drizzle-driver";
import { asc, count, desc, eq, inArray, or } from "drizzle-orm";
import { QueryResult, Transaction } from "@powersync/web";
import { QueryResult } from "@powersync/web";
import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType, relationships } from "@/lib/schemas/relationship-schema";
import DatabaseHistoryProvider from "../database-history/database-history-provider";
@@ -16,7 +16,7 @@ import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-schema";
import { v4 } from "uuid";
import { DataType } from "@/lib/schemas/data-type-schema";
import { Modifiers } from "@/lib/field";
import { deleteFieldsWithCascade, deleteTablesWithCascade } from "@/utils/cascade";
@@ -47,6 +47,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
type: true
}
},
indices: {
orderBy: asc(indices.createdAt),
with: {
@@ -55,6 +56,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
}
},
relationships: {
with: {
sourceTable: true,
@@ -66,7 +68,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
}
})
)
), [], { runQueryOnce: false },
);
@@ -84,6 +86,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
else
database = undefined as any;
// Fetch all data types
let { data: data_types, isLoading: loadingDataTypes, isFetching: fetchingDatatypes } = useQuery(toCompilableQuery(
db.query.data_types.findMany({
@@ -149,11 +152,13 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
);
}
})
});
} else {
throw Error("No Database selected");
}
}, [db, currentDatabaseId]);
}, [db, currentDatabaseId, database]);
const editTable = useCallback(async (table: TableInsertType): Promise<QueryResult> => {
return await db.update(tables).set(table).where(eq(tables.id, table.id));
@@ -162,9 +167,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const deleteTable = useCallback(async (id: string): Promise<void> => {
if (currentDatabaseId) {
await db.transaction(async (tx) => {
await tx.delete(tables).where(eq(tables.id, id));
await updateDbNumTables(currentDatabaseId, tx)
})
await deleteFieldsWithCascade([id], tx);
await updateDbNumTables(currentDatabaseId, tx);
});
} else {
throw Error("No Database selected");
}
@@ -174,9 +179,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const deleteMultiTables = useCallback(async (ids: string[]): Promise<void> => {
if (currentDatabaseId) {
await db.transaction(async (tx) => {
await tx.delete(tables).where(inArray(tables.id, ids));;
await updateDbNumTables(currentDatabaseId, tx)
})
await deleteTablesWithCascade(ids, tx);
await updateDbNumTables(currentDatabaseId, tx);
});
} else {
throw Error("No Database selected");
}
@@ -195,8 +200,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
// Delete field and its related relationships
const deleteField = useCallback(async (id: string): Promise<void> => {
return await db.transaction(async (tx) => {
await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, id), eq(relationships.targetFieldId, id)));
await tx.delete(fields).where(eq(fields.id, id));
await deleteFieldsWithCascade([id], tx);
})
}, [db]);
@@ -271,85 +275,131 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [db]);
// Update table positions (for UI layout)
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<void> => {
const updateTablePositions = useCallback(async (tableList: TableInsertType[]): Promise<any> => {
return await db.transaction(async (tx) => {
let operations: Promise<any>[] = [];
for (const table of tableList) {
await tx.update(tables).set({
operations.push(tx.update(tables).set({
posX: (table as any).posX,
posY: (table as any).posY
}).where(eq(tables.id, (table as any).id))
}).where(eq(tables.id, (table as any).id)))
}
return await Promise.all(operations);
})
}, [db]);
// Apply a list of diff operations to sync database
const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => {
const executeDbDiffOps = useCallback(async (diffOperations: DBDiffOperation[]) => {
try {
await db.transaction(async (tx) => {
for (const operation of operations) {
let operations: Promise<any>[] = [];
for (const operation of diffOperations) {
if (operation.type == "RENAME_DATABASE") {
currentDatabaseId && await tx.update(databaseModel).set({
name: operation.chnages.name
}).where(eq(databaseModel.id, currentDatabaseId));
if (currentDatabaseId)
operations.push(tx.update(databaseModel).set({
name: operation.chnages.name
}).where(eq(databaseModel.id, currentDatabaseId)));
}
else if (operation.type == "UPDATE_NUM_TABLES") {
console.log("update number of tables with ", operation.value);
if (currentDatabaseId)
operations.push(
tx.update(databaseModel).set({
numOfTables: operation.value
}).where(eq(databaseModel.id, currentDatabaseId))
)
}
else if (operation.type == "CREATE_TABLE") {
await tx.insert(tables).values(operation.table);
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
if (operation.table.fields && Object.values(operation.table.fields).length > 0)
await tx.insert(fields).values(Object.values(operation.table.fields));
operations.push(tx.insert(tables).values(operation.table));
if (operation.table.fields && Object.values(operation.table.fields).length > 0) {
operations.push(
tx.insert(fields).values(Object.values(operation.table.fields))
);
}
} else if (operation.type === "UPDATE_TABLE") {
await tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId));
operations.push(
tx.update(tables).set(operation.changes).where(eq(tables.id, operation.tableId))
)
} else if (operation.type === "DELETE_TABLE") {
operations.push(
deleteTablesWithCascade([operation.tableId], tx)
);
await tx.delete(tables).where(eq(tables.id, operation.tableId));
currentDatabaseId && await updateDbNumTables(currentDatabaseId, tx);
} else if (operation.type === "CREATE_FIELD") {
await tx.insert(fields).values(operation.field);
operations.push(
tx.insert(fields).values(operation.field)
);
} else if (operation.type === "DELETE_FIELD") {
await tx.delete(relationships).where(or(eq(relationships.sourceFieldId, operation.fieldId), eq(relationships.targetFieldId, operation.fieldId)));
await tx.delete(fields).where(eq(fields.id, operation.fieldId));
operations.push(
deleteFieldsWithCascade([operation.fieldId], tx)
)
} else if (operation.type === "UPDATE_FIELD") {
await tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId));
operations.push(
tx.update(fields).set(operation.changes).where(eq(fields.id, operation.fieldId))
);
} else if (operation.type === "CREATE_RELATIONSHIP") {
await tx.insert(relationships).values(operation.relationship);
operations.push(
tx.insert(relationships).values(operation.relationship)
);
} else if (operation.type === "DELETE_RELATIONSHIP") {
await tx.delete(relationships).where(eq(relationships.id, operation.relationshipId));
operations.push(
tx.delete(relationships).where(eq(relationships.id, operation.relationshipId))
);
} else if (operation.type === "UPDATE_RELATIONSHIP") {
await tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId));
operations.push(
tx.update(relationships).set(operation.changes).where(eq(relationships.id, operation.relationshipId))
);
}
else if (operation.type == "CREATE_INDEX") {
await tx.insert(indices).values(operation.index);
operations.push(
tx.insert(indices).values(operation.index)
);
if (operation.index.fieldIndices && Object.values(operation.index.fieldIndices).length > 0)
await tx.insert(field_indices).values(Object.values(operation.index.fieldIndices));
operations.push(
tx.insert(field_indices).values(Object.values(operation.index.fieldIndices))
);
}
else if (operation.type == "DELETE_INDEX") {
await tx.delete(indices).where(eq(indices.id, operation.indexId));
operations.push(
tx.delete(indices).where(eq(indices.id, operation.indexId))
);
}
else if (operation.type == "UPDATE_INDEX") {
await tx.update(indices).set(operation.changes).where(eq(indices.id, operation.indexId));
operations.push(
tx.update(indices).set(operation.changes).where(eq(indices.id, operation.indexId))
);
} else if (operation.type == "UPDATE_FIELD_INDICES") {
if (operation.delete.length > 0) {
await tx.delete(field_indices).where(inArray(field_indices.id, operation.delete))
operations.push(
tx.delete(field_indices).where(inArray(field_indices.id, operation.delete))
)
}
if (operation.create.length > 0) {
await tx.insert(field_indices).values(operation.create);
operations.push(
tx.insert(field_indices).values(operation.create)
);
}
}
}
return await Promise.all(operations);
})
} catch (error) {
console.log(error);
throw error
}
}, [db, currentDatabaseId]);
@@ -361,45 +411,49 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
if (currentDatabaseId) {
return await db.transaction(async (tx) => {
let operations: Promise<any>[] = [];
for (const table of importedTables) {
await tx.insert(tables).values({
operations.push(tx.insert(tables).values({
...table,
databaseId: currentDatabaseId,
createdAt: table.createdAt || getTimestamp()
} as TableInsertType);
await updateDbNumTables(currentDatabaseId, tx);
} as TableInsertType));
if (table.fields) {
await tx.insert(fields).values(
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
operations.push(
tx.insert(fields).values(
table.fields.map((field: FieldInsertType) => ({ ...field, tableId: table.id }))
)
);
}
}
for (const relationship of importedRelationships) {
await tx.insert(relationships).values({
operations.push(tx.insert(relationships).values({
...relationship,
databaseId: currentDatabaseId,
createdAt: relationship.createdAt || getTimestamp()
});
}));
}
for (const index of importedIndices) {
await tx.insert(indices).values({
operations.push(tx.insert(indices).values({
...index,
createdAt: index.createdAt ? index.createdAt : getTimestamp()
});
}));
if (index.fieldIndices) {
await tx.insert(field_indices).values(
operations.push(tx.insert(field_indices).values(
index.fieldIndices.map((fieldIndex: FieldIndexInsertType) => ({ ...fieldIndex, indexId: index.id }))
)
))
}
}
await Promise.all(operations);
await updateDbNumTables(currentDatabaseId, tx)
});
} else {
throw Error("no database selected")
}
}, [currentDatabaseId])
}, [db, currentDatabaseId])
const databaseOpsValue = useMemo(() => ({
isSwitchingDatabase,
@@ -457,11 +511,8 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
getInteger,
grouped_data_types
]);
return (
<DatabaseDataContext.Provider value={{
database: database as unknown as DatabaseType,
currentDatabaseId,
databases: databases as DatabaseType[],
@@ -471,23 +522,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
getField,
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
{
/* !database && !isLoading &&
<>
{children}
</>
}
{
database && !isLoading && !isSwitchingDatabase &&
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>*/
}
{
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>
}
<DatabaseHistoryProvider>
{children}
</DatabaseHistoryProvider>
</DatabaseOperationsContext.Provider>
</DatabaseDataContext.Provider>
)
@@ -1,3 +1,4 @@
import { CardinalityStyle } from "@/lib/database";
import { createContext, Dispatch } from "react";
@@ -10,7 +11,12 @@ interface DiagramOpsContextType {
focusOnTable: (id: string, transition?: boolean) => void,
focusOnRelationship: (id: string, transition?: boolean , withNavigate? : boolean) => void,
setIsConnectionInProgress: Dispatch<boolean>,
isConnectionInProgress: boolean
isConnectionInProgress: boolean ,
showController : boolean ,
setShowController : ( value : boolean ) => void ,
cardinalityStyle : CardinalityStyle ,
setCardinalityStyle : (style : CardinalityStyle) => void
}
@@ -4,6 +4,7 @@ import { useReactFlow } from "@xyflow/react";
import { useNavigate } from "react-router-dom";
import { RelationshipType } from "@/lib/schemas/relationship-schema";
import { DiagramDataContext, DiagramOpsContext } from "./diagram-context";
import { CardinalityStyle } from "@/lib/database";
@@ -16,6 +17,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
const [focusedTableId, setFocusedTableId] = useState<string | undefined>(undefined)
const [focusedRelationshipId, setFocusedRelationshipId] = useState<string | undefined>(undefined)
const [isConnectionInProgress, setIsConnectionInProgress] = useState<boolean>(false);
const [showController, setShowController] = useState<boolean>(true);
const [cardinalityStyle, setCardinalityStyle] = useState<CardinalityStyle>(CardinalityStyle.SYMBOLIC);
const focusOnTable = useCallback((id: string, transition: boolean = false) => {
navigate("/database/tables");
@@ -23,6 +26,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
setNodes((nodes) =>
nodes.map((node) => {
const selected: boolean = node.id === id;
if (selected && transition) {
fitView({
duration: 500,
@@ -33,6 +38,10 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
}],
});
}
if (node.selected == selected)
return node;
return {
...node,
selected,
@@ -46,11 +55,12 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
const focusOnRelationship = useCallback((id: string, transition: boolean = false, withNavigate: boolean = true) => {
if (withNavigate)
navigate("/database/relationships");
setFocusedRelationshipId(id);
setEdges((edges) =>
edges.map((edge) => {
const selected: boolean = edge.id === id;
if (selected && transition) {
@@ -65,6 +75,8 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
}]
});
}
if (edge.selected == selected)
return edge
return {
...edge,
selected
@@ -86,8 +98,13 @@ const DiagramProvider: React.FC<Props> = ({ children }) => {
focusOnRelationship,
setIsConnectionInProgress,
isConnectionInProgress,
showController,
setShowController,
cardinalityStyle,
setCardinalityStyle
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress])
}), [focusOnTable, focusOnRelationship, setIsConnectionInProgress, isConnectionInProgress, showController, setShowController, cardinalityStyle,
setCardinalityStyle])
return (
<DiagramDataContext.Provider
value={contextDatatValue}
@@ -6,7 +6,8 @@ export enum Modals {
CREATE_DATABASE = "CREATE_DATABASE" ,
OPEN_DATABASE = "OPEN_DATABASE" ,
DELETE_DATABASE = "DELETE_DATABASE" ,
IMPORT_DATABASE = "IMPOR_DATABASE"
IMPORT_DATABASE = "IMPOR_DATABASE" ,
EXPORT_SQL = "EXPORT_SQL"
}
interface ModalContextType {
@@ -8,6 +8,7 @@ import { CreateDatabaseModal } from "@/pages/database/modals/create-database-mod
import OpenDatabaseModal from "@/pages/database/modals/open-database-modal";
import DeleteDatabaseModal from "@/pages/database/modals/delete-database-modal";
import ImportDatabaseModal from "@/pages/database/modals/import-database";
import ExportSqlModal from "@/pages/database/modals/export-sql-modal";
interface Props { children: React.ReactNode }
@@ -57,7 +58,10 @@ export const ModalProvider: React.FC<Props> = ({ children }) => {
<DeleteDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
(currentModal.modal == Modals.IMPORT_DATABASE ) &&
<ImportDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
<ImportDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
(currentModal.modal == Modals.EXPORT_SQL ) &&
<ExportSqlModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
) : undefined
}
+7 -11
View File
@@ -10,14 +10,14 @@ import { PowerSyncSQLiteDatabase, wrapPowerSyncWithDrizzle } from '@powersync/dr
export const powerSyncDb = new PowerSyncDatabase({
database: {
dbFilename: 'stackrender.sqlite'
dbFilename: 'stackrender.sqlite',
},
schema: AppSchema,
});
export const db: PowerSyncSQLiteDatabase<typeof drizzleSchema> = wrapPowerSyncWithDrizzle(powerSyncDb, {
schema: drizzleSchema
schema: drizzleSchema,
});
const ConnectorContext = createContext<StackRenderConnector | null>(null);
@@ -33,17 +33,13 @@ export const SyncProvider: React.FC<SyncProviderProps> = ({ children }) => {
const [connector] = useState(new StackRenderConnector());
useEffect(() => {
powerSync.init();
powerSync.connect(connector);
(async () => {
const setup = async () => {
await powerSync.init();
await powerSync.execute("PRAGMA foreign_keys = ON;");
})();
powerSync.connect(connector);
};
setup();
}, [powerSync, connector])
return (
<Suspense fallback={<CircularProgress />}>
<PowerSyncContext.Provider value={powerSync}>
+3 -6
View File
@@ -85,6 +85,7 @@ div[data-slot="content"] hr[role="separator"] {
}
}
/* Target the scrollbar */
@@ -131,12 +132,7 @@ div[data-slot="content"] hr[role="separator"] {
.database-checkbox[data-selected="true"] {
border: 1.5px solid hsl(var(--heroui-primary-300));
}
thead[role="rowgroup"] th {
border-radius: 0px !important;
@@ -172,6 +168,7 @@ tbody[role="rowgroup"] tr td:first-child {
}
.cm-editor {
min-width: 100% !important;
+51
View File
@@ -0,0 +1,51 @@
import { fields } from "@/lib/schemas/field-schema";
import { QueryResult } from "@powersync/web";
import { ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
import { tables } from "@/lib/schemas/table-schema";
import { relationships } from "@/lib/schemas/relationship-schema";
import { field_indices } from "@/lib/schemas/field_index-schema";
import { indices } from "@/lib/schemas/index-schema";
export const deleteFieldsWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
return Promise.all([
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, ids), inArray(relationships.targetFieldId, ids))),
tx.delete(field_indices).where(inArray(field_indices.fieldId, ids)),
tx.delete(fields).where(inArray(fields.id, ids)),
]);
}
export const deleteTablesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
let fieldIds: { id: string }[] | string[] = await tx.select({
id: fields.id
}).from(fields).where(inArray(fields.tableId, ids));
let indicesIds: { id: string }[] | string[] = await tx.select({
id: indices.id
}).from(indices).where(inArray(indices.tableId, ids));
fieldIds = fieldIds.map((fieldId: { id: string }) => fieldId.id);
indicesIds = indicesIds.map((indexId: { id: string }) => indexId.id);
return Promise.all([
tx.delete(relationships).where(or(inArray(relationships.sourceFieldId, fieldIds), inArray(relationships.targetFieldId, fieldIds))),
tx.delete(field_indices).where(inArray(field_indices.fieldId, fieldIds)),
tx.delete(fields).where(inArray(fields.id, fieldIds)),
tx.delete(field_indices).where(inArray(field_indices.indexId, indicesIds)),
tx.delete(indices).where(inArray(indices.id, indicesIds)),
tx.delete(tables).where(inArray(tables.id, ids)),
])
}
export const deleteIndicesWithCascade = async (ids: string[], tx: SQLiteTransaction<'async', QueryResult, any, ExtractTablesWithRelations<any>>): Promise<QueryResult[]> => {
return Promise.all([
tx.delete(field_indices).where(inArray(field_indices.indexId, ids)),
tx.delete(indices).where(inArray(indices.id, ids))
])
}
+6 -3
View File
@@ -24,7 +24,7 @@ export type DBDiffOperation =
| { type: 'DELETE_INDEX'; tableId: string; indexId: string }
| { type: 'UPDATE_INDEX'; tableId: string; indexId: string; changes: Partial<IndexType> }
| { type: 'UPDATE_FIELD_INDICES'; tableId: string; indexId: string; create: FieldIndexType[]; delete: string[] }
| { type: "UPDATE_NUM_TABLES", value: number };
/**
* Convert a list of low-level JSON patch operations into higher-level database diff operations
@@ -58,6 +58,8 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
operations.push({ type: "RENAME_DATABASE", chnages: { name: op.value } as DatabaseInsertType })
}
if (op.op == "replace" && parts[0] == "numOfTables")
operations.push({ type: "UPDATE_NUM_TABLES", value: op.value })
// Handle table-related changes
else if (parts[0] == "tables") {
const tableId = parts[1];
@@ -209,8 +211,6 @@ export function mapDiffToDBDiffOperation(patch: any[]): DBDiffOperation[] {
}
}
// Index creates
for (const [tableId, indices] of Object.entries(indexCreates)) {
for (const index of indices) {
@@ -303,3 +303,6 @@ export function normalizeDatabase(db: DatabaseType): any {
)
};
}
+7 -1
View File
@@ -1,4 +1,9 @@
import { FieldType } from "@/lib/schemas/field-schema";
import { fields, FieldType } from "@/lib/schemas/field-schema";
import { field_indices } from "@/lib/schemas/field_index-schema";
import { relationships } from "@/lib/schemas/relationship-schema";
import { QueryResult } from "@powersync/web";
import { eq, ExtractTablesWithRelations, inArray, or } from "drizzle-orm";
import { SQLiteTransaction } from "drizzle-orm/sqlite-core";
import { v4 } from "uuid";
@@ -17,6 +22,7 @@ export const getNextSequence = (fields: FieldType[]): number => {
export const cloneField = (field: FieldType): FieldType => {
return {
...field,
+22 -15
View File
@@ -15,7 +15,8 @@ import { DatabaseInsertType } from "@/lib/schemas/database-schema";
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
const parser = new Parser();
let errors : Error[] = [] ;
const createTableStatements: string[] = [];
const alterTableStatements: string[] = [];
const createIndexStatements: string[] = [];
@@ -61,8 +62,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
postgresTypes.push(instructionAst[0]);
}
} catch (error) {
continue;
} catch (error ) {
errors.push(error as Error);
}
}
@@ -85,7 +86,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
}
}
} catch (error) {
continue;
errors.push(error as Error);
}
}
@@ -96,7 +98,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
indices.push(postgresAstToIndex(instructionAst[0], tables));
}
} catch (error) {
continue;
errors.push(error as Error);
}
}
for (const alterTable of alterTableStatements) {
@@ -107,6 +110,8 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
relationships = relationships.concat(extractedRelationships);
}
} catch (error) {
errors.push(error as Error);
if ((error as any).relationships && (error as any).relationships.length > 0)
relationships = relationships.concat((error as any).relationships);
}
@@ -129,13 +134,10 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
instructionAst = instructionAst[0];
}
console.log (instructionAst)
}
if (instructionAst) {
const table: TableInsertType = astToTable(instructionAst, data_types);
tables.push(table);
const tableForeignKeyConstraints = (instructionAst as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
const tableReferenceDefinitions = (instructionAst as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
@@ -153,16 +155,16 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
} catch (error) {
console.log(error)
errors.push(error as Error);
continue;
}
}
for (const foreignKeyConstraint of foreignKeyConstraints) {
try {
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
} catch (error) {
console.log(error);
} catch (error) {
errors.push(error as Error);
continue;
}
}
@@ -170,7 +172,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
try {
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
} catch (error) {
console.log(error);
errors.push(error as Error);
continue;
}
}
@@ -190,6 +192,7 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
}
} catch (error) {
errors.push(error as Error);
if ((error as any).relationships && (error as any).relationships.length > 0)
relationships = relationships.concat((error as any).relationships);
@@ -208,13 +211,17 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
indices.push(astToIndex(instructionAst, tables));
} catch (error) {
errors.push(error as Error);
continue;
}
}
}
if ( tables.length == 0 )
throw Error ("Error while Parsing")
return { tables, relationships, indices };
return { tables, relationships, indices , errors };
}
+4 -3
View File
@@ -52,9 +52,9 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
}
async uploadData(database: AbstractPowerSyncDatabase): Promise<void> {
const transaction = await database.getNextCrudTransaction();
if (!transaction) {
return;
}
@@ -64,6 +64,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
}
try {
let batch: any[] = [];
for (let operation of transaction.crud) {
@@ -102,7 +103,7 @@ export class StackRenderConnector implements PowerSyncBackendConnector {
localStorage.setItem("last_upload_at", new Date().toISOString());
window.dispatchEvent(new StorageEvent("storage", { key: "lastUploadAt" }));
} catch (ex: any) {
console.debug(ex);
throw ex;
+7 -2
View File
@@ -9,6 +9,7 @@ import { v4 } from "uuid";
import { getTimestamp } from "./utils";
import { SortableTable } from "@/lib/table";
const elk = new ELK();
const adjustTablesPositions = async (
@@ -27,12 +28,12 @@ const adjustTablesPositions = async (
'elk.algorithm': 'layered',
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
'elk.spacing.nodeNode': '80',
},
children: nodes.map((node) => ({
id: node.id,
width: node.measured?.width ?? 224,
height: node.measured?.height ?? 150,
height: node.measured?.height ?? ((node.data?.table as TableType)?.fields?.length * 32 + 36),
})),
edges: relationships.map((rel) => ({
id: `${rel.sourceTableId}->${rel.targetTableId}`,
@@ -195,6 +196,10 @@ function orderTables(tables: SortableTable[]): string[] {
}
export {
adjustTablesPositions,
getDefaultTableOverlapping,
+8 -2
View File
@@ -1,3 +1,5 @@
import { drizzleSchema } from "@/lib/schemas/app-schema";
const areArraysEqual = (a: string[], b: string[]): boolean => {
if (a.length !== b.length) return false;
@@ -64,11 +66,15 @@ function groupBy(array: any[], key: string) {
return acc;
}, {});
}
export {
areArraysEqual,
getTimestamp,
excludeFields,
groupBy
}
}
+52 -6
View File
@@ -6,8 +6,8 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
],
darkMode: "class",
@@ -32,8 +32,31 @@ export default {
100: "#1c2026"
},
danger: {
DEFAULT: "#F92814"
danger: {
DEFAULT: "#F92814",
50: '#fff1f0',
100: '#ffe0dd',
200: '#fcb8b1',
300: '#f78c81',
400: '#f46156',
500: '#F92814', // base
600: '#d01711',
700: '#a2120d',
800: '#760d09',
900: '#4a0805'
},
warning: {
DEFAULT: "#ffb400",
50: '#fffbea',
100: '#fff3c4',
200: '#ffe88a',
300: '#ffdc4f',
400: '#ffd01b',
500: '#ffb400', // base
600: '#cc9000',
700: '#a17100',
800: '#755200',
900: '#4a3400'
},
font: {
DEFAULT: "#cecfd2"
@@ -41,7 +64,7 @@ export default {
content1: {
DEFAULT: "#20252c"
},
default: {
50: '#f2f3f5',
100: '#d6d7db',
@@ -88,7 +111,30 @@ export default {
colors: {
danger: {
DEFAULT: "#F92814"
DEFAULT: "#F92814",
50: '#fff1f0',
100: '#ffe0dd',
200: '#fcb8b1',
300: '#f78c81',
400: '#f46156',
500: '#F92814', // base
600: '#d01711',
700: '#a2120d',
800: '#760d09',
900: '#4a0805'
},
warning: {
DEFAULT: "#ffb400",
50: '#fffbea',
100: '#fff3c4',
200: '#ffe88a',
300: '#ffdc4f',
400: '#ffd01b',
500: '#ffb400', // base
600: '#cc9000',
700: '#a17100',
800: '#755200',
900: '#4a3400'
},
font: {
DEFAULT: "#333639"
@@ -0,0 +1,25 @@
// vite.config.ts
import { defineConfig } from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite/dist/node/index.js";
import react from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/@vitejs/plugin-react/dist/index.mjs";
import tsconfigPaths from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-tsconfig-paths/dist/index.mjs";
import topLevelAwait from "file:///C:/Users/taman/OneDrive/Desktop/stackrender/stackrender/node_modules/vite-plugin-top-level-await/exports/import.mjs";
var vite_config_default = defineConfig({
plugins: [react(), tsconfigPaths(), topLevelAwait()],
server: {
port: 3e3
},
optimizeDeps: {
// Don't optimize these packages as they contain web workers and WASM files.
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
exclude: ["@journeyapps/wa-sqlite", "@powersync/web"],
include: ["@powersync/web > js-logger"]
},
worker: {
format: "es",
plugins: () => [topLevelAwait()]
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCJDOlxcXFxVc2Vyc1xcXFx0YW1hblxcXFxPbmVEcml2ZVxcXFxEZXNrdG9wXFxcXHN0YWNrcmVuZGVyXFxcXHN0YWNrcmVuZGVyXFxcXHZpdGUuY29uZmlnLnRzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9DOi9Vc2Vycy90YW1hbi9PbmVEcml2ZS9EZXNrdG9wL3N0YWNrcmVuZGVyL3N0YWNrcmVuZGVyL3ZpdGUuY29uZmlnLnRzXCI7aW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSdcbmltcG9ydCByZWFjdCBmcm9tICdAdml0ZWpzL3BsdWdpbi1yZWFjdCdcbmltcG9ydCB0c2NvbmZpZ1BhdGhzIGZyb20gJ3ZpdGUtdHNjb25maWctcGF0aHMnXG5pbXBvcnQgdG9wTGV2ZWxBd2FpdCBmcm9tICd2aXRlLXBsdWdpbi10b3AtbGV2ZWwtYXdhaXQnO1xuLy8gaHR0cHM6Ly92aXRlanMuZGV2L2NvbmZpZy9cbmV4cG9ydCBkZWZhdWx0IGRlZmluZUNvbmZpZyh7XG4gIHBsdWdpbnM6IFtyZWFjdCgpLCB0c2NvbmZpZ1BhdGhzKCkgLCB0b3BMZXZlbEF3YWl0KCldLFxuICBzZXJ2ZXIgOiB7IFxuICAgIHBvcnQgOiAzMDAwXG4gIH0gLCBcbiAgb3B0aW1pemVEZXBzOiB7XG4gICAgLy8gRG9uJ3Qgb3B0aW1pemUgdGhlc2UgcGFja2FnZXMgYXMgdGhleSBjb250YWluIHdlYiB3b3JrZXJzIGFuZCBXQVNNIGZpbGVzLlxuICAgIC8vIGh0dHBzOi8vZ2l0aHViLmNvbS92aXRlanMvdml0ZS9pc3N1ZXMvMTE2NzIjaXNzdWVjb21tZW50LTE0MTU4MjA2NzNcbiAgICBleGNsdWRlOiBbJ0Bqb3VybmV5YXBwcy93YS1zcWxpdGUnLCAnQHBvd2Vyc3luYy93ZWInXSxcbiAgICBpbmNsdWRlOiBbJ0Bwb3dlcnN5bmMvd2ViID4ganMtbG9nZ2VyJ11cbiAgfSxcbiAgd29ya2VyOiB7XG4gICAgZm9ybWF0OiAnZXMnLFxuICAgIHBsdWdpbnM6ICgpID0+IFsgdG9wTGV2ZWxBd2FpdCgpXVxuICB9XG59KVxuIl0sCiAgIm1hcHBpbmdzIjogIjtBQUFxVyxTQUFTLG9CQUFvQjtBQUNsWSxPQUFPLFdBQVc7QUFDbEIsT0FBTyxtQkFBbUI7QUFDMUIsT0FBTyxtQkFBbUI7QUFFMUIsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsU0FBUyxDQUFDLE1BQU0sR0FBRyxjQUFjLEdBQUksY0FBYyxDQUFDO0FBQUEsRUFDcEQsUUFBUztBQUFBLElBQ1AsTUFBTztBQUFBLEVBQ1Q7QUFBQSxFQUNBLGNBQWM7QUFBQTtBQUFBO0FBQUEsSUFHWixTQUFTLENBQUMsMEJBQTBCLGdCQUFnQjtBQUFBLElBQ3BELFNBQVMsQ0FBQyw0QkFBNEI7QUFBQSxFQUN4QztBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ04sUUFBUTtBQUFBLElBQ1IsU0FBUyxNQUFNLENBQUUsY0FBYyxDQUFDO0FBQUEsRUFDbEM7QUFDRixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo=