mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 03:05:42 +00:00
UX improvements + Manage loading states + Numerical Cardinalities implemented
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 184 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 264 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 200 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
+6
-4
@@ -23,11 +23,13 @@ function App() {
|
||||
<ReactFlowProvider>
|
||||
<DatabaseProvider>
|
||||
<DiagramProvider>
|
||||
<ModalProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<ModalProvider>
|
||||
{appRoutes}
|
||||
</TooltipProvider>
|
||||
</ModalProvider>
|
||||
</ModalProvider>
|
||||
</TooltipProvider>
|
||||
|
||||
</DiagramProvider>
|
||||
</DatabaseProvider>
|
||||
</ReactFlowProvider>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
import { Autocomplete as HeroUiAutocomplete, AutocompleteItem, AutocompleteSection } from "@heroui/react";
|
||||
import { Key } from "react";
|
||||
import { Key, useState } from "react";
|
||||
|
||||
interface AutocompleteProps {
|
||||
|
||||
@@ -16,12 +16,13 @@ interface AutocompleteProps {
|
||||
|
||||
|
||||
const headingClasses =
|
||||
"flex w-full py-1.5 px-2 bg-default shadow-small rounded-small";
|
||||
"flex w-full py-1.5 px-2 bg-default shadow-md border-1 font-medium rounded-md dark:border-font/5 dark:bg-background-100";
|
||||
|
||||
|
||||
const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSelectionChange, defaultSelection, placeholder, isDisabled, selectedItem, grouped = false }) => {
|
||||
|
||||
|
||||
|
||||
const onItemChange = (item: Key | null) => {
|
||||
onSelectionChange && onSelectionChange(item);
|
||||
}
|
||||
@@ -30,7 +31,7 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
|
||||
<HeroUiAutocomplete
|
||||
radius="sm"
|
||||
isDisabled={isDisabled}
|
||||
className="w-full"
|
||||
|
||||
defaultItems={items || []}
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
@@ -39,11 +40,15 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
selectedKey={selectedItem as any}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
clearButton: "text-icon",
|
||||
selectorButton: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
|
||||
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -55,35 +60,37 @@ const Autocomplete: React.FC<AutocompleteProps> = ({ items, label = "name", onSe
|
||||
return <HeroUiAutocomplete
|
||||
radius="sm"
|
||||
isDisabled={isDisabled}
|
||||
className="w-full"
|
||||
|
||||
size="sm"
|
||||
onSelectionChange={onItemChange}
|
||||
defaultSelectedKey={defaultSelection as any}
|
||||
variant="bordered"
|
||||
aria-label={placeholder}
|
||||
placeholder={placeholder}
|
||||
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
selectedKey={selectedItem as any}
|
||||
classNames={{
|
||||
clearButton: "text-icon",
|
||||
selectorButton: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
inputProps={{
|
||||
classNames: {
|
||||
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
|
||||
inputWrapper: "border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
},
|
||||
}}
|
||||
|
||||
>
|
||||
{
|
||||
items ? Object.keys(items).map((key: any) => (
|
||||
items ? Object.keys(items).map((key: string) => (
|
||||
<AutocompleteSection
|
||||
classNames={{
|
||||
classNames={{
|
||||
heading: headingClasses,
|
||||
}}
|
||||
title={key}
|
||||
|
||||
title={key.toUpperCase()}
|
||||
>
|
||||
{
|
||||
items[key].map((item: any) => (
|
||||
items[key as any].map((item: any) => (
|
||||
<AutocompleteItem key={item.id}>{item[label]}</AutocompleteItem>
|
||||
))
|
||||
}
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import { cn } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
export interface CardinalityMarkerProps {
|
||||
|
||||
selected?: boolean,
|
||||
direction?: "start" | "end",
|
||||
type: "one" | "many"
|
||||
cardinality: "one" | "many",
|
||||
type?: "symbole" | "numeric"
|
||||
}
|
||||
|
||||
|
||||
|
||||
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, type, direction = "start" }) => {
|
||||
const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false, cardinality, direction = "start", type = "symbole" }) => {
|
||||
|
||||
const id = `${type}_${direction}${selected ? "_selected" : ""}`;
|
||||
const id = `${cardinality}_${direction}${selected ? "_selected" : ""}`;
|
||||
const renderMarker = () => {
|
||||
if (type == "many") {
|
||||
|
||||
if (cardinality == "many") {
|
||||
if (direction == "start")
|
||||
return (<path d="M 0 50 L 100 50 M 100 50 L 0 0 M 100 50 L 0 100 " />)
|
||||
else if (direction == "end")
|
||||
@@ -19,7 +24,7 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
|
||||
}
|
||||
|
||||
if (type == "one") {
|
||||
if (cardinality == "one") {
|
||||
if (direction == "start") {
|
||||
return (<path d="M 0 50 L 100 50 M 50 50 M 50 50 M 75 25 L 75 75" />)
|
||||
}
|
||||
@@ -27,34 +32,78 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
return (<path d="M 100 50 L 0 50 M 50 50 M 50 50 M 25 25 L 25 75" />)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
return (
|
||||
<>
|
||||
|
||||
|
||||
if (type == "symbole")
|
||||
return (
|
||||
<>
|
||||
<marker
|
||||
id={id}
|
||||
markerWidth="24"
|
||||
markerHeight="24"
|
||||
refX="12"
|
||||
refY="12"
|
||||
orient="auto"
|
||||
markerUnits="userSpaceOnUse"
|
||||
>
|
||||
<svg
|
||||
fill="transparent"
|
||||
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
|
||||
strokeWidth="4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 100 100">
|
||||
{
|
||||
renderMarker()
|
||||
}
|
||||
</svg>
|
||||
</marker>
|
||||
</>
|
||||
)
|
||||
else if (type == "numeric") {
|
||||
return (
|
||||
<marker
|
||||
id={id}
|
||||
viewBox="0 0 24 24"
|
||||
markerWidth="24"
|
||||
markerHeight="24"
|
||||
refX="12"
|
||||
refX={direction == "start" ? "4" : "20"}
|
||||
refY="12"
|
||||
orient="auto"
|
||||
markerUnits="userSpaceOnUse"
|
||||
>
|
||||
<svg
|
||||
fill="transparent"
|
||||
className={selected ? "stroke-primary" : "stroke-default-600 dark:stroke-default-400"}
|
||||
strokeWidth="4"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 100 100">
|
||||
{
|
||||
renderMarker()
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="6"
|
||||
stroke-width="1"
|
||||
className={
|
||||
cn("dark:fill-default fill-default",
|
||||
selected ? " stroke-primary fill-background dark:fill-primary-900" : " stroke-default-600 dark:stroke-default-400"
|
||||
)
|
||||
}
|
||||
</svg>
|
||||
/>
|
||||
<text
|
||||
x="12"
|
||||
y="13"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-size="7"
|
||||
className={
|
||||
cn("fill-font/90 dark:fill-font/90" ,
|
||||
selected ? "fill-primary dark:fill-primary" : "" ,
|
||||
)
|
||||
}
|
||||
>
|
||||
{cardinality == "one" ? "I" : "N"}
|
||||
</text>
|
||||
</marker>
|
||||
</>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default CardinalityMarker;
|
||||
export default React.memo( CardinalityMarker);
|
||||
@@ -17,9 +17,11 @@ const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
|
||||
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 " ,
|
||||
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 "
|
||||
}}
|
||||
|
||||
|
||||
>
|
||||
<div className="min-w-full h-full flex items-center justify-center ">
|
||||
<Image
|
||||
|
||||
@@ -1,40 +1,47 @@
|
||||
import { ImportDatabaseMethod, ImportDatabaseOption } from "@/lib/database"
|
||||
import { Avatar, Checkbox, Chip } from "@heroui/react";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
import { Checkbox, Chip, cn, useCheckbox } from "@heroui/react";
|
||||
|
||||
interface OptionCheckboxProps {
|
||||
value: string;
|
||||
label: string ;
|
||||
label: string;
|
||||
icon?: React.ReactNode;
|
||||
logo?: string
|
||||
|
||||
logo?: string;
|
||||
isSelected?: boolean;
|
||||
}
|
||||
|
||||
|
||||
const OptionCheckbox: React.FC<OptionCheckboxProps> = ({ value, icon, logo , label }) => {
|
||||
const OptionCheckbox: React.FC<OptionCheckboxProps> = (props) => {
|
||||
const { value, icon, logo, label, isSelected } = props;
|
||||
|
||||
const variant = isSelected ? {
|
||||
variant: "flat",
|
||||
color: "primary"
|
||||
} : {
|
||||
variant: "bordered",
|
||||
color: "default"
|
||||
}
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
aria-label={value}
|
||||
value={value}
|
||||
size="sm"
|
||||
size="sm"
|
||||
className="option-checkbox"
|
||||
classNames={{
|
||||
wrapper: "hidden",
|
||||
label: "flex items-center justify-center w-full h-full"
|
||||
label: "flex items-center justify-center w-full h-full "
|
||||
}}
|
||||
>
|
||||
<Chip radius="sm" variant="bordered" color="default" className="option-span px-2 border-1 transition-all duration-300 border-divider"
|
||||
avatar={logo ? <Avatar src={logo} /> : undefined}
|
||||
|
||||
<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
|
||||
)}
|
||||
avatar={logo ? <img src={logo} /> : undefined}
|
||||
startContent={
|
||||
icon
|
||||
}
|
||||
>
|
||||
<span className="text-sm text-font font-semibold">
|
||||
<span className="text-sm font-medium">
|
||||
{label}
|
||||
</span>
|
||||
</Chip>
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button } from "@heroui/react";
|
||||
import { Copy, CopyCheck } from "lucide-react";
|
||||
|
||||
|
||||
|
||||
interface ClipboardProps {
|
||||
text?: string
|
||||
}
|
||||
|
||||
const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const [isCopied, setIsCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboard = useCallback(async () => {
|
||||
if (text)
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => {
|
||||
setIsCopied(false);
|
||||
}, 500)
|
||||
|
||||
} catch (err) {
|
||||
|
||||
setIsCopied(false);
|
||||
}
|
||||
|
||||
}, [text])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="bordered"
|
||||
className="text-font/90 border-divider"
|
||||
onPressEnd={copyToClipboard}
|
||||
>
|
||||
{
|
||||
!isCopied ? <Copy className="size-4" /> : <CopyCheck className="size-4" />
|
||||
|
||||
}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{!isCopied ? t("clipboard.copy") : t("clipboard.copied")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
};
|
||||
|
||||
export default React.memo(Clipboard)
|
||||
@@ -7,26 +7,14 @@ import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
interface MenuProps {
|
||||
}
|
||||
|
||||
const Menu: React.FC<MenuProps> = ({ }) => {
|
||||
const Menu: React.FC<any > = ({ }) => {
|
||||
|
||||
const { setTheme } = useTheme()
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { open } = useModal();
|
||||
|
||||
useEffect(() => {
|
||||
open(Modals.IMPORT_DATABASE)
|
||||
} , [])
|
||||
|
||||
const menu: MenuDropdownProps[] = useMemo(() => [
|
||||
{
|
||||
title: t("menu.file"),
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Spinner } from "@heroui/react";
|
||||
|
||||
|
||||
|
||||
|
||||
interface LoadingProps {
|
||||
text?: string;
|
||||
}
|
||||
|
||||
|
||||
export const Loading: React.FC<LoadingProps> = ({ text }) => {
|
||||
|
||||
return (
|
||||
<div className="fixed bg-overlay/50 backdrop-opacity-disabled w-screen h-screen fixed flex items-center justify-center z-[99] left-0 top-0 text-white flex-col gap-2">
|
||||
<Spinner size="lg" />
|
||||
<span className="font-medium">
|
||||
Loading ...
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,13 +8,14 @@ import {
|
||||
ModalFooter,
|
||||
Button,
|
||||
useDraggable,
|
||||
cn,
|
||||
} from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
export interface ModalProps {
|
||||
isOpen?: boolean,
|
||||
onOpenChange? : (open: boolean) => void,
|
||||
onOpenChange?: (open: boolean) => void,
|
||||
className?: string,
|
||||
backdrop?: "blur" | "transparent" | "opaque",
|
||||
title: string,
|
||||
@@ -23,10 +24,11 @@ export interface ModalProps {
|
||||
actionHandler?: () => void,
|
||||
isDisabled?: boolean,
|
||||
header?: string,
|
||||
variant?: "default" | "danger"
|
||||
variant?: "default" | "danger",
|
||||
closable?: boolean
|
||||
}
|
||||
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default" }) => {
|
||||
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled, variant = "default", closable = true }) => {
|
||||
|
||||
|
||||
const targetRef = React.useRef(null);
|
||||
@@ -39,17 +41,21 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
setIsLoading(true)
|
||||
actionHandler && await actionHandler();
|
||||
setIsLoading(false);
|
||||
onClose()
|
||||
}
|
||||
return (
|
||||
<HeroUiModal
|
||||
ref={targetRef}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange ? onOpenChange : undefined}
|
||||
onOpenChange={onOpenChange && closable ? onOpenChange : undefined}
|
||||
className={className}
|
||||
backdrop={backdrop}
|
||||
radius="sm"
|
||||
|
||||
classNames={{
|
||||
base: "dark:bg-background-100",
|
||||
closeButton: cn("dark:bg-background-100 dark:hover:bg-background rounded-md" , !closable ? "hidden" : "")
|
||||
}}
|
||||
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => (
|
||||
@@ -72,9 +78,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
{
|
||||
variant == "default" &&
|
||||
<>
|
||||
<Button color="danger" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
{
|
||||
closable &&
|
||||
<Button color="danger" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
@@ -83,9 +95,15 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
|
||||
{
|
||||
variant == "danger" &&
|
||||
<>
|
||||
<Button color="default" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
{
|
||||
closable &&
|
||||
<Button color="default" variant="light" onPress={onClose} size="sm">
|
||||
{t("modals.close")}
|
||||
</Button>
|
||||
}
|
||||
{
|
||||
!closable && <div></div>
|
||||
}
|
||||
<Button color="danger" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
|
||||
{actionName}
|
||||
</Button>
|
||||
|
||||
@@ -2,8 +2,11 @@ import { usePowerSync } from "@powersync/react";
|
||||
import { Wifi, WifiOff } from "lucide-react";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { cn, Divider, Progress } from "@heroui/react";
|
||||
import { addToast, cn, Divider, Progress } from "@heroui/react";
|
||||
import { differenceInHours, differenceInMinutes } from "date-fns";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
const ConnectionStatus: React.FC = () => {
|
||||
|
||||
@@ -11,6 +14,10 @@ const ConnectionStatus: React.FC = () => {
|
||||
const [syncStatus, setSyncStatus] = useState(powerSync.currentStatus);
|
||||
const [downloadProgress, setDownloadProgress] = useState<number | undefined>(undefined);
|
||||
const clearProgressHandler: any = useRef(undefined);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
|
||||
const [lastUploadAt, setLastUploadAt] = useState<Date | undefined>(() => {
|
||||
const last_upload_at = localStorage.getItem("last_upload_at");
|
||||
if (last_upload_at)
|
||||
@@ -65,20 +72,23 @@ const ConnectionStatus: React.FC = () => {
|
||||
const hours = differenceInHours(currentDate, lastUploadAt);
|
||||
|
||||
if (hours > 0 && hours < 24) {
|
||||
return `${hours} hour ago`
|
||||
return `${hours} ${t("connection_status.hour_ago")}`
|
||||
}
|
||||
if (minutes > 0 && minutes < 60)
|
||||
return `${minutes} min ago`
|
||||
return `${minutes} ${t("connection_status.min_ago")}`
|
||||
|
||||
if (hours > 24) {
|
||||
return `${lastUploadAt.toLocaleDateString()}`
|
||||
}
|
||||
}
|
||||
return "Just now";
|
||||
|
||||
}, [lastUploadAt]);
|
||||
return t("connection_status.just_now") ;
|
||||
|
||||
}, [lastUploadAt , t]);
|
||||
|
||||
const lastSyncedDate: string | undefined = useMemo(() => {
|
||||
return syncStatus.lastSyncedAt ? syncStatus.lastSyncedAt.toLocaleString("en-US") : undefined;
|
||||
}, [syncStatus.lastSyncedAt]);
|
||||
|
||||
return (
|
||||
<div className="flex items-center ">
|
||||
<Tooltip>
|
||||
@@ -89,10 +99,9 @@ const ConnectionStatus: React.FC = () => {
|
||||
syncStatus.connected ?
|
||||
<Wifi className="size-4 " /> :
|
||||
<WifiOff className="size-4 " />
|
||||
|
||||
}
|
||||
{
|
||||
syncStatus.connected ? "Online" : "Offline"
|
||||
syncStatus.connected ? t("connection_status.online") : t("connection_status.offline")
|
||||
}
|
||||
|
||||
</span>
|
||||
@@ -103,7 +112,7 @@ const ConnectionStatus: React.FC = () => {
|
||||
downloadProgress !== undefined ?
|
||||
<>
|
||||
<span className="text-font/90 w-[96px] truncate">
|
||||
Saving ...
|
||||
{t("connection_status.saving")} ...
|
||||
</span>
|
||||
|
||||
<Progress
|
||||
@@ -122,16 +131,19 @@ const ConnectionStatus: React.FC = () => {
|
||||
</>
|
||||
:
|
||||
<span className="text-font/70 truncate font-semibold" >
|
||||
Saved {deltaUploadTime}
|
||||
{t("connection_status.saved")} {deltaUploadTime}
|
||||
</span>
|
||||
|
||||
}
|
||||
</div>
|
||||
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="dark:bg-default-900">
|
||||
Last synced {syncStatus.lastSyncedAt?.toISOString()}
|
||||
</TooltipContent>
|
||||
{
|
||||
lastSyncedDate &&
|
||||
<TooltipContent >
|
||||
{t("connection_status.last_synced")} : {lastSyncedDate}
|
||||
</TooltipContent>
|
||||
}
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -42,13 +42,13 @@ const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<label
|
||||
className=" w-full text-editable truncate h-8 px-3 text-sm font-bold dark:text-white flex gap-4 items-center justify-center text-font/90 hover:underline"
|
||||
className=" w-full text-editable truncate h-8 px-3 text-sm font-bold dark:text-white flex gap-4 items-center justify-center text-font hover:underline"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
<Image
|
||||
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
|
||||
width={22}
|
||||
className=" rounded-none "
|
||||
src={getDatabaseByDialect(database.dialect as DatabaseDialect).small_logo}
|
||||
width={20}
|
||||
className="rounded-none "
|
||||
/>
|
||||
|
||||
{dbName}
|
||||
@@ -64,27 +64,25 @@ const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
|
||||
editMode &&
|
||||
<div className="flex justify-center items-center gap-4">
|
||||
<Image
|
||||
src={getDatabaseByDialect(database.dialect as DatabaseDialect).logo}
|
||||
width={32}
|
||||
src={getDatabaseByDialect(database.dialect as DatabaseDialect).small_logo}
|
||||
width={30}
|
||||
className=" rounded-none "
|
||||
/>
|
||||
|
||||
|
||||
<Input
|
||||
placeholder={database.name}
|
||||
type="text"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
variant="bordered"
|
||||
onChange={(event: any) => setDbName(event.target.value)}
|
||||
value={dbName}
|
||||
onBlur={saveDatabaseName}
|
||||
autoFocus
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none"
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
|
||||
input: "font-semibold text-black dark:text-font"
|
||||
}}
|
||||
|
||||
|
||||
/>
|
||||
<Button
|
||||
variant="solid"
|
||||
|
||||
@@ -48,9 +48,9 @@ const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange })
|
||||
<ReactTagInput
|
||||
tags={tags}
|
||||
classNames={{
|
||||
tag: "font-normal inline-block m-0.5 border-1 border-default rounded-full px-2 py-1 flex-row ",
|
||||
remove: " bg-default-900 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
|
||||
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-default-400 h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
|
||||
tag: "font-normal inline-block m-0.5 border-1 border-default dark:border-divider rounded-full px-2 py-1 flex-row ",
|
||||
remove: " bg-default-900 dark:bg-default-500 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
|
||||
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-primary h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
|
||||
tags : "max-h-[256px] overflow-auto "
|
||||
}}
|
||||
handleDelete={handleDelete}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
export const useRelationshipName = (relationship: RelationshipType) => {
|
||||
const { t } = useTranslation();
|
||||
const name: string = useMemo(() => {
|
||||
return getDefaultRelationshipName(relationship) ;
|
||||
}, [relationship, t])
|
||||
return {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const getDefaultRelationshipName = (relationship: RelationshipType) => {
|
||||
|
||||
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
|
||||
return "";
|
||||
return `${relationship.sourceTable?.name}_${relationship.sourceField?.name} - ${relationship.targetTable?.name}_${relationship.targetField?.name}_fk`
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { useEffect } from "react";
|
||||
import { getDefaultTableOverlapping } from "@/utils/tables";
|
||||
import hash from "object-hash";
|
||||
import hash from "object-hash";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
|
||||
|
||||
export const useTableToNode = (tables: TableType[]): void => {
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
const { setNodes } = useReactFlow();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const tableNodes = tables.map((table: TableType) => {
|
||||
@@ -31,20 +32,18 @@ export const useTableToNode = (tables: TableType[]): void => {
|
||||
}
|
||||
} 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])
|
||||
|
||||
+29
-1
@@ -64,7 +64,7 @@ export const ar = {
|
||||
}
|
||||
},
|
||||
table: {
|
||||
double_click: "انقر مزدوجًا للتعديل" ,
|
||||
double_click: "انقر مزدوجًا للتعديل",
|
||||
overlapping_tables: "الجداول المتداخلة",
|
||||
},
|
||||
control_buttons: {
|
||||
@@ -104,6 +104,34 @@ export const ar = {
|
||||
help: "مساعدة",
|
||||
show_docs: "عرض المستندات",
|
||||
join_discord: "الانضمام إلى ديسكورد"
|
||||
},
|
||||
connection_status: {
|
||||
online: "متصل",
|
||||
offline: "غير متصل",
|
||||
saving: "جارٍ الحفظ",
|
||||
saved: "تم الحفظ",
|
||||
last_synced: "آخر مزامنة"
|
||||
},
|
||||
import: {
|
||||
instructions: "التعليمات",
|
||||
install: "التثبيت",
|
||||
run_command: "قم بتشغيل الأمر التالي في الطرفية.",
|
||||
example: "مثال",
|
||||
copy_code: "انسخ محتوى ملف .sql إلى قسم الكود أدناه.",
|
||||
pg_admin: {
|
||||
step1: "افتح <bold>Pg Admin</bold>.",
|
||||
step2: "انقر بزر الفأرة الأيمن على قاعدة البيانات واختر <bold>Backup</bold> من القائمة السياقية.",
|
||||
step3: "قم بتسمية ملف <code>.sql</code>، ثم اختر التنسيق <bold>Plain</bold>، وحدد <bold>Encoding: UTF8</bold>.",
|
||||
step4: "تأكد من أن <bold>Only schema</bold> محددة، و<bold>Only data</bold> غير محددة، وذلك في علامة تبويب <bold>Data Options</bold>.",
|
||||
step5: "انقر على <bold>Backup</bold> لتصدير الملف، ثم انسخ محتواه إلى قسم محرر الكود أدناه."
|
||||
},
|
||||
workbench: {
|
||||
step1: "افتح <bold>MySQL Workbench</bold> و<bold>اتصل</bold> بخادم MySQL الخاص بك.",
|
||||
step2: "من القائمة العلوية، انتقل إلى <bold>Server > Data Export</bold>.",
|
||||
step3: "في قسم <bold>خيارات التصدير</bold>، اختر <bold>Dump Structure Only</bold>.",
|
||||
step4: "حدد <bold>Export to Self-Contained File</bold>، ثم اختر مكان الحفظ واسم ملف الإخراج <code>.sql</code>.",
|
||||
step5: "انقر على <bold>Start Export</bold> لبدء عملية التصدير، ثم انسخ محتوى الملف إلى قسم محرر الكود أدناه."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+97
-43
@@ -56,43 +56,43 @@ export const en = {
|
||||
title: "Field Setting",
|
||||
unique: "Unique",
|
||||
unsigned: "Unsigned",
|
||||
numeric_setting : "Numeric Setting" ,
|
||||
decimal_setting : "Decimal Setting" ,
|
||||
numeric_setting: "Numeric Setting",
|
||||
decimal_setting: "Decimal Setting",
|
||||
zeroFill: "Zero Fill",
|
||||
autoIncrement: "Auto Increment",
|
||||
note: "Note",
|
||||
|
||||
delete_field: "Delete Field",
|
||||
field_note: "Field note",
|
||||
precision : "Precision" ,
|
||||
text_setting : "Text Setting" ,
|
||||
charset : "Charset" ,
|
||||
collation : "Collation" ,
|
||||
scale : "Scale" ,
|
||||
max_length : "Max length" ,
|
||||
integer_width : "Integer Width" ,
|
||||
width : "width" ,
|
||||
default_value : "Default value" ,
|
||||
value : "Value" ,
|
||||
length : "Length" ,
|
||||
values : "Values" ,
|
||||
type_enter : "Type and press enter" ,
|
||||
precision_def : "Total digits allowed (before + after the decimal)." ,
|
||||
scale_def : "Digits allowed after the decimal." ,
|
||||
time_default_value : {
|
||||
no_value : "No value" ,
|
||||
custom : "Custom time" ,
|
||||
now : "Now"
|
||||
} ,
|
||||
errors : {
|
||||
max_length : "must be positive number, no decimals." ,
|
||||
integer_default_value : "Invalid default value for Integer" ,
|
||||
precision : "Precision must be positive number, no decimals." ,
|
||||
scale : "Scale must be positive number, no decimals." ,
|
||||
scale_max_value : "Scale must be ≤ precision."
|
||||
|
||||
} ,
|
||||
pick_value : "Pick value"
|
||||
precision: "Precision",
|
||||
text_setting: "Text Setting",
|
||||
charset: "Charset",
|
||||
collation: "Collation",
|
||||
scale: "Scale",
|
||||
max_length: "Max length",
|
||||
integer_width: "Integer Width",
|
||||
width: "width",
|
||||
default_value: "Default value",
|
||||
value: "Value",
|
||||
length: "Length",
|
||||
values: "Values",
|
||||
type_enter: "Type and press enter",
|
||||
precision_def: "Total digits allowed (before + after the decimal).",
|
||||
scale_def: "Digits allowed after the decimal.",
|
||||
time_default_value: {
|
||||
no_value: "No value",
|
||||
custom: "Custom time",
|
||||
now: "Now"
|
||||
},
|
||||
errors: {
|
||||
max_length: "must be positive number, no decimals.",
|
||||
integer_default_value: "Invalid default value for Integer",
|
||||
precision: "Precision must be positive number, no decimals.",
|
||||
scale: "Scale must be positive number, no decimals.",
|
||||
scale_max_value: "Scale must be ≤ precision."
|
||||
|
||||
},
|
||||
pick_value: "Pick value"
|
||||
},
|
||||
delete: "Delete",
|
||||
|
||||
@@ -111,13 +111,13 @@ export const en = {
|
||||
invalid_relationship: {
|
||||
title: "Invalid Relationship",
|
||||
description: "The source key type does not match the referenced key type. Please ensure both keys have the same data type."
|
||||
} ,
|
||||
circular_dependency : {
|
||||
title : "Circular Dependency Detected" ,
|
||||
toast_description : "A circular reference between tables was found. Check the diagram on the left and remove one of the relationships to fix it." ,
|
||||
description : "Your schema contains a circular foreign key relationship between tables. To fix it" ,
|
||||
suggestion : "remove one of the relationships listed below that are causing the cycle." ,
|
||||
remove_relationship : "Remove relationship"
|
||||
},
|
||||
circular_dependency: {
|
||||
title: "Circular Dependency Detected",
|
||||
toast_description: "A circular reference between tables was found. Check the diagram on the left and remove one of the relationships to fix it.",
|
||||
description: "Your schema contains a circular foreign key relationship between tables. To fix it",
|
||||
suggestion: "remove one of the relationships listed below that are causing the cycle.",
|
||||
remove_relationship: "Remove relationship"
|
||||
}
|
||||
},
|
||||
table: {
|
||||
@@ -180,13 +180,67 @@ export const en = {
|
||||
open_database_header: "Open a database by selecting one from the list.",
|
||||
delete_database: "Delete Database",
|
||||
delete_database_content: "This action is irreversible and will permanently remove the diagram.",
|
||||
delete: "Delete" ,
|
||||
delete: "Delete",
|
||||
|
||||
import_database : {
|
||||
title : "Import your Database" ,
|
||||
import : "Import" ,
|
||||
import_options : "Would you like to import using :"
|
||||
import_database: {
|
||||
title: "Import your Database",
|
||||
import: "Import",
|
||||
import_options: "Would you like to import using :"
|
||||
}
|
||||
},
|
||||
clipboard: {
|
||||
copy: "Copy",
|
||||
copied: "Copied"
|
||||
},
|
||||
|
||||
connection_status : {
|
||||
online: "Online" ,
|
||||
offline : "Offline" ,
|
||||
saving : "Saving" ,
|
||||
saved : "Saved" ,
|
||||
last_synced : "Last synced" ,
|
||||
min_ago : "min ago" ,
|
||||
hour_ago : "hour ago" ,
|
||||
just_now : "Just now"
|
||||
},
|
||||
import: {
|
||||
instructions: "Instructions",
|
||||
install: "Install",
|
||||
run_command: "Run the following command in your terminal.",
|
||||
example: "Example",
|
||||
copy_code: "Copy the content of .sql file in code section below.",
|
||||
pg_admin: {
|
||||
"step1": "Open <bold>Pg Admin</bold>.",
|
||||
"step2": "Right-click your database and select <bold>Backup</bold> from the context menu.",
|
||||
"step3": "Name your <code>.sql</code> file, set Format to <bold>Plain</bold>, and choose <bold>Encoding: UTF8.</bold>",
|
||||
"step4": "Make sure <bold>Only schema</bold> is checked and <bold>Only data</bold> is unchecked in the <bold>Data Options tab</bold>.",
|
||||
"step5": "Click <bold>Backup</bold> to export the file, then copy its content into the code editor section below."
|
||||
},
|
||||
workbench: {
|
||||
"step1": "Open <bold>MySQL Workbench</bold> and <bold>connect</bold> to your MySQL server.",
|
||||
"step2": "In the top menu, go to <bold>Server > Data Export</bold>.",
|
||||
"step3": "In the <bold>Export Options</bold>, choose <bold>Dump Structure Only</bold>.",
|
||||
"step4": "Check <bold>Export to Self-Contained File</bold>, then choose a location and enter a name for the output <code>.sql</code> file.",
|
||||
"step5": "Click <bold>Start Export</bold> to begin the export process. Then, copy its content into the code editor section below."
|
||||
|
||||
},
|
||||
heidisql: {
|
||||
"step1": "<bold>Open HeidiSQL</bold> and connect to your server.",
|
||||
"step2": "In the <bold>left sidebar, right-click</bold> on the database you want to export.",
|
||||
"step3": "Choose <bold>Export database as SQL</bold> from the context menu.",
|
||||
"step4": "Select <bold>No data</bold> and make sure <bold>Create</bold> is checked to export the table structure only.",
|
||||
"step5": "Click the <bold>Export</bold> button, and finally copy the content of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
||||
|
||||
},
|
||||
dbbrowser: {
|
||||
step1: "Launch <bold>DB Browser for SQLite</bold>.",
|
||||
step2: "Click <bold>File > Open Database</bold> and select your <code>.sqlite</code> or <code>.db</code> file.",
|
||||
step3: "Go to <bold>File > Export > Database to SQL file</bold> from the top menu.",
|
||||
step4: "In the dialog, choose <bold>Export schema only</bold> and click <bold>Save</bold>.",
|
||||
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
||||
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+23
-1
@@ -65,7 +65,7 @@ export const fr = {
|
||||
}
|
||||
},
|
||||
table: {
|
||||
double_click: "Double-cliquez pour éditer" ,
|
||||
double_click: "Double-cliquez pour éditer",
|
||||
overlapping_tables: "Tables qui se chevauchent",
|
||||
},
|
||||
control_buttons: {
|
||||
@@ -105,6 +105,28 @@ export const fr = {
|
||||
help: "Aide",
|
||||
show_docs: "Afficher la documentation",
|
||||
join_discord: "Rejoindre Discord"
|
||||
},
|
||||
|
||||
import: {
|
||||
instructions: "Instructions",
|
||||
install: "Installer",
|
||||
run_command: "Exécutez la commande suivante dans votre terminal.",
|
||||
example: "Exemple",
|
||||
copy_code: "Copiez le contenu du fichier .sql dans la section de code ci-dessous.",
|
||||
pg_admin: {
|
||||
step1: "Ouvrez <bold>Pg Admin</bold>.",
|
||||
step2: "Faites un clic droit sur votre base de données et sélectionnez <bold>Sauvegarder</bold> dans le menu contextuel.",
|
||||
step3: "Nommez votre fichier <code>.sql</code>, définissez le format sur <bold>Plain</bold> et choisissez <bold>Encodage : UTF8.</bold>",
|
||||
step4: "Assurez-vous que <bold>Only schema</bold> est coché et que <bold>Only data</bold> ne l'est pas dans l'onglet <bold>Data Options</bold>.",
|
||||
step5: "Cliquez sur <bold>Sauvegarder</bold> pour exporter le fichier, puis copiez son contenu dans la section de l'éditeur de code ci-dessous."
|
||||
},
|
||||
workbench: {
|
||||
step1: "Ouvrez <bold>MySQL Workbench</bold> et <bold>connectez-vous</bold> à votre serveur MySQL.",
|
||||
step2: "Dans le menu du haut, allez dans <bold>Server > Data Export</bold>.",
|
||||
step3: "Dans les <bold>Options d'exportation</bold>, choisissez <bold>Dump Structure Only</bold>.",
|
||||
step4: "Cochez <bold>Export to Self-Contained File</bold>, puis choisissez un emplacement et donnez un nom au fichier <code>.sql</code> de sortie.",
|
||||
step5: "Cliquez sur <bold>Démarrer l'exportation</bold> pour lancer le processus. Ensuite, copiez son contenu dans la section de l'éditeur de code ci-dessous."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+13
-13
@@ -1,5 +1,4 @@
|
||||
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { EditorView } from '@uiw/react-codemirror';
|
||||
|
||||
export const colorOptions = [
|
||||
@@ -26,29 +25,30 @@ export const randomColor = () => {
|
||||
|
||||
|
||||
export const overrideDarkTheme = EditorView.theme({
|
||||
|
||||
|
||||
'.cm-content': {
|
||||
backgroundColor: "#20252c"
|
||||
backgroundColor: "#20252c" ,
|
||||
},
|
||||
".cm-gutter": {
|
||||
backgroundColor: "#20252c",
|
||||
},
|
||||
},
|
||||
|
||||
".cm-gutterElement": {
|
||||
color: "#4b515a"
|
||||
},
|
||||
|
||||
".ͼp": {
|
||||
|
||||
".ͼp": {
|
||||
color: "#A994FF"
|
||||
},
|
||||
|
||||
".cm-line .ͼq": {
|
||||
color : "#ff6363"
|
||||
} ,
|
||||
".ͼu" : {
|
||||
color : "#B6E672"
|
||||
} ,
|
||||
color: "#ff6363"
|
||||
},
|
||||
".ͼu": {
|
||||
color: "#B6E672"
|
||||
},
|
||||
".ͼv": {
|
||||
color : "#6cdcc4"
|
||||
color: "#6cdcc4"
|
||||
}
|
||||
|
||||
}, { dark: true });
|
||||
@@ -73,5 +73,5 @@ export const overrideLightTheme = EditorView.theme({
|
||||
".cm-line": {
|
||||
color: "#333639"
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
+48
-23
@@ -2,7 +2,8 @@
|
||||
export interface DatabaseType {
|
||||
name: string,
|
||||
dialect: string;
|
||||
logo: string
|
||||
logo: string;
|
||||
small_logo?: string
|
||||
}
|
||||
|
||||
export enum DatabaseDialect {
|
||||
@@ -16,47 +17,71 @@ export const DBTypes: DatabaseType[] = [
|
||||
{
|
||||
name: "Postgresql",
|
||||
dialect: DatabaseDialect.POSTGRES,
|
||||
logo: "/postgresql_logo.png"
|
||||
logo: "/postgresql_logo.png",
|
||||
small_logo: "/postgresql_logo_small.png"
|
||||
}, {
|
||||
name: "MySQL",
|
||||
dialect: DatabaseDialect.MYSQL,
|
||||
logo: "/mysql_logo.png"
|
||||
logo: "/mysql_logo.png",
|
||||
|
||||
small_logo: "/mysql_logo_small.png"
|
||||
},
|
||||
{
|
||||
name: "Sqlite",
|
||||
dialect: DatabaseDialect.SQLITE,
|
||||
logo: "/sqlite_logo.png"
|
||||
logo: "/sqlite_logo.png",
|
||||
small_logo: "/sqlite_logo_small.png"
|
||||
|
||||
},
|
||||
{
|
||||
{
|
||||
name: "MariaDB",
|
||||
dialect: DatabaseDialect.MARIADB,
|
||||
logo: "/mariadb_logo.png"
|
||||
logo: "/mariadb_logo.png",
|
||||
small_logo: "/mariadb_logo_small.png"
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
export const getDatabaseByDialect = (dialect: DatabaseDialect): DatabaseType => {
|
||||
const dbType : DatabaseType | undefined = DBTypes.find((dbType : DatabaseType) => dbType.dialect == dialect) ;
|
||||
return dbType ? dbType : DBTypes[0] ;
|
||||
const dbType: DatabaseType | undefined = DBTypes.find((dbType: DatabaseType) => dbType.dialect == dialect);
|
||||
return dbType ? dbType : DBTypes[0];
|
||||
}
|
||||
|
||||
|
||||
export enum ImportMethodType {
|
||||
DUMP = "DUMP" ,
|
||||
DB_CLIENT = "DB_CLIENT" ,
|
||||
DUMP = "DUMP",
|
||||
DB_CLIENT = "DB_CLIENT",
|
||||
|
||||
}
|
||||
|
||||
export interface ImportDatabaseMethod {
|
||||
id : string ;
|
||||
name : string ;
|
||||
logo? : string ;
|
||||
icon? : React.ReactNode ;
|
||||
instructions? : string ;
|
||||
type : ImportMethodType ;
|
||||
}
|
||||
|
||||
export interface ImportDatabaseOption {
|
||||
dialect : DatabaseDialect ,
|
||||
methods : ImportDatabaseMethod []
|
||||
}
|
||||
export interface ImportDatabaseMethod {
|
||||
id: string;
|
||||
name: string;
|
||||
logo?: string;
|
||||
icon?: React.ReactNode;
|
||||
instruction?: string;
|
||||
example?: string;
|
||||
type: ImportMethodType;
|
||||
numberOfInstructions?: number;
|
||||
}
|
||||
|
||||
export interface ImportDatabaseOption {
|
||||
dialect: DatabaseDialect,
|
||||
methods: ImportDatabaseMethod[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const PG_DUMP_INSTRUCTIONS = "pg_dump -U [username] -d [database_name] -f [output_file.sql]";
|
||||
export const PG_DUMP_EXAMPLE = "pg_dump -U root -d example_db -f example_db.sql";
|
||||
|
||||
export const MYSQL_DUMP_INSTRUCTIONS = "mysqldump -u [username] -p --no-data [database_name] > [output_file.sql]";
|
||||
export const MYSQL_DUMP_EXAMPLE = "mysqldump -u root -p --no-data example_db > example_db.sql";
|
||||
|
||||
|
||||
export const MARIADB_DUMP_INSTRUCTIONS = "mariadb-dump -u [username] -p --no-data [database_name] > [output_file.sql]";
|
||||
export const MARIADB_DUMP_EXAMPLE = "mariadb-dump -u root -p --no-data example_db > example_db.sql";
|
||||
|
||||
|
||||
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";
|
||||
@@ -16,8 +16,11 @@ interface DbControlButtons {
|
||||
const ZOOM_DURATION = 100
|
||||
|
||||
const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions }) => {
|
||||
const { zoomIn, zoomOut, fitView } = useReactFlow();
|
||||
const [zoom, setZoom] = useState<string>();
|
||||
const { zoomIn, zoomOut, fitView , getZoom} = useReactFlow();
|
||||
console.log ()
|
||||
const [zoom, setZoom] = useState<string>(
|
||||
`${Math.round(getZoom() * 100)}%`
|
||||
);
|
||||
const { t } = useTranslation();
|
||||
const { undo, redo, canRedo, canUndo } = useDatabaseHistory();
|
||||
|
||||
@@ -71,7 +74,7 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
>
|
||||
<Undo className={cn(
|
||||
"size-4 dark:text-white",
|
||||
!canUndo ? "text-font/30" : ""
|
||||
!canUndo ? "text-font/30 dark:text-font/30" : ""
|
||||
)} />
|
||||
</Button>
|
||||
</span>
|
||||
@@ -185,14 +188,13 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
>
|
||||
<Redo className={cn(
|
||||
"size-4 dark:text-white",
|
||||
!canRedo ? "text-font/30" : ""
|
||||
!canRedo ? "text-font/30 dark:text-font/30" : ""
|
||||
)} />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("control_buttons.redo")}
|
||||
|
||||
<span className="ml-2 text-default-400">
|
||||
Ctnl + Shift + Z
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Importing necessary types and hooks from React Flow (XYFlow)
|
||||
import {
|
||||
addEdge, Background, ColorMode, Connection, Controls, EdgeChange,
|
||||
EdgeRemoveChange, NodeChange, NodePositionChange,
|
||||
EdgeRemoveChange, MiniMap, NodeChange, NodePositionChange,
|
||||
NodeRemoveChange, OnEdgesChange, OnNodesChange,
|
||||
ReactFlow, useEdgesState, useNodesState, useReactFlow
|
||||
} from "@xyflow/react";
|
||||
@@ -40,22 +40,31 @@ import { useTheme } from "next-themes";
|
||||
|
||||
import { getRelationshipSourceAndTarget } from "@/utils/relationship";
|
||||
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";
|
||||
|
||||
|
||||
|
||||
const DatabasePage: React.FC = () => {
|
||||
const { open } = useModal();
|
||||
const syncStatus = usePowerSync();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
const { resolvedTheme } = useTheme();
|
||||
// Extract database state and operations
|
||||
const { database, getField } = useDatabase();
|
||||
const { database, getField, databases, isSwitchingDatabase, isLoading, isFetching } = useDatabase();
|
||||
|
||||
const { updateTablePositions, deleteMultiTables, deleteMultiRelationships, createRelationship } = useDatabaseOperations();
|
||||
|
||||
// Node and edge state hooks
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
||||
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress } = useDiagramOps();
|
||||
|
||||
@@ -69,6 +78,41 @@ const DatabasePage: React.FC = () => {
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const edgeTypes = useMemo(() => ({ 'relationship-edge': Relationship }), []);
|
||||
|
||||
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, {
|
||||
closable: false
|
||||
})
|
||||
}
|
||||
else if (databases.length == 0) {
|
||||
// there is no databases in the first place to select from ,
|
||||
// we have to create a new one .
|
||||
open(Modals.CREATE_DATABASE, {
|
||||
closable: false
|
||||
});
|
||||
}
|
||||
}, [database?.id, isLoading, isFetching , (syncStatus.currentStatus as any).options.hasSynced])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (isSwitchingDatabase) {
|
||||
setNodes([]);
|
||||
setEdges([]);
|
||||
} else {
|
||||
fitView({
|
||||
duration: 500
|
||||
});
|
||||
}
|
||||
}, [isSwitchingDatabase])
|
||||
|
||||
// Called when a connection is made between fields
|
||||
const onConnect = useCallback(async (connection: Connection) => {
|
||||
const sourceId: string | undefined = (connection.sourceHandle as string).split("_").pop();
|
||||
@@ -98,8 +142,8 @@ const DatabasePage: React.FC = () => {
|
||||
addToast({
|
||||
title: t("db_controller.invalid_relationship.title"),
|
||||
description: t("db_controller.invalid_relationship.description"),
|
||||
color: "danger",
|
||||
variant : "solid"
|
||||
color: "danger",
|
||||
variant: "solid"
|
||||
});
|
||||
}
|
||||
|
||||
@@ -108,6 +152,7 @@ const DatabasePage: React.FC = () => {
|
||||
|
||||
// Called when nodes are updated (position changes or removed)
|
||||
const handleNodesChanges: OnNodesChange<never> = useCallback(async (changes: NodeChange<never>[]) => {
|
||||
|
||||
const nodePositionChanges: NodePositionChange[] = changes.filter((change: NodeChange) =>
|
||||
change.type == "position" && !change.dragging
|
||||
) as NodePositionChange[];
|
||||
@@ -127,7 +172,7 @@ const DatabasePage: React.FC = () => {
|
||||
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
|
||||
}
|
||||
return onNodesChange(changes);
|
||||
}, [onNodesChange]);
|
||||
}, [onNodesChange, database]);
|
||||
|
||||
// Called when edges (relationships) change
|
||||
const handleEdgeChanges: OnEdgesChange<any> = useCallback((changes: EdgeChange<any>[]) => {
|
||||
@@ -137,7 +182,6 @@ const DatabasePage: React.FC = () => {
|
||||
if (edgeRemoveChanges.length > 0) {
|
||||
deleteMultiRelationships(edgeRemoveChanges.map((change: EdgeRemoveChange) => change.id));
|
||||
}
|
||||
|
||||
return onEdgesChange(changes as EdgeChange<never>[]);
|
||||
}, [onEdgesChange]);
|
||||
|
||||
@@ -170,9 +214,14 @@ const DatabasePage: React.FC = () => {
|
||||
useHighlightedEdges(nodes, relationships, edges);
|
||||
const { isOverlapping, puls } = useOverlappingTables(tables);
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className="w-full h-screen flex relative overflow-hidden">
|
||||
{
|
||||
(isSwitchingDatabase || isLoading || !(syncStatus.currentStatus as any).options.hasSynced) && <Loading />
|
||||
}
|
||||
<div className="flex max-w-full">
|
||||
<DBController />
|
||||
</div>
|
||||
@@ -191,31 +240,37 @@ const DatabasePage: React.FC = () => {
|
||||
defaultEdgeOptions={{
|
||||
type: 'relationship-edge',
|
||||
}}
|
||||
// onlyRenderVisibleElements
|
||||
|
||||
panOnDrag={true}
|
||||
zoomOnScroll={true}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
snapGrid={[20, 20]}
|
||||
minZoom={0.10}
|
||||
onConnectStart={onConnectStart}
|
||||
onConnectEnd={onConnectEnd}
|
||||
|
||||
>
|
||||
|
||||
<Controls
|
||||
position="bottom-center"
|
||||
showFitView={false}
|
||||
showZoom={false}
|
||||
showInteractive={false}
|
||||
className="shadow-none "
|
||||
>
|
||||
className="shadow-none">
|
||||
|
||||
<DatabaseControlButtons
|
||||
adjustPositions={adjustPositions}
|
||||
/>
|
||||
</Controls >
|
||||
<MiniMap
|
||||
nodeStrokeWidth={4}
|
||||
className="dark:bg-content1 bg-background border-1 border-default-200 dark:border-divider rounded-md"
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#272c35" : "#e9edf1"}
|
||||
maskColor={resolvedTheme == "dark" ? "#20252c99" : "#f2f4f733"}
|
||||
maskStrokeWidth={1}
|
||||
nodeClassName={"fill-default-300 dark:fill-font/30"}
|
||||
|
||||
<Background className=" dark:bg-background-100" />
|
||||
/>
|
||||
<Background className="dark:bg-background-100 " />
|
||||
</ReactFlow>
|
||||
<div
|
||||
className="absolute left-[24px] bottom-[24px] "
|
||||
@@ -231,8 +286,7 @@ const DatabasePage: React.FC = () => {
|
||||
isIconOnly
|
||||
color="danger"
|
||||
className="size-8 p-1 "
|
||||
onPressEnd={puls}
|
||||
>
|
||||
onPressEnd={puls}>
|
||||
<AlertTriangle className="size-4 text-white" />
|
||||
</Button>
|
||||
</span>
|
||||
@@ -248,17 +302,17 @@ const DatabasePage: React.FC = () => {
|
||||
}
|
||||
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
|
||||
<defs>
|
||||
<CardinalityMarker type="one" direction="start" />
|
||||
<CardinalityMarker type="one" direction="start" selected />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="start" />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="one" direction="end" />
|
||||
<CardinalityMarker type="one" direction="end" selected />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="end" />
|
||||
<CardinalityMarker type="numeric" cardinality="one" direction="end" selected />
|
||||
|
||||
<CardinalityMarker type="many" direction="start" />
|
||||
<CardinalityMarker type="many" direction="start" selected />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="start" />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="start" selected />
|
||||
|
||||
<CardinalityMarker type="many" direction="end" />
|
||||
<CardinalityMarker type="many" direction="end" selected />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="end" />
|
||||
<CardinalityMarker type="numeric" cardinality="many" direction="end" selected />
|
||||
|
||||
</defs>
|
||||
</svg>
|
||||
@@ -268,4 +322,8 @@ const DatabasePage: React.FC = () => {
|
||||
}
|
||||
|
||||
|
||||
export default DatabasePage;
|
||||
export default DatabasePage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { 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";
|
||||
|
||||
const ResizableBox = ResizableBoxRaw as unknown as React.FC<any>;
|
||||
|
||||
@@ -16,17 +17,17 @@ const DBController: React.FC<Props> = ({ }) => {
|
||||
const onResize = (event: any, params: any) => {
|
||||
setWidth(params.size.width);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<ResizableBox
|
||||
width={width}
|
||||
onResize={onResize} className="min-h-full overflow-hidden "
|
||||
onResize={onResize} className="min-h-full overflow-visible "
|
||||
minConstraints={[512]}
|
||||
|
||||
axis="x"
|
||||
handle={
|
||||
<div className="w-[6px] border-r-2 border-transparent h-full absolute right-0 top-0 cursor-ew-resize hover:border-primary active:border-primary transition-colors duration-200">
|
||||
|
||||
<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>
|
||||
}
|
||||
>
|
||||
|
||||
+11
-8
@@ -2,7 +2,7 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Cardinality, RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
|
||||
import { Button, Select, SelectItem, SharedSelection } from "@heroui/react";
|
||||
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, Trash2 } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -19,17 +19,19 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
const { t } = useTranslation();
|
||||
|
||||
const changeCardinality = (keys: SharedSelection) => {
|
||||
|
||||
if (keys.anchorKey != relationship.cardinality) {
|
||||
|
||||
|
||||
if (keys.anchorKey && (keys.anchorKey != relationship.cardinality)) {
|
||||
|
||||
editRelationship({
|
||||
id: relationship.id,
|
||||
cardinality: keys.anchorKey as Cardinality,
|
||||
|
||||
} as RelationshipInsertType);
|
||||
setCardinality(keys as any);
|
||||
|
||||
}
|
||||
|
||||
setCardinality(keys as any);
|
||||
}
|
||||
|
||||
const removeRelationship = () => {
|
||||
@@ -90,17 +92,18 @@ const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ r
|
||||
{t('db_controller.cardinality.name')}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="cardinality"
|
||||
selectedKeys={cardinality}
|
||||
onSelectionChange={changeCardinality}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
|
||||
}}
|
||||
|
||||
|
||||
>
|
||||
<SelectItem key={Cardinality.one_to_one}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
|
||||
<SelectItem key={Cardinality.one_to_many}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
|
||||
|
||||
+70
-55
@@ -1,12 +1,14 @@
|
||||
import { useRelationshipName } from "@/hooks/use-relationship-name";
|
||||
|
||||
|
||||
import { RelationshipInsertType, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDiagramOps } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { Button, cn, Input, Listbox, ListboxItem, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil, Trash } from "lucide-react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import hash from "object-hash";
|
||||
import { getDefaultRelationshipName } from "@/utils/relationship";
|
||||
|
||||
|
||||
interface RelationshipAccordionHeaderProps {
|
||||
@@ -14,13 +16,17 @@ interface RelationshipAccordionHeaderProps {
|
||||
relationship: RelationshipType
|
||||
}
|
||||
|
||||
|
||||
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen, relationship }) => {
|
||||
const defaultName : string = useMemo(() => {
|
||||
return getDefaultRelationshipName(relationship);
|
||||
}, [relationship])
|
||||
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { name: defaultName } = useRelationshipName(relationship);
|
||||
|
||||
const { editRelationship, deleteRelationship } = useDatabaseOperations();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [name, setName] = useState<string>(relationship.name ? relationship.name : defaultName);
|
||||
const { focusOnRelationship } = useDiagramOps();
|
||||
@@ -48,10 +54,10 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
'tarnsition-all duration-200 text-icon hover:text-font/90',
|
||||
isOpen ? "rotate-[90deg]" : ""
|
||||
)}>
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
<ChevronRight className="size-4" />
|
||||
</div>
|
||||
{
|
||||
editMode && <>
|
||||
@@ -64,16 +70,20 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
onValueChange={setName}
|
||||
onBlur={editRelationshipName}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
classNames={{
|
||||
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
|
||||
input: "font-semibold text-black dark:text-font"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font "
|
||||
className=" text-icon hover:bg-default hover:text-font/90 "
|
||||
size="sm"
|
||||
onPress={editRelationshipName}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-4 text-icon dark:text-white" />
|
||||
<Check className="size-4 " />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -82,63 +92,68 @@ const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> =
|
||||
!editMode && <>
|
||||
|
||||
<label
|
||||
className="w-full truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
|
||||
className=" flex-1 truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
|
||||
>
|
||||
{relationship.name ? relationship.name : defaultName}
|
||||
</label>
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||
<div className="flex w-fit shrink-0 items-center justify-stretch ">
|
||||
<div className="hidden shrink-0 group-hover:block ">
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPressEnd={() => focusOnRelationship(relationship.id, true)}
|
||||
>
|
||||
<Focus className="size-4 text-icon" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => setEditMode(true)}
|
||||
>
|
||||
<Pencil className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
onPressEnd={() => focusOnRelationship(relationship.id, true)}
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon" />
|
||||
<Focus className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px]" >
|
||||
<div className="w-full flex flex-col gap-2 ">
|
||||
<h3 className="font-semibold text-sm text-font/90 p-2">
|
||||
{t("db_controller.actions")}
|
||||
</h3>
|
||||
</div>
|
||||
<hr className="border-divider" />
|
||||
<Listbox aria-label="Actions" className="p-0 pb-1" >
|
||||
|
||||
<ListboxItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onPressEnd={onDeleteRelationship}
|
||||
endContent={<Trash className="size-4" />}
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
onPress={() => setEditMode(true)}
|
||||
>
|
||||
<Pencil className="size-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
<Popover placement="bottom" radius="sm" shadow="sm" showArrow isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
>
|
||||
{t("db_controller.delete")}
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<EllipsisVertical className="size-4 " />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px]" >
|
||||
<div className="w-full flex flex-col gap-2 ">
|
||||
<h3 className="font-semibold text-sm text-font/90 p-2">
|
||||
{t("db_controller.actions")}
|
||||
</h3>
|
||||
</div>
|
||||
<hr className="border-divider" />
|
||||
<Listbox aria-label="Actions" className="p-0 pb-1" >
|
||||
|
||||
<ListboxItem
|
||||
key="delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onPressEnd={onDeleteRelationship}
|
||||
endContent={<Trash className="size-4" />}
|
||||
>
|
||||
{t("db_controller.delete")}
|
||||
</ListboxItem>
|
||||
</Listbox>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
+15
-15
@@ -10,23 +10,23 @@ 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 { getDefaultRelationshipName } from "@/hooks/use-relationship-name";
|
||||
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";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const RelationshipController: React.FC = ({ }) => {
|
||||
|
||||
|
||||
|
||||
const { open } = useModal();
|
||||
const { database } = useDatabase();
|
||||
const { relationships: allRelationships } = database || { relationships : []};
|
||||
const { relationships: allRelationships } = database || { relationships: [] };
|
||||
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
|
||||
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
@@ -34,8 +34,7 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
const [selectedRelationship, setSelectedRelationship] = useState(new Set([]));
|
||||
const { focusedRelationshipId } = useDiagram();
|
||||
|
||||
useEffect(() => setRelationships(allRelationships), [allRelationships]);
|
||||
|
||||
useEffect(() => searchRelationships(), [allRelationships]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedRelationshipId) {
|
||||
@@ -67,8 +66,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([])) ;
|
||||
} , [])
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
@@ -81,10 +84,8 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
variant="light"
|
||||
className="size-8 p-0 text-icon hover:text-font/90"
|
||||
isIconOnly
|
||||
onPress={() =>
|
||||
//setShowDBML((value) => !value)
|
||||
console.log("hello world ")
|
||||
}
|
||||
onPressEnd={collapseAll}
|
||||
|
||||
|
||||
>
|
||||
<ListCollapse className="size-4" />
|
||||
@@ -100,15 +101,14 @@ const RelationshipController: React.FC = ({ }) => {
|
||||
<Input
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.filter")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
onKeyUp={searchRelationships}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default group-hover:border-primary ",
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
}}
|
||||
|
||||
/>
|
||||
|
||||
@@ -11,19 +11,45 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import CircularDependencyAlert from "./circular-dependecy-alert";
|
||||
import { addToast } from "@heroui/react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Clipboard from "@/components/clipboard/clipboard";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
|
||||
interface SqlPreviewProps {
|
||||
tableFilterIds ? : string[]
|
||||
}
|
||||
|
||||
|
||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
const { database : currentDatabase } = useDatabase();
|
||||
|
||||
|
||||
const database = useMemo(() => {
|
||||
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 ])
|
||||
|
||||
|
||||
|
||||
|
||||
const SqlPreview: React.FC = ({ }) => {
|
||||
|
||||
const { database } = useDatabase();
|
||||
|
||||
|
||||
|
||||
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
|
||||
const { resolvedTheme } = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
|
||||
console.log ( tableFilterIds ) ;
|
||||
|
||||
useEffect(() => {
|
||||
if (circularDependency)
|
||||
addToast({
|
||||
@@ -38,14 +64,22 @@ const SqlPreview: React.FC = ({ }) => {
|
||||
return <CircularDependencyAlert error={circularDependency} />
|
||||
else
|
||||
return (
|
||||
<div className="flex w-full h-full ">
|
||||
<div className="flex w-full h-full relative">
|
||||
<div className="absolute right-[12px] top-[4px] z-[1] bg-background ">
|
||||
<Clipboard
|
||||
text={sqlCode}
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
<CodeMirror
|
||||
<CodeMirror
|
||||
defaultValue={sqlCode}
|
||||
value={sqlCode}
|
||||
className="flex flex-1 w-full"
|
||||
className="flex flex-1 w-full "
|
||||
extensions={[sql()]}
|
||||
readOnly
|
||||
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@@ -55,4 +89,4 @@ const SqlPreview: React.FC = ({ }) => {
|
||||
|
||||
|
||||
|
||||
export default React.memo(SqlPreview);
|
||||
export default React.memo(SqlPreview) ;
|
||||
+34
-12
@@ -76,7 +76,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
const defaultValueType: DefaultValueType = useMemo(() => {
|
||||
return {
|
||||
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
|
||||
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year" ,
|
||||
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year",
|
||||
boolean: field.type?.type == DataTypes.BOOLEAN,
|
||||
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
|
||||
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
|
||||
@@ -251,7 +251,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
placeholder={t("db_controller.field_settings.value")}
|
||||
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 ",
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -274,15 +274,20 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
defaultValueType.time &&
|
||||
<>
|
||||
<Select
|
||||
className="w-full"
|
||||
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="Time"
|
||||
selectedKeys={timeSelection}
|
||||
onSelectionChange={changeTimeDefaultValue}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
|
||||
>
|
||||
<SelectItem key={TimeDefaultValues.NO_VALUE}>{t("db_controller.field_settings.time_default_value.no_value")}</SelectItem>
|
||||
<SelectItem key={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
|
||||
@@ -310,15 +315,22 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
onChange={setDefaultDateTime}
|
||||
granularity={field.type.name == "date" ? "day" : "second"}
|
||||
hideTimeZone
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
inputWrapper: "border-divider group-hover:border-primary focus-within:border-primary",
|
||||
|
||||
|
||||
}}
|
||||
calendarProps={{
|
||||
classNames: {
|
||||
title: " text-font/90"
|
||||
}
|
||||
title: "text-font/90",
|
||||
header : "bg-background" ,
|
||||
|
||||
} ,
|
||||
|
||||
|
||||
}}
|
||||
|
||||
/>
|
||||
|
||||
</>
|
||||
@@ -333,8 +345,9 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
endContent={
|
||||
<Clock className="text-icon size-4" />
|
||||
}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
inputWrapper: "border-divider group-hover:border-primary focus-within:border-primary",
|
||||
|
||||
}}
|
||||
onBlur={saveDefaultDateTime}
|
||||
@@ -348,8 +361,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
}
|
||||
{
|
||||
(defaultValueType.select || defaultValueType.multiSelect) &&
|
||||
<Select
|
||||
className="w-full"
|
||||
<Select
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="Enum Values"
|
||||
@@ -357,8 +369,13 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
selectedKeys={selectedValues}
|
||||
onSelectionChange={enumValueChange}
|
||||
selectionMode={!defaultValueType.multiSelect ? 'single' : "multiple"}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
|
||||
}}
|
||||
>
|
||||
{
|
||||
@@ -377,4 +394,9 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(fieldDefautlValue);
|
||||
export default React.memo(fieldDefautlValue);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+18
-14
@@ -2,12 +2,12 @@
|
||||
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Button, Input, Popover, PopoverContent, PopoverTrigger, Switch, Textarea } from "@heroui/react";
|
||||
import { Ellipsis, EllipsisVertical, GripVertical, KeyRound, Settings, Settings2 } from "lucide-react";
|
||||
import { Ellipsis, EllipsisVertical, GripVertical, KeyRound, Settings, Settings2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { Key, useEffect, useState } from "react";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import Autocomplete from "@/components/auto-complete/auto-complete";
|
||||
import ToggleButton from "@/components/toggle/toggle";
|
||||
import FieldSetting from "./field-setting";
|
||||
@@ -22,8 +22,8 @@ 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 { editField } = useDatabaseOperations();
|
||||
const { grouped_data_types } = useDatabaseOperations();
|
||||
const { editField } = useDatabaseOperations();
|
||||
|
||||
const [selectedType, setSelectedType] = useState<string | undefined>(field.typeId as string | undefined);
|
||||
const { t } = useTranslation();
|
||||
@@ -42,7 +42,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
setSelectedType(field.typeId as string | undefined);
|
||||
}, [field.typeId])
|
||||
|
||||
|
||||
|
||||
const saveFieldName = () => {
|
||||
editField({
|
||||
id: field.id,
|
||||
@@ -50,11 +50,14 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
} as FieldType);
|
||||
}
|
||||
const updateFieldType = (key: Key | null) => {
|
||||
editField({
|
||||
id: field.id,
|
||||
typeId: key
|
||||
} as FieldType);
|
||||
setSelectedType(key as string | undefined);
|
||||
|
||||
if (key != null) {
|
||||
editField({
|
||||
id: field.id,
|
||||
typeId: key
|
||||
} as FieldType);
|
||||
setSelectedType(key as string | undefined);
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNullable = (nullable: boolean) => {
|
||||
@@ -86,14 +89,15 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
value={fieldName}
|
||||
onValueChange={setFieldName}
|
||||
onBlur={saveFieldName}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary",
|
||||
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
}}
|
||||
/>
|
||||
<Autocomplete
|
||||
items={grouped_data_types}
|
||||
onSelectionChange={updateFieldType}
|
||||
grouped
|
||||
grouped
|
||||
selectedItem={selectedType}
|
||||
placeholder={t("db_controller.type")}
|
||||
/>
|
||||
@@ -116,7 +120,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
<KeyRound className="size-4" />
|
||||
</ToggleButton>
|
||||
|
||||
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
|
||||
<PopoverTrigger>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -128,7 +132,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent >
|
||||
<FieldSetting field={field}/>
|
||||
<FieldSetting field={field} />
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
+2
-1
@@ -23,7 +23,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [fields, setFields] = useState<FieldType[]>(tableFields);
|
||||
const { createField, orderTableFields } = useDatabaseOperations();
|
||||
const { createField, orderTableFields , getInteger } = useDatabaseOperations();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -59,6 +59,7 @@ const FieldList: React.FC<Props> = ({ tableFields , tableId}) => {
|
||||
tableId: tableId,
|
||||
sequence: getNextSequence(fields) ,
|
||||
nullable: true,
|
||||
typeId: getInteger()?.id
|
||||
})
|
||||
}
|
||||
return (
|
||||
|
||||
+41
-13
@@ -272,7 +272,14 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.unique")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
|
||||
<Checkbox
|
||||
defaultSelected={field.unique as boolean}
|
||||
size="md"
|
||||
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
onValueChange={toggleUnqiue} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
@@ -287,7 +294,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.autoIncrement")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.autoIncrement as boolean} size="md" onValueChange={toggleAutoIncrement} />
|
||||
<Checkbox defaultSelected={field.autoIncrement as boolean}
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
size="md" onValueChange={toggleAutoIncrement} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
@@ -296,7 +307,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.unsigned")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
|
||||
<Checkbox
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
|
||||
</div>
|
||||
}
|
||||
{
|
||||
@@ -305,7 +320,11 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
<span className="text-xs text-font/70 font-medium dark:text-font/90">
|
||||
{t("db_controller.field_settings.zeroFill")}
|
||||
</span>
|
||||
<Checkbox defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
|
||||
<Checkbox
|
||||
classNames={{
|
||||
wrapper: "before:border-divider group-data-[hover=true]:before:bg-default",
|
||||
}}
|
||||
defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -356,7 +375,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
placeholder={t("db_controller.field_settings.precision")}
|
||||
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 ",
|
||||
}}
|
||||
|
||||
/>
|
||||
@@ -400,7 +419,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
placeholder={t("db_controller.field_settings.scale")}
|
||||
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 ",
|
||||
}}
|
||||
|
||||
/>
|
||||
@@ -422,16 +441,21 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{t("db_controller.field_settings.charset")}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="charset"
|
||||
placeholder={t("db_controller.field_settings.charset")}
|
||||
selectedKeys={charset as any}
|
||||
onSelectionChange={changeCharset}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
}}
|
||||
|
||||
|
||||
>
|
||||
{
|
||||
Object.values(charsets).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
|
||||
@@ -445,15 +469,18 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
{t("db_controller.field_settings.collation")}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
aria-label="collation"
|
||||
placeholder={t("db_controller.field_settings.collation")}
|
||||
selectedKeys={collation as any}
|
||||
onSelectionChange={changeCollation}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
|
||||
}}
|
||||
>
|
||||
{
|
||||
@@ -503,9 +530,9 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
}
|
||||
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 ",
|
||||
}}
|
||||
|
||||
/>
|
||||
</>
|
||||
}
|
||||
@@ -539,9 +566,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
|
||||
onValueChange={setNote}
|
||||
onBlur={updateFieldNote}
|
||||
classNames={{
|
||||
inputWrapper: "bg-default border-divider ",
|
||||
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"
|
||||
}} />
|
||||
<hr className="border-divider" />
|
||||
<Button
|
||||
|
||||
+9
-4
@@ -67,14 +67,18 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Select
|
||||
className="w-full"
|
||||
placeholder={t("db_controller.select_fields")}
|
||||
selectionMode="multiple"
|
||||
size="sm"
|
||||
aria-label={t("db_controller.select_fields")}
|
||||
variant="bordered"
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
|
||||
classNames={{
|
||||
trigger: "border-divider group-hover:border-primary",
|
||||
trigger: "border-divider group-hover:border-primary data-[focus=true]:border-primary data-[open=true]:border-primary",
|
||||
selectorIcon: "text-icon",
|
||||
popoverContent: "rounded-md "
|
||||
|
||||
}}
|
||||
|
||||
onSelectionChange={onInexFieldChange}
|
||||
@@ -122,8 +126,9 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
placeholder={t("db_controller.index_name")}
|
||||
onBlur={editIndexName}
|
||||
size="sm"
|
||||
autoFocus
|
||||
classNames={{
|
||||
inputWrapper: "border-divider group-hover:border-primary ",
|
||||
inputWrapper: " border-divider group-hover:border-primary group-data-[focus=true]:border-primary",
|
||||
}}
|
||||
defaultValue={index.name}
|
||||
/>
|
||||
@@ -140,7 +145,7 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
|
||||
<span className="font-medium text-sm">
|
||||
{t("db_controller.delete_index")}
|
||||
</span>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
|
||||
<Trash2 className="mr-1 size-3.5 text-danger dark:text-white" />
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
|
||||
+16
-5
@@ -8,7 +8,7 @@ import ColorPicker from "@/components/color-picker/color-picker";
|
||||
import FieldList from "./field/field-list";
|
||||
import IndexesList from "./index/indexes-list";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { getNextSequence } from "@/utils/field";
|
||||
import { v4 } from "uuid";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
@@ -16,14 +16,16 @@ import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
|
||||
export interface TableAccordionBodyProps {
|
||||
table: TableType,
|
||||
keys?: string[];
|
||||
}
|
||||
|
||||
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table, keys }) => {
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
|
||||
const [note, setNote] = useState<string>(table.note ? table.note : "");
|
||||
const { t } = useTranslation();
|
||||
const { editTable, createField, createIndex } = useDatabaseOperations();
|
||||
|
||||
const { editTable, createField, createIndex, getInteger } = useDatabaseOperations();
|
||||
|
||||
const onColorChange = useCallback((color: string | undefined) => {
|
||||
editTable({ id: table.id, color: color ? color : null } as TableType);
|
||||
@@ -38,7 +40,8 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(table.fields),
|
||||
nullable: true,
|
||||
})
|
||||
typeId: getInteger()?.id
|
||||
});
|
||||
}
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
@@ -49,6 +52,7 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
unique: true,
|
||||
tableId: table.id
|
||||
} as IndexInsertType);
|
||||
|
||||
}
|
||||
const saveNote = () => {
|
||||
editTable({
|
||||
@@ -63,7 +67,13 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
}, [table.note]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(new Set([...selectedKeys, "fields"]))
|
||||
}, [table.fields.length])
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedKeys(new Set([...selectedKeys, "indexes"]))
|
||||
}, [table.indices.length])
|
||||
|
||||
return (
|
||||
<div className="w-full dark:bg-background-50">
|
||||
@@ -149,7 +159,8 @@ const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
}>
|
||||
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")}
|
||||
classNames={{
|
||||
inputWrapper: "bg-default border-divider dark:bg-background-100 ",
|
||||
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"
|
||||
}}
|
||||
value={note}
|
||||
onValueChange={setNote}
|
||||
|
||||
+29
-20
@@ -21,7 +21,7 @@ export interface TableAccordionHeaderProps {
|
||||
|
||||
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen }) => {
|
||||
const { editTable, deleteTable, createField, createTable , createIndex } = useDatabaseOperations();
|
||||
const { editTable, deleteTable, createField, createTable, createIndex , getInteger } = useDatabaseOperations();
|
||||
|
||||
const [popOverOpen, setPopOverOpen] = useState<boolean>(false);
|
||||
const [tableName, setTableName] = useState<string>(table.name);
|
||||
@@ -30,12 +30,12 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { focusOnTable } = useDiagramOps();
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTableName(table.name);
|
||||
}, [table.name])
|
||||
|
||||
const saveTableName = useCallback(async () => {
|
||||
|
||||
await editTable({ id: table.id, name: tableName } as TableInsertType);
|
||||
setEditMode(false);
|
||||
}, [tableName])
|
||||
@@ -53,12 +53,13 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
tableId: table.id,
|
||||
sequence: getNextSequence(table.fields),
|
||||
nullable: true,
|
||||
typeId : getInteger()?.id
|
||||
})
|
||||
}
|
||||
|
||||
const addIndex = (event: any) => {
|
||||
setPopOverOpen(false)
|
||||
|
||||
|
||||
createIndex({
|
||||
id: v4(),
|
||||
name: `index_${table.indices.length + 1}`,
|
||||
@@ -84,10 +85,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
}}
|
||||
>
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
'tarnsition-all duration-200 text-icon hover:text-font/90',
|
||||
isOpen ? "rotate-[90deg]" : ""
|
||||
)}>
|
||||
<ChevronRight className="size-4 text-icon " />
|
||||
<ChevronRight className="size-4 " />
|
||||
</div>
|
||||
<div className=" w-[1px] h-full bg-divider ">
|
||||
</div>
|
||||
@@ -99,6 +100,7 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<label
|
||||
className="w-full text-editable truncate px-2 py-1 text-sm font-semibold text-black dark:text-font"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{tableName}
|
||||
</label>
|
||||
@@ -113,23 +115,27 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
editMode && <>
|
||||
<Input
|
||||
placeholder={"Table name"}
|
||||
autoFocus
|
||||
size="sm"
|
||||
value={tableName}
|
||||
onChange={(event: any) => setTableName(event.target.value)}
|
||||
variant="bordered"
|
||||
onBlur={saveTableName}
|
||||
autoFocus
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-primary-700 focus-visible:ring-0 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
classNames={{
|
||||
inputWrapper: "rounded-sm focus-visible:border-0 text-sm dark:bg-content1 bg-background group-data-[hover=true]:border-primary group-data-[focus=true]:border-primary border-primary ",
|
||||
input: "font-semibold text-black dark:text-font"
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font "
|
||||
className=" text-icon hover:bg-default hover:text-font/90 "
|
||||
size="sm"
|
||||
onPressEnd={saveTableName}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-4 text-icon " />
|
||||
<Check className="size-4 " />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
@@ -143,17 +149,19 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
onPress={() => setEditMode(true)}
|
||||
>
|
||||
<Pencil className="size-4 text-icon " />
|
||||
<Pencil className="size-4 " />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
onPressEnd={() => focusOnTable(table.id, true)}
|
||||
>
|
||||
<Focus className="size-4 text-icon " />
|
||||
<Focus className="size-4 " />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -163,8 +171,9 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon " />
|
||||
<EllipsisVertical className="size-4 " />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[160px]" >
|
||||
@@ -180,19 +189,19 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
onPressEnd={addField}
|
||||
className="text-font/90"
|
||||
endContent={
|
||||
<FileType className="size-4 " />
|
||||
<FileType className="size-4 text-icon group-hover:text-font/90 " />
|
||||
|
||||
}>
|
||||
{t("db_controller.add_field")}
|
||||
</ListboxItem>
|
||||
<ListboxItem
|
||||
key="add_index"
|
||||
|
||||
|
||||
className="text-font/90"
|
||||
endContent={<FileKey className="size-4" />}
|
||||
endContent={<FileKey className="size-4 text-icon group-hover:text-font/90" />}
|
||||
showDivider
|
||||
onPressEnd={addIndex}
|
||||
>
|
||||
>
|
||||
|
||||
{t("db_controller.add_index")}
|
||||
</ListboxItem>
|
||||
@@ -200,10 +209,10 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
<ListboxItem
|
||||
key="duplicate"
|
||||
showDivider
|
||||
|
||||
|
||||
className="text-font/90"
|
||||
onPressEnd={duplicate}
|
||||
endContent={<Copy className="size-4" />}>
|
||||
endContent={<Copy className="size-4 text-icon group-hover:text-font/90" />}>
|
||||
|
||||
{t("db_controller.duplicate")}
|
||||
</ListboxItem>
|
||||
@@ -212,8 +221,8 @@ const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOp
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onPressEnd={onDeleteTable}
|
||||
|
||||
endContent={<Trash className="size-4" />}
|
||||
|
||||
endContent={<Trash className="size-4 " />}
|
||||
>
|
||||
{t("db_controller.delete_table")}
|
||||
</ListboxItem>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/to
|
||||
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
|
||||
import { Code, List, Table } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Ref, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
|
||||
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
@@ -12,16 +12,17 @@ import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
|
||||
import { useReactFlow } from "@xyflow/react";
|
||||
import SqlPreview from "../sql-preview";
|
||||
|
||||
interface Props { }
|
||||
|
||||
const PADDING_X = 40;
|
||||
const PADDING_Y = 80;
|
||||
|
||||
const TablesController: React.FC<Props> = ({ }) => {
|
||||
|
||||
const { database, getDefaultPrimaryKeyType } = useDatabase();
|
||||
const { createTable, data_types } = useDatabaseOperations();
|
||||
const TablesController: React.FC = ({ }) => {
|
||||
|
||||
const { database } = useDatabase();
|
||||
const { createTable, getInteger } = useDatabaseOperations();
|
||||
const { getViewport } = useReactFlow();
|
||||
const { tables: allTables } = database || { tables : []};
|
||||
const { tables: allTables } = database || { tables: [] };
|
||||
const [tables, setTables] = useState<TableType[]>(allTables);
|
||||
|
||||
const { t } = useTranslation();
|
||||
@@ -29,14 +30,16 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
const [showSqlPreview, setShowSqlPreview] = useState<boolean>(false);
|
||||
const { focusedTableId } = useDiagram();
|
||||
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
|
||||
const accordionRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => setTables(allTables), [allTables]);
|
||||
useEffect(() => searchTables(), [allTables]);
|
||||
|
||||
const addNewTable = useCallback(async () => {
|
||||
|
||||
const newTableId: string = v4();
|
||||
const viewport = getViewport();
|
||||
const { x, y, zoom } = viewport;
|
||||
// Convert screen (0,0) to flow coordinates using viewport values
|
||||
|
||||
const posX = -x / zoom + (PADDING_X / zoom);
|
||||
const posY = -y / zoom + (PADDING_Y / zoom);
|
||||
|
||||
@@ -50,18 +53,25 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
name: "id",
|
||||
isPrimary: true,
|
||||
unique: true,
|
||||
typeId: getDefaultPrimaryKeyType(database?.dialect)?.id
|
||||
typeId: getInteger()?.id,
|
||||
autoIncrement: true,
|
||||
|
||||
}]
|
||||
} as TableInsertType);
|
||||
|
||||
setSelectedTable(new Set([newTableId]) as any);
|
||||
}, [database , tables, getViewport, getDefaultPrimaryKeyType]);
|
||||
|
||||
}, [database, tables, getViewport, getInteger]);
|
||||
|
||||
useEffect(() => {
|
||||
if (focusedTableId) {
|
||||
|
||||
setSelectedTable(new Set([focusedTableId]) as any);
|
||||
const accordionItem = document.getElementById(focusedTableId)
|
||||
if (accordionItem)
|
||||
|
||||
accordionItem?.scrollIntoView({
|
||||
behavior: 'smooth', block: 'center'
|
||||
})
|
||||
}
|
||||
}, [focusedTableId]);
|
||||
|
||||
@@ -73,10 +83,13 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
setTables(() => allTables.filter((table: TableType) => table.name.toLowerCase().trim().includes(keyword?.toLowerCase().trim())))
|
||||
}, [nameRef, allTables]);
|
||||
|
||||
|
||||
const toggleSqlPreview = useCallback(() => {
|
||||
setShowSqlPreview(preview => !preview);
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const tableFilterIds = useMemo(() => {
|
||||
return tables.map((table: TableType) => table.id);
|
||||
}, [tables]);
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
@@ -109,13 +122,12 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
ref={nameRef}
|
||||
type="text"
|
||||
size="sm"
|
||||
autoFocus
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("db_controller.filter")}
|
||||
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",
|
||||
}}
|
||||
onKeyUp={searchTables}
|
||||
|
||||
@@ -130,29 +142,27 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
<Table className="h-4 w-4 " />
|
||||
}
|
||||
className="h-8 p-2 text-xs font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
> {t("db_controller.add_table")}
|
||||
</Button>
|
||||
</div>
|
||||
{
|
||||
!showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto">
|
||||
|
||||
<Accordion
|
||||
hideIndicator
|
||||
|
||||
selectedKeys={selectedTable}
|
||||
onSelectionChange={setSelectedTable as any}
|
||||
isCompact
|
||||
|
||||
ref={accordionRef}
|
||||
>
|
||||
{tables.map((table: TableType) => (
|
||||
<AccordionItem
|
||||
key={table.id}
|
||||
id={table.id}
|
||||
aria-label={table.name}
|
||||
classNames={{
|
||||
trigger: "w-full h-12 hover:bg-default transition-all duration-200 dark:hover:bg-background",
|
||||
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
|
||||
base: "rounded-md mb-1 mt-1 p-0 overflow-hidden dark:border-background-100",
|
||||
content: "bg-transparent"
|
||||
}}
|
||||
subtitle={
|
||||
@@ -160,8 +170,7 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
isOpen={selectedTableId == table.id}
|
||||
table={table}
|
||||
/>
|
||||
}
|
||||
>
|
||||
}>
|
||||
<TableAccordionBody table={table} />
|
||||
</AccordionItem>
|
||||
))}
|
||||
@@ -171,7 +180,9 @@ const TablesController: React.FC<Props> = ({ }) => {
|
||||
{
|
||||
showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto ">
|
||||
<SqlPreview />
|
||||
<SqlPreview
|
||||
tableFilterIds={tableFilterIds}
|
||||
/>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -2,19 +2,27 @@ import DatabaseCheckbox from "@/components/checkbox/database-checkbox";
|
||||
import Modal, { ModalProps } from "@/components/modal/modal"
|
||||
import { DatabaseType, DBTypes } from "@/lib/database";
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { Button, CheckboxGroup, Input } from "@heroui/react";
|
||||
import { usePowerSync, usePowerSyncStatus } from "@powersync/react";
|
||||
import { Database, SquareMenu } from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { v4 } from "uuid";
|
||||
|
||||
|
||||
export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
const [isValid, setIsValid] = useState<boolean>(false);
|
||||
|
||||
export const CreateDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
const { t } = useTranslation();
|
||||
const { isOpen, onOpenChange } = props ;
|
||||
|
||||
|
||||
const [isValid, setIsValid] = useState<boolean>(false);
|
||||
const [selectedDbType, setSelectedDbType] = useState<string[]>([DBTypes[0].dialect]);
|
||||
const [dbName, setDbName] = useState<string>("db_example");
|
||||
const { createDatabase, switchDatabase } = useDatabaseOperations();
|
||||
const { open } = useModal();
|
||||
|
||||
const onDatabaseTypeChange = (types: string[]) => {
|
||||
const selectedType: string | undefined = types.pop();
|
||||
@@ -24,106 +32,101 @@ export const CreateDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange
|
||||
|
||||
const createNewDatabase = useCallback(async () => {
|
||||
const databaseId: string = v4();
|
||||
|
||||
return new Promise(async (res, rej) => {
|
||||
|
||||
await createDatabase({
|
||||
id: databaseId,
|
||||
name: dbName,
|
||||
dialect: selectedDbType[0] as any
|
||||
});
|
||||
|
||||
switchDatabase(databaseId);
|
||||
res(databaseId)
|
||||
})
|
||||
|
||||
}, [selectedDbType, dbName])
|
||||
|
||||
|
||||
try {
|
||||
await createDatabase({
|
||||
id: databaseId,
|
||||
name: dbName,
|
||||
dialect: selectedDbType[0] as any
|
||||
});
|
||||
switchDatabase(databaseId);
|
||||
open(Modals.IMPORT_DATABASE);
|
||||
res(databaseId);
|
||||
} catch (error) {
|
||||
rej(error);
|
||||
}
|
||||
})
|
||||
}, [selectedDbType, dbName])
|
||||
|
||||
useEffect(() => {
|
||||
setIsValid((selectedDbType.length > 0 && dbName.trim().length > 0) as boolean)
|
||||
}, [selectedDbType, dbName])
|
||||
}, [selectedDbType, dbName]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.pick_database")}
|
||||
actionName={t("modals.continue")}
|
||||
className="min-w-[720px]"
|
||||
isDisabled={!isValid}
|
||||
actionHandler={createNewDatabase}
|
||||
header={t("modals.create_database_header")}
|
||||
>
|
||||
<div className="w-full justify-center flex ">
|
||||
<div className="flex flex-col gap-1 w-[70%]">
|
||||
<div className="p-8 py-2 pb-4 space-y-2">
|
||||
<label className="text-sm text-font/90 font-semibold">
|
||||
{t("modals.db_name")}
|
||||
</label>
|
||||
<Input
|
||||
errorMessage={t("modals.db_name_error")}
|
||||
isInvalid={dbName.trim().length == 0}
|
||||
type="text"
|
||||
value={dbName}
|
||||
onValueChange={setDbName}
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
placeholder={t("modals.db_name")}
|
||||
className="h-8 w-full focus-visible:ring-0 shadow-none "
|
||||
startContent={
|
||||
<Database className="text-icon size-4 " />
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.pick_database")}
|
||||
actionName={t("modals.continue")}
|
||||
className="min-w-[720px]"
|
||||
isDisabled={!isValid}
|
||||
actionHandler={createNewDatabase}
|
||||
header={t("modals.create_database_header")}
|
||||
>
|
||||
<div className="w-full justify-center flex ">
|
||||
<div className="flex flex-col gap-1 w-[70%]">
|
||||
<div className="p-8 py-2 pb-4 space-y-2">
|
||||
<label className="text-sm text-font/90 font-semibold">
|
||||
{t("modals.db_name")}
|
||||
</label>
|
||||
<Input
|
||||
errorMessage={t("modals.db_name_error")}
|
||||
isInvalid={dbName.trim().length == 0}
|
||||
type="text"
|
||||
value={dbName}
|
||||
onValueChange={setDbName}
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="faded"
|
||||
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 ",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<CheckboxGroup
|
||||
classNames={{
|
||||
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
|
||||
base: "w-full p-0 ",
|
||||
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
|
||||
}}
|
||||
|
||||
/>
|
||||
</div>
|
||||
<CheckboxGroup
|
||||
classNames={{
|
||||
base: "w-full p-0 ",
|
||||
wrapper: "flex-row p-4 gap-8 px-0 items-center justify-center"
|
||||
}}
|
||||
aria-label="Select Database"
|
||||
value={selectedDbType}
|
||||
onChange={onDatabaseTypeChange}
|
||||
>
|
||||
{
|
||||
DBTypes.map((db: DatabaseType) => (
|
||||
<DatabaseCheckbox
|
||||
database={db}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</CheckboxGroup>
|
||||
<div className="p-8 py-4 space-y-2">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
aria-label="Select Database"
|
||||
value={selectedDbType}
|
||||
onChange={onDatabaseTypeChange}
|
||||
>
|
||||
<SquareMenu className="size-4" /> Check examples
|
||||
{
|
||||
DBTypes.map((db: DatabaseType) => (
|
||||
<DatabaseCheckbox
|
||||
database={db}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</CheckboxGroup>
|
||||
<div className="p-8 py-4 space-y-2">
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
>
|
||||
<SquareMenu className="size-4" /> Check examples
|
||||
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
</Button>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
className="w-full text-font border-divider"
|
||||
|
||||
>
|
||||
<span className="underline">
|
||||
Empty Diagram
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
>
|
||||
<span className="underline">
|
||||
Empty Diagram
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -79,8 +79,8 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
|
||||
id,
|
||||
} as RelationshipInsertType);
|
||||
onRlationshipCreated && onRlationshipCreated(id);
|
||||
|
||||
}, [relationship]);
|
||||
onOpenChange && onOpenChange(false) ;
|
||||
}, [relationship , onOpenChange]);
|
||||
|
||||
|
||||
return (
|
||||
@@ -92,7 +92,6 @@ const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRel
|
||||
className="min-w-[520px]"
|
||||
isDisabled={!isValid}
|
||||
actionHandler={addRelationship}
|
||||
|
||||
>
|
||||
|
||||
<div className="grid grid-cols-2 grid-rows-2 gap-4">
|
||||
|
||||
@@ -14,7 +14,8 @@ const DeleteDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
return await new Promise(async (res, rej) => {
|
||||
await deleteDatabase(currentDatabaseId as string);
|
||||
switchDatabase( undefined ) ;
|
||||
res(currentDatabaseId)
|
||||
res(currentDatabaseId) ;
|
||||
onOpenChange && onOpenChange(false) ;
|
||||
})
|
||||
}, [currentDatabaseId])
|
||||
|
||||
|
||||
@@ -1,66 +1,109 @@
|
||||
import Modal, { ModalProps } from "@/components/modal/modal"
|
||||
import ReactCodeMirror, { oneDark } from "@uiw/react-codemirror";
|
||||
import { useCallback, useMemo, useState } from "react"
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
|
||||
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { useTheme } from "next-themes";
|
||||
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType } from "@/lib/database";
|
||||
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 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 { adjustTablesPositions } from "@/utils/tables";
|
||||
import Clipboard from "@/components/clipboard/clipboard";
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const options: ImportDatabaseOption[] = [{
|
||||
dialect: DatabaseDialect.POSTGRES,
|
||||
methods: [{
|
||||
id: "pg_dump",
|
||||
name: "pg_dump",
|
||||
icon: <Code className="size-4 text-font/90" />,
|
||||
type: ImportMethodType.DUMP
|
||||
|
||||
}, {
|
||||
id: "pg_admin",
|
||||
name: "pg Admin",
|
||||
logo: "/postgresql_logo.png",
|
||||
type: ImportMethodType.DB_CLIENT
|
||||
}]
|
||||
}, {
|
||||
dialect: DatabaseDialect.MYSQL,
|
||||
methods: [{
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-4 text-font/90" />,
|
||||
type: ImportMethodType.DUMP
|
||||
|
||||
}]
|
||||
}
|
||||
, {
|
||||
dialect: DatabaseDialect.MARIADB,
|
||||
methods: [{
|
||||
id: "mysql_dump",
|
||||
name: "mysqldump",
|
||||
icon: <Code className="size-4 text-font/90" />,
|
||||
type: ImportMethodType.DUMP
|
||||
|
||||
}]
|
||||
}]
|
||||
|
||||
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 } = useDatabase();
|
||||
const { database , isLoading , isSwitchingDatabase } = useDatabase();
|
||||
const { data_types, importDatabase } = useDatabaseOperations();
|
||||
const [sqlCode, setSqlCode] = useState<string>("");
|
||||
|
||||
@@ -68,11 +111,11 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
|
||||
}, [database]);
|
||||
|
||||
const [selectedMethodId, setSelectedMethodId] = useState<string[]>(currentOption ? [currentOption.methods[0].id] : []);
|
||||
|
||||
if (!currentOption) {
|
||||
onOpenChange && onOpenChange(false);
|
||||
}
|
||||
const [selectedMethodId, setSelectedMethodId] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedMethodId(currentOption ? [currentOption.methods[0].id] : [])
|
||||
}, [currentOption])
|
||||
|
||||
const onImportMethodChange = (types: string[]) => {
|
||||
const selectedType: string | undefined = types.pop();
|
||||
@@ -80,16 +123,21 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
setSelectedMethodId([selectedType]);
|
||||
}
|
||||
|
||||
const selectedImportMethod: ImportDatabaseMethod = useMemo(() => {
|
||||
const selectedImportMethod: ImportDatabaseMethod | undefined = useMemo(() => {
|
||||
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId?.[0]) as ImportDatabaseMethod;
|
||||
}, [selectedMethodId, currentOption])
|
||||
|
||||
const onImport = useCallback(async () => {
|
||||
|
||||
const { tables, relationships, indices } = SqlToDatabase(sqlCode, data_types, database?.dialect as DatabaseDialect);
|
||||
await importDatabase(tables, relationships, indices);
|
||||
onOpenChange && onOpenChange(false);
|
||||
|
||||
return await importDatabase(tables, relationships, indices)
|
||||
}, [sqlCode, database?.dialect, data_types]);
|
||||
|
||||
|
||||
if (isLoading || isSwitchingDatabase)
|
||||
return ;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -102,7 +150,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="w-full ">
|
||||
<p className="text-sm text-font/90">
|
||||
<p className="text-sm text-font">
|
||||
{t("modals.import_database.import_options")}
|
||||
</p>
|
||||
<CheckboxGroup
|
||||
@@ -121,88 +169,82 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
value={method.id}
|
||||
icon={method.icon}
|
||||
logo={method.logo}
|
||||
isSelected={selectedImportMethod?.id == method.id}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</CheckboxGroup>
|
||||
{
|
||||
selectedImportMethod.type == ImportMethodType.DUMP &&
|
||||
selectedImportMethod?.type == ImportMethodType.DUMP &&
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
Instructions :
|
||||
<label className="text-sm font-medium ">
|
||||
{t("import.instructions")}
|
||||
</label>
|
||||
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-2 marker:font-semibold ">
|
||||
<ul className="list-decimal list-outside px-4 text-font text-sm space-y-2 marker:font-medium ">
|
||||
<li>
|
||||
install <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
|
||||
{t("import.install")} <span className=" font-medium ">{selectedImportMethod?.name}</span> .
|
||||
</li>
|
||||
<li className="space-y-2">
|
||||
<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" >
|
||||
{selectedImportMethod.instruction}
|
||||
<Clipboard text={selectedImportMethod.instruction} />
|
||||
</CodeSection>
|
||||
|
||||
<li>
|
||||
Run the following command in your terminal :
|
||||
<ReactCodeMirror
|
||||
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
|
||||
|
||||
editable={false}
|
||||
|
||||
value={`pg_dump -h <host> -p <port> -d <database_name>
|
||||
-U <username> -s -F p -E UTF-8
|
||||
-f <output_file_path>`}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
|
||||
/>
|
||||
Example :
|
||||
<ReactCodeMirror
|
||||
className="flex flex-1 w-full border-1 my-2 rounded-md border-divider overflow-hidden "
|
||||
value={`pg_dump -h localhost -p 5432 -d my_db
|
||||
-U postgres -s -F p -E UTF-8
|
||||
-f schema_export.sql`}
|
||||
editable={false}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [overrideDarkTheme, oneDark]}
|
||||
/>
|
||||
|
||||
<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" >
|
||||
{selectedImportMethod.example}
|
||||
<Clipboard text={selectedImportMethod.example} />
|
||||
</CodeSection>
|
||||
</li>
|
||||
<li>
|
||||
Drag and drop the output .sql file in code section or copy it content
|
||||
{t("import.copy_code")}
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
selectedImportMethod.type == ImportMethodType.DB_CLIENT &&
|
||||
selectedImportMethod?.type == ImportMethodType.DB_CLIENT &&
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-semibold">
|
||||
Instructions :
|
||||
<label className="text-sm font-medium">
|
||||
{t("import.instructions")}
|
||||
</label>
|
||||
<ul className="list-decimal list-outside px-4 text-font/90 text-sm space-y-3 marker:font-semibold ">
|
||||
<li>
|
||||
Open <span className="font-semibold text-font/90">{selectedImportMethod.name}</span> .
|
||||
</li>
|
||||
<li>
|
||||
Right-click your database and select <span className="font-semibold text-font/90">Backup</span> from the context menu.
|
||||
</li>
|
||||
<li>
|
||||
Name your <CodeSection className="p-1" color="default" size="sm"> .sql </CodeSection> file, set Format to <span className="font-semibold text-font/90">Plain</span>, and choose <span className="font-semibold text-font/90">Encoding: UTF8.</span>
|
||||
</li>
|
||||
<li>
|
||||
Make sure <span className="font-semibold text-font/90">Only schema</span> is checked and <span className="font-semibold text-font/90">Only data</span> is unchecked in the <span className="font-semibold text-font/90">Data Options tab</span>.
|
||||
</li>
|
||||
<li>
|
||||
Click <span className="font-semibold text-font/90">Backup</span> to export the file, then copy its content into the code editor section.
|
||||
</li>
|
||||
<ul className="list-decimal list-outside px-4 text-font text-sm space-y-3 marker:font-medium">
|
||||
{
|
||||
Array.from({ length: selectedImportMethod.numberOfInstructions as number }, (_, i) => i).map((index: number) => (
|
||||
<li>
|
||||
<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"
|
||||
size="sm"
|
||||
/>
|
||||
}} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="flex flex-1 ">
|
||||
{
|
||||
|
||||
<ReactCodeMirror
|
||||
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
|
||||
extensions={[sql()]}
|
||||
value={sqlCode}
|
||||
onChange={setSqlCode}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
/>
|
||||
<ReactCodeMirror
|
||||
className="flex w-full min-h-[360px] max-h-[360px] border-1 rounded-md border-divider overflow-hidden"
|
||||
extensions={[sql()]}
|
||||
value={sqlCode}
|
||||
onChange={setSqlCode}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
@@ -12,21 +12,23 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
|
||||
const OpenDatabaseModal: React.FC<ModalProps> = (props) => {
|
||||
const { isOpen, onOpenChange } = props;
|
||||
const { t } = useTranslation();
|
||||
const { databases , currentDatabaseId} = useDatabase();
|
||||
const { switchDatabase } = useDatabaseOperations();
|
||||
const { databases, currentDatabaseId } = useDatabase();
|
||||
const { switchDatabase } = useDatabaseOperations();
|
||||
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(
|
||||
(() => currentDatabaseId ? new Set([currentDatabaseId]) : undefined)
|
||||
);
|
||||
|
||||
|
||||
const openDatabase = () => {
|
||||
switchDatabase( selectedDatabase.currentKey)
|
||||
switchDatabase(selectedDatabase.currentKey) ;
|
||||
onOpenChange && onOpenChange(false) ;
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.open_database")}
|
||||
@@ -35,19 +37,18 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
actionHandler={openDatabase}
|
||||
header={t("modals.open_database_header")}
|
||||
isDisabled={!selectedDatabase?.size}
|
||||
|
||||
>
|
||||
<Table
|
||||
aria-label="Example static collection table"
|
||||
color={"primary"}
|
||||
|
||||
|
||||
selectionMode="single"
|
||||
selectedKeys={selectedDatabase}
|
||||
onSelectionChange={setSelectedDatabase}
|
||||
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: "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 "
|
||||
}}
|
||||
>
|
||||
<TableHeader className="rounded-sm">
|
||||
@@ -62,8 +63,9 @@ const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
<TableRow key={database.id}>
|
||||
<TableCell>
|
||||
<Image
|
||||
src={getDatabaseByDialect(database.dialect).logo}
|
||||
src={getDatabaseByDialect(database.dialect).small_logo}
|
||||
width={24}
|
||||
radius="none"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="font-semibold">{database.name}</TableCell>
|
||||
|
||||
@@ -46,11 +46,9 @@ const Field: React.FC<Props> = (props) => {
|
||||
setEditMode(false);
|
||||
}, [fieldName])
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<div className={cn(
|
||||
"group relative flex h-8 items-center justify-between gap-1 border-t border-divider px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-primary/5 transition-all duration-200 ease-in-out",
|
||||
"group relative flex h-8 items-center justify-between gap-1 border-t border-divider px-3 text-sm last:rounded-b-[6px] hover:bg-default dark:hover:bg-primary/10 transition-all duration-200 ease-in-out",
|
||||
highlight ? "bg-primary/5" : ""
|
||||
)}>
|
||||
{
|
||||
@@ -70,7 +68,7 @@ const Field: React.FC<Props> = (props) => {
|
||||
<TooltipTrigger>
|
||||
<MessageSquareQuote className="size-3.5 text-icon" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<TooltipContent>
|
||||
{field.note}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
@@ -111,12 +109,12 @@ const Field: React.FC<Props> = (props) => {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
|
||||
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent text-font/90"
|
||||
className="rounded-sm outline-none px-2 py-0.5 w-full border-[0.5px] border-primary bg-content1 focus-visible:ring-0 text-sm dark:text-white"
|
||||
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-icon hover:text-font/90"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
onPress={saveFieldName}
|
||||
|
||||
@@ -124,10 +124,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
|
||||
markerEnd={`url(#${endMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
|
||||
`!stroke-1 ${selected ? '!stroke-primary' : 'stroke-default-600 dark:stroke-default-300'}`,
|
||||
|
||||
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
|
||||
@@ -114,11 +114,11 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
value={tableName}
|
||||
onBlur={saveTableName}
|
||||
type="text"
|
||||
className="rounded-md outline-none px-2 py-0.5 w-full border-[0.5px] border-primary-700 font-bold bg-slate-100 focus-visible:ring-0 text-sm dark:bg-transparent dark:text-white"
|
||||
className="rounded-sm outline-none px-2 py-0.5 w-full border-[0.5px] border-primary font-bold bg-content1 focus-visible:ring-0 text-sm dark:text-white"
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-icon hover:bg-primary-foreground hover:text-font/90"
|
||||
className="text-icon hover:bg-default hover:text-font/90"
|
||||
size="sm"
|
||||
onPressEnd={saveTableName}
|
||||
isIconOnly
|
||||
@@ -149,7 +149,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="size-6 p-0 text-icon hover:bg-primary-foreground "
|
||||
className="text-icon hover:bg-default hover:text-font/90 "
|
||||
isIconOnly
|
||||
onPressEnd={focus}
|
||||
>
|
||||
|
||||
@@ -19,13 +19,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
const [datatbaseState, { set, undo: undoChanges, redo: redoChanges, canUndo, canRedo }] = useUndo<DatabaseType>(database);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
udpateDbFlag.current = false;
|
||||
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
|
||||
const databaseHash: string = hash(database, { algorithm: 'sha1' });
|
||||
if (presentHash != databaseHash)
|
||||
set(database);
|
||||
|
||||
if (datatbaseState.present && database) {
|
||||
udpateDbFlag.current = false;
|
||||
const presentHash: string = hash(datatbaseState.present, { algorithm: 'sha1' });
|
||||
const databaseHash: string = hash(database, { algorithm: 'sha1' });
|
||||
if (presentHash != databaseHash)
|
||||
set(database);
|
||||
}
|
||||
}, [database]);
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if ( ! database || !datatbaseState.present )
|
||||
return ;
|
||||
const normalizedDatabase = normalizeDatabase(database);
|
||||
|
||||
const normalizedPresent = normalizeDatabase(datatbaseState.present);
|
||||
@@ -63,7 +64,7 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
|
||||
const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences);
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
try {
|
||||
await executeDbDiffOps(operations)
|
||||
setIsProcessing(false);
|
||||
}
|
||||
|
||||
@@ -19,9 +19,10 @@ interface DatabaseDataContextType {
|
||||
currentDatabaseId: string | undefined,
|
||||
databases: DatabaseType[],
|
||||
isLoading: boolean,
|
||||
isFetching : boolean ,
|
||||
isSwitchingDatabase: boolean,
|
||||
getField: (tableId: string, id: string) => FieldType | undefined,
|
||||
getDefaultPrimaryKeyType : (dialect? : DatabaseDialect) => DataType | undefined
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -30,6 +31,8 @@ interface DatabaseDataContextType {
|
||||
interface DatabaseOperationsContextType {
|
||||
data_types: DataType[],
|
||||
grouped_data_types : any ;
|
||||
isSwitchingDatabase : boolean ;
|
||||
getInteger : () => DataType | undefined ;
|
||||
// database operations
|
||||
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
|
||||
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
|
||||
|
||||
@@ -17,7 +17,6 @@ import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-s
|
||||
import { v4 } from "uuid";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { Modifiers } from "@/lib/field";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
|
||||
|
||||
@@ -27,12 +26,14 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
|
||||
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(localStorage.getItem("database_id") as string | undefined);
|
||||
// Fetch all databases
|
||||
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
|
||||
db.query.databases.findMany()
|
||||
const { data: databases, isLoading: loadingDatabases, isFetching: fetchingDatabases } = useQuery(toCompilableQuery(
|
||||
db.query.databases.findMany({
|
||||
orderBy: desc(databaseModel.createdAt)
|
||||
})
|
||||
));
|
||||
|
||||
// Fetch the current database with nested tables, fields, and relationships
|
||||
let { data: database, isLoading: loadingCurrentDatabase, isFetching } = useQuery(
|
||||
let { data: database, isLoading: loadingCurrentDatabase, isFetching: fetchingDatabase } = useQuery(
|
||||
toCompilableQuery(
|
||||
db.query.databases.findMany({
|
||||
where: (databases, { eq }) => eq(databases.id, currentDatabaseId as string),
|
||||
@@ -84,49 +85,31 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
database = undefined as any;
|
||||
|
||||
// Fetch all data types
|
||||
let { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
|
||||
let { data: data_types, isLoading: loadingDataTypes, isFetching: fetchingDatatypes } = useQuery(toCompilableQuery(
|
||||
db.query.data_types.findMany({
|
||||
where: (data_types, { eq }) => eq(data_types.dialect, (database as any)?.dialect)
|
||||
})
|
||||
));
|
||||
|
||||
|
||||
const charsetOrCollationDataTypes = data_types.filter((dataType: DataType) => {
|
||||
const modifiers: string[] | undefined = dataType.modifiers ? JSON.parse(dataType.modifiers) : undefined;
|
||||
if (modifiers) {
|
||||
return modifiers.includes(Modifiers.COLLATE || Modifiers.CHARSET)
|
||||
}
|
||||
}).map((dataType: DataType) => dataType.name?.toUpperCase());
|
||||
|
||||
|
||||
|
||||
|
||||
const grouped_data_types: any = useMemo(() => {
|
||||
return groupBy(data_types, "type");
|
||||
}, [data_types]);
|
||||
|
||||
|
||||
// Auto-select first database if none is selected
|
||||
useEffect(() => {
|
||||
if (databases.length > 0 && !currentDatabaseId) {
|
||||
switchDatabase(databases[0].id);
|
||||
}
|
||||
}, [currentDatabaseId, databases]);
|
||||
|
||||
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
|
||||
const isSwitchingDatabase: boolean = useMemo(() => {
|
||||
return isFetching && currentDatabaseId != (database as any)?.id
|
||||
}, [currentDatabaseId, database, isFetching]);
|
||||
const isFetching: boolean = fetchingDatabase || fetchingDatatypes || fetchingDatabases;
|
||||
|
||||
|
||||
|
||||
const isSwitchingDatabase: boolean = useMemo(() => {
|
||||
return fetchingDatabase && currentDatabaseId != (database as any)?.id
|
||||
}, [currentDatabaseId, database, fetchingDatabase]);
|
||||
|
||||
// CRUD for database
|
||||
const createDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
|
||||
|
||||
return await db.insert(databaseModel).values({
|
||||
...database,
|
||||
createdAt: getTimestamp()
|
||||
});
|
||||
|
||||
}, [db]);
|
||||
|
||||
const editDatabase = useCallback(async (database: DatabaseInsertType): Promise<QueryResult> => {
|
||||
@@ -371,19 +354,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
|
||||
const getDefaultPrimaryKeyType = useCallback((dialect?: DatabaseDialect) => {
|
||||
if (dialect == DatabaseDialect.POSTGRES)
|
||||
return data_types.find((dataType: DataType) => dataType.name == "bigserial");
|
||||
else if (dialect == DatabaseDialect.SQLITE)
|
||||
return data_types.find((dataType: DataType) => dataType.name == "integer");
|
||||
|
||||
return data_types.find((dataType: DataType) => dataType.name == "bigint");
|
||||
|
||||
const getInteger = useCallback(() => {
|
||||
return data_types.find((dataType: DataType) => dataType.name == "integer");
|
||||
}, [data_types]);
|
||||
|
||||
|
||||
|
||||
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
|
||||
if (currentDatabaseId) {
|
||||
return await db.transaction(async (tx) => {
|
||||
@@ -428,7 +402,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}, [currentDatabaseId])
|
||||
|
||||
const databaseOpsValue = useMemo(() => ({
|
||||
|
||||
isSwitchingDatabase,
|
||||
createDatabase,
|
||||
editDatabase,
|
||||
deleteDatabase,
|
||||
@@ -453,8 +427,10 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
editFieldIndices,
|
||||
importDatabase,
|
||||
data_types,
|
||||
grouped_data_types
|
||||
getInteger,
|
||||
grouped_data_types,
|
||||
}), [
|
||||
isSwitchingDatabase,
|
||||
createDatabase,
|
||||
editDatabase,
|
||||
deleteDatabase,
|
||||
@@ -478,6 +454,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
editFieldIndices,
|
||||
importDatabase,
|
||||
data_types,
|
||||
getInteger,
|
||||
grouped_data_types
|
||||
]);
|
||||
|
||||
@@ -490,18 +467,23 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
databases: databases as DatabaseType[],
|
||||
isLoading,
|
||||
isSwitchingDatabase,
|
||||
getDefaultPrimaryKeyType,
|
||||
isFetching,
|
||||
getField,
|
||||
}}>
|
||||
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
|
||||
{
|
||||
!database && !isLoading &&
|
||||
/* !database && !isLoading &&
|
||||
<>
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
{
|
||||
database && !isLoading && !isSwitchingDatabase &&
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>*/
|
||||
}
|
||||
{
|
||||
<DatabaseHistoryProvider>
|
||||
{children}
|
||||
</DatabaseHistoryProvider>
|
||||
|
||||
+16
-31
@@ -5,7 +5,7 @@
|
||||
|
||||
|
||||
.text-editable {
|
||||
@apply dark:group-hover:bg-background group-hover:bg-slate-100 group-hover:ring-[0.5px] group-hover:ring-primary dark:group-hover:ring-divider rounded-sm cursor-pointer;
|
||||
@apply dark:group-hover:bg-content1 group-hover:bg-background border-[0.5px] border-transparent group-hover:border-primary-300 dark:group-hover:ring-divider rounded-sm cursor-pointer;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,17 @@ body {
|
||||
|
||||
}
|
||||
|
||||
|
||||
.selectable {
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
/* Safari */
|
||||
-moz-user-select: text;
|
||||
/* Firefox */
|
||||
-ms-user-select: text;
|
||||
/* IE/Edge */
|
||||
}
|
||||
|
||||
.react-flow__renderer {
|
||||
cursor: default !important;
|
||||
/* or pointer, grab, etc. */
|
||||
@@ -118,16 +129,7 @@ div[data-slot="content"] hr[role="separator"] {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
svg.text-icon:hover {
|
||||
|
||||
color: #333639
|
||||
}
|
||||
|
||||
|
||||
|
||||
.dark .bg-background-50 {
|
||||
background-color: red !important;
|
||||
}
|
||||
|
||||
|
||||
.database-checkbox[data-selected="true"] {
|
||||
@@ -148,14 +150,9 @@ thead[role="rowgroup"] th:last-child {
|
||||
|
||||
thead[role="rowgroup"] th:first-child {
|
||||
border-radius: 4px 0px 0px 4px !important;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
tbody[role="rowgroup"] tr td::before {
|
||||
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -202,11 +199,7 @@ button[data-testid="remove"]:after {
|
||||
}
|
||||
|
||||
button[data-testid="remove"] svg {
|
||||
|
||||
|
||||
|
||||
display: none;
|
||||
|
||||
}
|
||||
|
||||
[data-invalid="true"] div[data-slot="input-wrapper"] {
|
||||
@@ -215,16 +208,8 @@ button[data-testid="remove"] svg {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.option-checkbox[data-selected="true"] .option-span {
|
||||
border-color: hsl(var(--heroui-divider));
|
||||
|
||||
div[data-slot="calendar"] div[data-slot="input-wrapper"]{
|
||||
background-color: hsl(var(--heroui-default));
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
border : hsl(var(--heroui-divider))
|
||||
}
|
||||
@@ -71,4 +71,11 @@ export const getForeignRelationships = (table: TableType): RelationshipType[] =>
|
||||
) : []);
|
||||
|
||||
return foreignRelationships;
|
||||
}
|
||||
|
||||
|
||||
export const getDefaultRelationshipName = (relationship: RelationshipType) => {
|
||||
if (!relationship.sourceTable || !relationship.targetTable || !relationship.sourceField || !relationship.targetField)
|
||||
return "";
|
||||
return `fk_${relationship.sourceTable?.name}_${relationship.targetTable?.name}`
|
||||
}
|
||||
@@ -112,35 +112,66 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const createTable of createTableStatements) {
|
||||
let foreignKeyConstraints: any[] = [];
|
||||
let referenceDefinitions: any[] = [];
|
||||
|
||||
for (let createTable of createTableStatements) {
|
||||
|
||||
try {
|
||||
const instructionAst = parser.astify(createTable, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
|
||||
if (dialect == DatabaseDialect.SQLITE)
|
||||
createTable = createTable.replace(/\btext\s*\(\s*\d+\s*\)/gi, 'TEXT');
|
||||
|
||||
|
||||
let instructionAst = parser.astify(createTable, {
|
||||
database: dialect == DatabaseDialect.MARIADB ? getDatabaseByDialect(DatabaseDialect.MYSQL).name : getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
console.log(instructionAst[0])
|
||||
const table: TableInsertType = astToTable(instructionAst[0], data_types);
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
console.log (instructionAst)
|
||||
if (instructionAst) {
|
||||
const table: TableInsertType = astToTable(instructionAst, data_types);
|
||||
|
||||
tables.push(table);
|
||||
|
||||
const foreignKeyConstraints = (instructionAst[0] as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
|
||||
const referenceDefinitions = (instructionAst[0] as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
|
||||
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);
|
||||
|
||||
|
||||
foreignKeyConstraints = foreignKeyConstraints.concat(
|
||||
tableForeignKeyConstraints.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
|
||||
referenceDefinitions = referenceDefinitions.concat(
|
||||
tableReferenceDefinitions.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
relationships.push(astToRelationship(tables, table, foreignKeyConstraint) as RelationshipInsertType);
|
||||
}
|
||||
for (const referenceDefinition of referenceDefinitions) {
|
||||
relationships.push(astToRelationship(tables, table, undefined, referenceDefinition) as RelationshipInsertType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
// console.log (error)
|
||||
continue;
|
||||
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const referenceDefinition of referenceDefinitions) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,8 +181,10 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
if (instructionAst) {
|
||||
const targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == (instructionAst as any).table?.[0].table)
|
||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, targetTable, undefined, undefined, instructionAst) as RelationshipInsertType[];
|
||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
|
||||
...instructionAst,
|
||||
table: (instructionAst as any).table?.[0].table
|
||||
}) as RelationshipInsertType[];
|
||||
|
||||
relationships = relationships.concat(extractedRelationships)
|
||||
}
|
||||
@@ -181,8 +214,6 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
return { tables, relationships, indices };
|
||||
|
||||
}
|
||||
@@ -191,23 +222,28 @@ export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: Data
|
||||
|
||||
|
||||
|
||||
export const astToRelationship = (tables: TableInsertType[], targetTable?: TableInsertType, constrainttAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
|
||||
export const astToRelationship = (tables: TableInsertType[], constraintAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
|
||||
|
||||
let targetField: FieldInsertType | undefined;
|
||||
let sourceTable: TableInsertType | undefined;
|
||||
let sourceField: FieldInsertType | undefined;
|
||||
let targetTable: TableInsertType | undefined;
|
||||
|
||||
|
||||
let relationships: RelationshipInsertType[] = [];
|
||||
|
||||
if (constrainttAst) {
|
||||
if (constraintAst) {
|
||||
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constrainttAst.definition?.[0].column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == constrainttAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constrainttAst.reference_definition?.definition?.[0].column);
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == constraintAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.definition?.[0].column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == constraintAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.reference_definition?.definition?.[0].column);
|
||||
|
||||
}
|
||||
|
||||
if (columnAst) {
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == columnAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.column?.column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == columnAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.reference_definition?.definition?.[0].column);
|
||||
@@ -216,7 +252,7 @@ export const astToRelationship = (tables: TableInsertType[], targetTable?: Table
|
||||
if (alterTableAst) {
|
||||
const expressions = alterTableAst.expr;
|
||||
const foreignKeyExpressions = expressions.filter((expression: any) => expression.resource == "constraint" && expression.create_definitions?.constraint_type == "FOREIGN KEY")
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == alterTableAst.table?.[0].table);
|
||||
for (const expression of foreignKeyExpressions) {
|
||||
|
||||
const targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.definition?.[0].column);
|
||||
@@ -265,7 +301,7 @@ export const astToRelationship = (tables: TableInsertType[], targetTable?: Table
|
||||
|
||||
|
||||
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
|
||||
console.log(ast)
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.table[0]?.table,
|
||||
@@ -295,10 +331,6 @@ export const astToField = (ast: any, data_types: DataType[], sequence: number):
|
||||
let charset: string | undefined;
|
||||
let collate: string | undefined;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
let defaultValue: string | undefined = ast.default_val?.value?.value ? ast.default_val?.value?.value : undefined;
|
||||
|
||||
let maxLength: number | null = null;
|
||||
|
||||
+4
-12
@@ -20,8 +20,6 @@ const adjustTablesPositions = async (
|
||||
const tables: TableType[] = nodes.map((node: Node) => (
|
||||
{ ...node.data.table as TableType }
|
||||
)) as TableType[];
|
||||
|
||||
|
||||
// Build the ELK graph structure
|
||||
const graph = {
|
||||
id: "root",
|
||||
@@ -29,6 +27,7 @@ const adjustTablesPositions = async (
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.layered.spacing.nodeNodeBetweenLayers': '100',
|
||||
'elk.spacing.nodeNode': '80',
|
||||
|
||||
},
|
||||
children: nodes.map((node) => ({
|
||||
id: node.id,
|
||||
@@ -40,28 +39,21 @@ const adjustTablesPositions = async (
|
||||
sources: [rel.sourceTableId],
|
||||
targets: [rel.targetTableId],
|
||||
})),
|
||||
};
|
||||
};
|
||||
// Run ELK layout (async)
|
||||
const layoutedGraph = await elk.layout(graph);
|
||||
|
||||
// Map positions back to your tables
|
||||
tables.forEach((table) => {
|
||||
const node = layoutedGraph?.children?.find((n) => n.id === table.id);
|
||||
if (node) {
|
||||
// ELK positions are top-left, adjust to center like before
|
||||
table.posX = node.x || 0 + (node.width / 2) + 112;
|
||||
table.posY = node.y || 0 + (node.height / 2) + 75;
|
||||
table.posX = (node.x || 0);
|
||||
table.posY = (node.y || 0);
|
||||
}
|
||||
});
|
||||
|
||||
return tables;
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const isTablesOverlapping = (tableA: TableType, tableB: TableType) => {
|
||||
const tableAWidth: number = 224;
|
||||
const tableAHeight: number = tableA.fields.length * 32 + 36;
|
||||
|
||||
+1
-1
@@ -39,7 +39,7 @@ export default {
|
||||
DEFAULT: "#cecfd2"
|
||||
},
|
||||
content1: {
|
||||
DEFAULT: "#1c2026"
|
||||
DEFAULT: "#20252c"
|
||||
},
|
||||
|
||||
default: {
|
||||
|
||||
Reference in New Issue
Block a user