Create new Database

This commit is contained in:
KarimTamani
2025-06-01 23:12:06 +01:00
parent adddbcf5f9
commit 5d821a9865
28 changed files with 616 additions and 229 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+12 -11
View File
@@ -9,6 +9,7 @@ import DatabaseProvider from "./providers/database-provider/database-provider";
import DiagramProvider from "./providers/diagram-provider/diagram-provider";
import { ToastProvider } from "@heroui/react";
import { ModalProvider } from "./providers/modal-provider/modal-provider";
@@ -17,22 +18,22 @@ function App() {
const appRoutes = useAppRoutes();
return (
<>
<ToastProvider placement="bottom-right"/>
<SyncProvider>
<ReactFlowProvider>
<DatabaseProvider>
<DiagramProvider>
<ToastProvider placement="bottom-right" />
<SyncProvider>
<ReactFlowProvider>
<DatabaseProvider>
<DiagramProvider>
<ModalProvider>
<TooltipProvider delayDuration={0}>
{appRoutes}
</TooltipProvider>
</ModalProvider>
</DiagramProvider>
</DatabaseProvider>
</ReactFlowProvider>
</SyncProvider>
</DiagramProvider>
</DatabaseProvider>
</ReactFlowProvider>
</SyncProvider>
</>
);
}
@@ -0,0 +1,37 @@
import { DatabaseType } from "@/lib/database"
import { Checkbox, Image } from "@heroui/react";
interface DatabaseCheckboxProps {
database: DatabaseType,
}
const DatabaseCheckbox: React.FC<DatabaseCheckboxProps> = ({ database }) => {
return (
<Checkbox
aria-label={database.name}
value={database.dialect}
className="database-checkbox"
classNames={{
base: "flex min-w-[128px] min-h-[128px] max-w-[128px] max-h-[128px] hover:bg-default rounded-md relative",
wrapper: "absolute top-2 left-2 " ,
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
src={database.logo}
className="w-full"
/>
</div>
</Checkbox>
)
}
export default DatabaseCheckbox;
+2 -2
View File
@@ -60,7 +60,7 @@ const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHand
{child.title}
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider dark:bg-font/10"></div>
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
}
</div>
:
@@ -68,7 +68,7 @@ const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, clickHand
<SubmenuDropdown {...child} />
{
child.divide &&
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider dark:bg-font/10"></div>
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-divider "></div>
}
</div>
+16 -3
View File
@@ -1,8 +1,10 @@
import { useTheme } from "next-themes";
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
import React, { useMemo } from "react";
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
@@ -20,12 +22,23 @@ const Menu: React.FC<MenuProps> = ({ }) => {
const { setTheme } = useTheme()
const { t } = useTranslation();
const { open } = useModal();
const menu: MenuDropdownProps[] = useMemo(() => [
{
title: t("menu.file"),
children: [
{ title: t("menu.new") },
{ title: t("menu.open"), shortcut: "Ctnl + O" },
{
title: t("menu.new"),
clickHandler: () => {
open(Modals.CREATE_DATABASE)
}
},
{
title: t("menu.open"), shortcut: "Ctnl + O", clickHandler: () => {
open(Modals.OPEN_DATABASE)
}
},
{ title: t("menu.save"), shortcut: "Ctnl + S", divide: true },
{
title: t("menu.import"),
+22 -11
View File
@@ -1,5 +1,5 @@
import React, { ReactNode } from "react";
import React, { ReactNode, useState } from "react";
import {
Modal as HeroUiModal,
ModalContent,
@@ -20,20 +20,24 @@ export interface ModalProps {
title: string,
children: ReactNode,
actionName?: string,
actionHandler?: () => void ,
isDisabled? : boolean
actionHandler?: () => void,
isDisabled?: boolean,
header?: string
}
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action" , actionHandler , isDisabled}) => {
const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop = "opaque", title, children, actionName = "Action", header, actionHandler, isDisabled }) => {
const targetRef = React.useRef(null);
const { moveProps } = useDraggable({ targetRef, canOverflow: true, isDisabled: !isOpen });
const [isLoading , setIsLoading] = useState<boolean>( false ) ;
const {t} = useTranslation() ;
const { t } = useTranslation();
const handleAction = (onClose: () => void) => {
actionHandler && actionHandler() ;
const handleAction = async (onClose: () => void) => {
setIsLoading( true)
actionHandler && await actionHandler();
setIsLoading( false) ;
onClose()
}
return (
@@ -44,13 +48,20 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
className={className}
backdrop={backdrop}
radius="sm"
>
<ModalContent>
{(onClose) => (
<>
<ModalHeader {...moveProps} className="flex flex-row gap-1" >
<ModalHeader {...moveProps} className=" flex flex-col gap-1" >
{title}
{
header &&
<p className="text-sm text-font/70 block">
{header}
</p>
}
</ModalHeader>
<ModalBody>
{children}
@@ -58,9 +69,9 @@ const Modal: React.FC<ModalProps> = ({ isOpen, onOpenChange, className, backdrop
<ModalFooter>
<div className="flex w-full justify-between">
<Button color="danger" variant="light" onPress={onClose} size="sm">
{t("modal.close")}
{t("modals.close")}
</Button>
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled}>
<Button color="primary" onPress={() => handleAction(onClose)} size="sm" isDisabled={isDisabled || isLoading} isLoading={isLoading}>
{actionName}
</Button>
</div>
+5 -13
View File
@@ -3,10 +3,11 @@ 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 { compare } from "fast-json-patch";
import hash from "object-hash";
export const useTableToNode = (tables: TableType[]): void => {
const { setNodes } = useReactFlow();
const { setNodes } = useReactFlow();
useEffect(() => {
@@ -38,19 +39,10 @@ export const useTableToNode = (tables: TableType[]): void => {
if (!node)
return tableNode;
else {
// const diff = compare(node.data.table as TableType, tableNode.data.table as TableType);
//console.log (diff)
const hashNode: string = hash(node.data.table as TableType);
const hashTableNode: string = hash(tableNode.data.table as TableType);
return hashNode == hashTableNode ? node : tableNode;
}
})
+1 -1
View File
@@ -28,7 +28,7 @@ i18n.use(LanguageDetector)
.use(initReactI18next)
.init({
resources ,
lng: 'en',
lng: 'fr',
interpolation: {
escapeValue: false,
+17 -5
View File
@@ -8,10 +8,7 @@ export const en = {
tables: "Tables",
relationships: "Relationships"
},
modal: {
close: "Close",
create: "Create"
},
color_picker: {
default_color: "Default color"
},
@@ -117,8 +114,23 @@ export const en = {
dark: "Dark",
help: "Help",
show_docs: "Show Docs",
join_discord: "Join Discord"
join_discord: "Join Discord",
},
modals: {
close: "Close",
create: "Create" ,
pick_database: "Pick your Database.",
create_database_header: "Every database offers distinct features and functionalities." ,
db_name : "Database name" ,
db_name_error : "Please provide a Database name" ,
continue : "Continue" ,
open : "Open" ,
open_database : "Open Database" ,
open_database_header : "Open a database by selecting one from the list."
}
}
}
+32
View File
@@ -0,0 +1,32 @@
export interface DatabaseType {
name: string,
dialect: string;
logo: string
}
export const DBTypes: DatabaseType[] = [
{
name: "PostgreSql",
dialect: "postgres",
logo: "/postgresql_logo.png"
}, {
name: "Mysql",
dialect: "mysql",
logo: "/mysql_logo.png"
},
{
name: "Sqlite",
dialect: "sqlite",
logo: "/sqlite_logo.png"
},
]
export const getDatabaseByDialect = (dialect: string): DatabaseType => {
const dbType : DatabaseType | undefined = DBTypes.find((dbType : DatabaseType) => dbType.dialect == dialect) ;
return dbType ? dbType : DBTypes[0] ;
}
+11 -4
View File
@@ -1,5 +1,5 @@
import { InferInsertModel, InferSelectModel, relations } from 'drizzle-orm';
import { sqliteTable, text } from 'drizzle-orm/sqlite-core';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { tables, TableType } from './table-schema';
import { relationships, RelationshipType } from './relationship-schema';
@@ -10,6 +10,13 @@ export const databases = sqliteTable('databases', {
.notNull()
.unique(),
name: text('name').notNull(),
dialect: text("dialect", {
enum: ["postgres", "mysql", "sqlite"],
}).notNull().default("postgres"),
numOfTables: integer("numOfTables")
.notNull()
.default(0),
createdAt: text('createdAt'),
});
@@ -20,7 +27,7 @@ export const databaseRelations = relations(databases, ({ many }) => ({
export interface DatabaseType extends InferSelectModel<typeof databases> {
tables : TableType[] ,
relationships : RelationshipType[]
};
tables: TableType[],
relationships: RelationshipType[]
};
export interface DatabaseInsertType extends InferInsertModel<typeof databases> { };
+74 -69
View File
@@ -40,6 +40,7 @@ import { useTheme } from "next-themes";
const DatabasePage: React.FC = () => {
const { t } = useTranslation();
const { resolvedTheme } = useTheme();
// Extract database state and operations
@@ -102,7 +103,7 @@ const DatabasePage: React.FC = () => {
) as NodePositionChange[];
const nodeRemoveChanges: NodeRemoveChange[] = changes.filter((change: NodeChange) => change.type == "remove");
// Save new positions to the database
if (nodePositionChanges.length > 0)
await updateTablePositions(nodePositionChanges.map((change: NodePositionChange) => ({
@@ -112,9 +113,10 @@ const DatabasePage: React.FC = () => {
} as TableInsertType)));
// Delete tables if removed
if (nodeRemoveChanges.length > 0)
if (nodeRemoveChanges.length > 0) {
deleteMultiTables(nodeRemoveChanges.map((change: NodeRemoveChange) => change.id));
}
return onNodesChange(changes);
}, [onNodesChange]);
@@ -142,9 +144,9 @@ const DatabasePage: React.FC = () => {
// Automatically reposition tables to avoid overlap and fit the view
const adjustPositions = useCallback(async () => {
const adjustedTables = await adjustTablesPositions(nodes, relationships);
console.log (adjustedTables )
updateTablePositions(adjustedTables)
const adjustedTables = await adjustTablesPositions(nodes, relationships);
updateTablePositions(adjustedTables)
setTimeout(() => {
fitView({
duration: 500
@@ -152,86 +154,89 @@ const DatabasePage: React.FC = () => {
}, 300);
}, [relationships, nodes]);
// Convert tables and relationships into flow elements
useTableToNode(tables);
useRelationshipToEdge(relationships);
useHighlightedEdges(nodes, relationships, edges);
const { isOverlapping, puls } = useOverlappingTables(tables);
return (
<div className="w-full h-screen flex relative overflow-hidden">
<div className="flex max-w-full">
<DBController />
</div>
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
}}
// onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
{
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none "
<div className="relative w-full h-full ">
<ReactFlow
colorMode={resolvedTheme as ColorMode}
nodes={nodes}
edges={edges}
fitView
className="w-full h-full cursor-default "
onNodesChange={handleNodesChanges}
onEdgesChange={handleEdgeChanges}
onConnect={onConnect}
defaultEdgeOptions={{
type: 'relationship-edge',
}}
// onlyRenderVisibleElements
panOnDrag={true}
zoomOnScroll={true}
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
snapGrid={[20, 20]}
onConnectStart={onConnectStart}
onConnectEnd={onConnectEnd}
>
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<Controls
position="bottom-center"
showFitView={false}
showZoom={false}
showInteractive={false}
className="shadow-none "
>
<Background className=" dark:bg-background-100" />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="shadow"
size="sm"
isIconOnly
color="danger"
className="size-8 p-1 "
onPressEnd={puls}
>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
<DatabaseControlButtons
adjustPositions={adjustPositions}
/>
</Controls >
<Background className=" dark:bg-background-100" />
</ReactFlow>
<div
className="absolute left-[24px] bottom-[24px] "
>
{
isOverlapping &&
<Tooltip>
<TooltipTrigger asChild>
<span>
<Button
variant="shadow"
size="sm"
isIconOnly
color="danger"
className="size-8 p-1 "
onPressEnd={puls}
>
<AlertTriangle className="size-4 text-white" />
</Button>
</span>
</TooltipTrigger>
<TooltipContent>
{t("table.overlapping_tables")}
</TooltipContent>
</Tooltip>
}
</div>
</div>
</div>
}
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<CardinalityMarker type="one" direction="start" />
@@ -5,28 +5,27 @@ import { Ref, useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import RelationshipAccordionHeader from "./relationship-accordion-item/relationship-accordion-header";
import RelationshipAccordionBody from "./relationship-accordion-item/relationship-accordion-body";
import Modal from "@/components/modal/modal";
import CreateRelationshipForm from "./create-relationship-form/create-relationship-form";
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 { v4 } from "uuid";
import { useDiagram } from "@/providers/diagram-provider/diagram-provider";
import { getDefaultRelationshipName } from "@/hooks/use-relationship-name";
import { useModal } from "@/providers/modal-provider/modal-provider";
import { Modals } from "@/providers/modal-provider/modal-contxet";
interface Props {
}
const RelationshipController: React.FC<Props> = ({ }) => {
const [relationship, setRelationship] = useState<RelationshipInsertType | undefined>(undefined);
const [isValid, setIsValid] = useState<boolean>(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
const RelationshipController: React.FC = ({ }) => {
const { open } = useModal();
const { database } = useDatabase();
const { createRelationship } = useDatabaseOperations();
const { relationships: allRelationships } = database;
const [relationships, setRelationships] = useState<RelationshipType[]>(allRelationships);
@@ -37,18 +36,6 @@ const RelationshipController: React.FC<Props> = ({ }) => {
useEffect(() => setRelationships(allRelationships), [allRelationships]);
const addRelationship = useCallback(() => {
const newRelationshipId: string = v4();
createRelationship({
id: newRelationshipId,
...relationship,
} as RelationshipInsertType);
setSelectedRelationship(new Set([newRelationshipId]) as any);
}, [relationship]);
useEffect(() => {
if (focusedRelationshipId) {
@@ -57,6 +44,11 @@ const RelationshipController: React.FC<Props> = ({ }) => {
}, [focusedRelationshipId]);
const onOpen = useCallback(() => {
open(Modals.CREATE_RELATIONSHIP, {
onRlationshipCreated: (id: string) => setSelectedRelationship(new Set([id]) as any)
})
}, [])
const searchRelationships = useCallback(() => {
const keyword: string | undefined = nameRef.current?.value;
@@ -164,21 +156,7 @@ const RelationshipController: React.FC<Props> = ({ }) => {
))}
</Accordion>
</div>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("db_controller.create_relationship")}
actionName={t("modal.create")}
className="min-w-[520px]"
isDisabled={!isValid}
actionHandler={addRelationship}
>
<CreateRelationshipForm
onRelationshipChanges={setRelationship}
onValidationChanges={setIsValid}
/>
</Modal>
</div>
)
}
@@ -147,12 +147,12 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<EllipsisVertical className="size-4 " />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[210px] bg-background" >
<PopoverContent className="w-[210px] " >
<div className="w-full flex flex-col gap-2 p-2 ">
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_setting")}
</h3>
<hr className="border-divider dark:border-font/10" />
<hr className="border-divider" />
<div className="flex w-full justify-between">
<span className="text-sm text-icon font-medium dark:text-font/90">
{t("db_controller.unique")}
@@ -174,11 +174,11 @@ const FieldItem: React.FC<Props> = ({ field }) => {
onValueChange={setNote}
onBlur={updateFieldNote}
classNames={{
inputWrapper: "bg-default border-divider dark:bg-background-100",
inputWrapper: "bg-default border-divider ",
base: "max-w-xs",
input: "resize-y min-h-[60px] max-h-[180px]",
}} />
<hr className="border-divider dark:border-font/10" />
<hr className="border-divider" />
<Button
className="bg-default dark:bg-danger dark:border-none dark:text-white"
@@ -111,7 +111,7 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.index_setting")}
</h3>
<hr className="border-divider dark:border-font/10" />
<hr className="border-divider " />
<label className="text-sm font-medium text-icon dark:text-font/90">
{t("db_controller.name")}
@@ -123,13 +123,13 @@ const IndexItem: React.FC<Props> = ({ index, fields }) => {
onBlur={editIndexName}
size="sm"
classNames={{
inputWrapper: "border-divider group-hover:border-primary dark:border-font/10",
inputWrapper: "border-divider group-hover:border-primary ",
}}
defaultValue={index.name}
/>
<hr className="border-divider dark:border-font/10" />
<hr className="border-divider" />
<Button
className="bg-default dark:bg-danger dark:border-none dark:text-white"
@@ -0,0 +1,129 @@
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 { Button, CheckboxGroup, Input } from "@heroui/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);
const { t } = useTranslation();
const [selectedDbType, setSelectedDbType] = useState<string[]>([DBTypes[0].dialect]);
const [dbName, setDbName] = useState<string>("db_example");
const { createDatabase, setCurrentDatabaseId } = useDatabaseOperations();
const onDatabaseTypeChange = (types: string[]) => {
const selectedType: string | undefined = types.pop();
if (selectedType && selectedType != selectedDbType?.[0])
setSelectedDbType([selectedType]);
}
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
});
setCurrentDatabaseId(databaseId);
res(databaseId)
})
}, [selectedDbType, dbName])
useEffect(() => {
setIsValid((selectedDbType.length > 0 && dbName.trim().length > 0) as boolean)
}, [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 " />
}
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
</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"
>
<SquareMenu className="size-4" /> Check examples
</Button>
<Button
variant="bordered"
size="sm"
className="w-full text-font border-divider"
>
<span className="underline">
Empty Diagram
</span>
</Button>
</div>
</div>
</div>
</Modal>
)
}
@@ -1,23 +1,27 @@
import Autocomplete from "@/components/auto-complete/auto-complete";
import Modal, { ModalProps } from "@/components/modal/modal";
import { FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
import { TableType } from "@/lib/schemas/table-schema";
import { useDatabase } from "@/providers/database-provider/database-provider";
import { FileKey, FileMinus2, FileOutput, KeyRound } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { FileKey, FileMinus2, FileOutput, KeyRound } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { v4 } from "uuid";
interface CreateRelationshipFormProps {
onRelationshipChanges: (relationship: RelationshipInsertType) => void,
onValidationChanges?: (isValid: boolean) => void
interface CreateRelationshipModalProps extends ModalProps {
onRelationshipChanges?: (relationship: RelationshipInsertType) => void,
onRlationshipCreated?: (id: string) => void
}
const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelationshipChanges, onValidationChanges }) => {
const CreateRelationshipModal: React.FC<CreateRelationshipModalProps> = ({ onRelationshipChanges, isOpen, onOpenChange, onRlationshipCreated }) => {
const [isValid, setIsValid] = useState<boolean>(false);
const { createRelationship } = useDatabaseOperations();
const [relationship, setRelationship] = useState<RelationshipInsertType>({
sourceTableId: "",
@@ -62,15 +66,39 @@ const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelat
}, [relationship, sourceFields, targetFields]);
useEffect(() => {
onValidationChanges && onValidationChanges((relationship.sourceFieldId && relationship.targetFieldId && fieldTypesMatches) as boolean)
}, [relationship, fieldTypesMatches])
setIsValid((relationship.sourceFieldId && relationship.targetFieldId && fieldTypesMatches) as boolean)
}, [relationship, fieldTypesMatches]);
const addRelationship = useCallback(() => {
const id: string = v4();
createRelationship({
...relationship,
id,
} as RelationshipInsertType);
onRlationshipCreated && onRlationshipCreated(id);
}, [relationship]);
return (
<>
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("db_controller.create_relationship")}
actionName={t("modals.create")}
className="min-w-[520px]"
isDisabled={!isValid}
actionHandler={addRelationship}
>
<div className="grid grid-cols-2 grid-rows-2 gap-4">
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileOutput className="size-4 text-icon" />
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
<FileOutput className="size-4" />
{t("db_controller.source_table")}
</label>
<Autocomplete
@@ -80,8 +108,8 @@ const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelat
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4 text-icon" />
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
<FileMinus2 className="size-4" />
{t("db_controller.target_table")}
</label>
@@ -92,8 +120,8 @@ const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelat
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<KeyRound className="size-4 text-icon" />
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
<KeyRound className="size-4 " />
{t("db_controller.primary_key")}
</label>
@@ -106,8 +134,8 @@ const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelat
/>
</div>
<div className="space-y-2">
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
<FileKey className="size-4 text-icon" />
<label className="font-medium flex text-font/90 flex items-center gap-1 text-sm">
<FileKey className="size-4 " />
{t("db_controller.foreign_key")}
</label>
<Autocomplete
@@ -127,9 +155,10 @@ const CreateRelationshipForm: React.FC<CreateRelationshipFormProps> = ({ onRelat
</div>
}
</>
</Modal>
)
}
export default CreateRelationshipForm;
export default CreateRelationshipModal;
@@ -0,0 +1,84 @@
import Modal, { ModalProps } from "@/components/modal/modal";
import { getDatabaseByDialect } from "@/lib/database";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Image, Selection, Table, TableBody, TableCell, TableColumn, TableHeader, TableRow } from "@heroui/react";
import { Key, useState } from "react";
import { useTranslation } from "react-i18next";
const OpenDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
const { t } = useTranslation();
const { databases } = useDatabase();
const { setCurrentDatabaseId } = useDatabaseOperations();
const [selectedDatabase, setSelectedDatabase] = useState<any | undefined>(undefined);
const openDatabase = () => {
setCurrentDatabaseId( selectedDatabase.currentKey)
}
return (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
title={t("modals.open_database")}
actionName={t("modals.open")}
className="min-w-[860px]"
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 border-sm"
}}
>
<TableHeader>
<TableColumn>Dialect</TableColumn>
<TableColumn>Name</TableColumn>
<TableColumn>Created at</TableColumn>
<TableColumn>Tables</TableColumn>
</TableHeader>
<TableBody>
{
databases.map((database: DatabaseType) => (
<TableRow key={database.id}>
<TableCell>
<Image
src={getDatabaseByDialect(database.dialect).logo}
width={24}
/>
</TableCell>
<TableCell>{database.name}</TableCell>
<TableCell>{
new Date(database.createdAt as string).toLocaleString("en-US")
}</TableCell>
<TableCell>{database.numOfTables}</TableCell>
</TableRow>
))
}
</TableBody>
</Table>
</Modal>
)
}
export default OpenDatabaseModal;
@@ -42,10 +42,13 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
}, [redoChanges, udpateDbFlag, isProcessing]);
useEffect(() => {
if (!udpateDbFlag.current) {
udpateDbFlag.current = true;
return;
}
const normalizedDatabase = normalizeDatabase(database);
const normalizedPresent = normalizeDatabase(datatbaseState.present);
@@ -1,6 +1,6 @@
import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { DatabaseInsertType, DatabaseType } from "@/lib/schemas/database-schema";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { IndexInsertType } from "@/lib/schemas/index-schema";
import { RelationshipInsertType } from "@/lib/schemas/relationship-schema";
@@ -15,6 +15,7 @@ import { createContext } from "react";
interface DatabaseDataContextType {
database: DatabaseType,
databases : DatabaseType[] ,
isLoading: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
@@ -23,6 +24,11 @@ interface DatabaseDataContextType {
interface DatabaseOperationsContextType {
data_types: DataType[],
// database operations
createDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
editDatabase: (database: DatabaseInsertType) => Promise<QueryResult>,
deleteDatabase : ( id : string) => Promise<void> ,
setCurrentDatabaseId : ( id : string) => void ,
createTable: (table: TableInsertType) => Promise<void>,
editTable: (table: TableInsertType) => Promise<QueryResult>,
@@ -36,10 +42,10 @@ interface DatabaseOperationsContextType {
orderTableFields: (fields: FieldType[]) => Promise<void>,
// index operations
createIndex : ( index : IndexInsertType) => Promise<QueryResult> ,
editIndex : ( index : IndexInsertType) => Promise<QueryResult> ,
deleteIndex : ( id : string) => Promise<QueryResult> ,
editFieldIndices:( indexId : string , fieldIds : string[])=> Promise<void> ,
createIndex: (index: IndexInsertType) => Promise<QueryResult>,
editIndex: (index: IndexInsertType) => Promise<QueryResult>,
deleteIndex: (id: string) => Promise<QueryResult>,
editFieldIndices: (indexId: string, fieldIds: string[]) => Promise<void>,
// relationship operations
createRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
editRelationship: (relationship: RelationshipInsertType) => Promise<QueryResult>,
@@ -10,7 +10,7 @@ import { FieldInsertType, fields, FieldType } from "@/lib/schemas/field-schema";
import { RelationshipInsertType, relationships } from "@/lib/schemas/relationship-schema";
import DatabaseHistoryProvider from "../database-history/database-history-provider";
import { DBDiffOperation } from "@/utils/database";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { DatabaseInsertType, DatabaseType, databases as databaseModel } from "@/lib/schemas/database-schema";
import { getTimestamp } from "@/utils/utils";
import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
import { field_indices } from "@/lib/schemas/field_index-schema";
@@ -23,19 +23,19 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
const [currentDatabaseId, setCurrentDatabaseId] = useState<string | undefined>(undefined);
// Fetch all databases
const { data: databases, isLoading: loadingDatabases } = useQuery(toCompilableQuery(
const { data: databases, isLoading: loadingDatabases , isFetching : fetchingDatabases} = useQuery(toCompilableQuery(
db.query.databases.findMany()
));
// Fetch all data types
const { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
const { data: data_types, isLoading: loadingDataTypes , isFetching : fetchingDataTypes} = useQuery(toCompilableQuery(
db.query.data_types.findMany()
));
// Fetch the current database with nested tables, fields, and relationships
let { data: database, isLoading: loadingCurrentDatabase } = useQuery(
let { data: database, isLoading: loadingCurrentDatabase , isFetching : fetchingCurrentDatabase} = useQuery(
toCompilableQuery(
db.query.databases.findFirst({
db.query.databases.findMany({
where: (databases, { eq }) => eq(databases.id, currentDatabaseId as string),
with: {
tables: {
@@ -68,9 +68,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
})
)
);
// Normalize result to single object
if (database.length == 1)
database = database[0] as any;
@@ -82,7 +82,22 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
}, [currentDatabaseId, databases])
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase ;
// 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> => {
return await db.update(databaseModel).set(database).where(eq(databaseModel.id, database.id))
}, [db]);
const deleteDatabase = useCallback(async (id: string): Promise<void> => {
await db.delete(databaseModel).where(eq(databaseModel.id, id));
}, [db]);
// CRUD operations for Tables
const createTable = useCallback(async (table: TableInsertType): Promise<void> => {
@@ -219,6 +234,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
// Apply a list of diff operations to sync database
const executeDbDiffOps = useCallback(async (operations: DBDiffOperation[]) => {
try {
console.log ("excuted diff operations")
await db.transaction(async (tx) => {
for (const operation of operations) {
if (operation.type == "CREATE_TABLE") {
@@ -253,9 +269,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}
else if (operation.type == "CREATE_INDEX") {
await tx.insert(indices).values(operation.index);
if (operation.index.fieldIndices && Object.values(operation.index.fieldIndices).length > 0)
await tx.insert(field_indices).values(Object.values(operation.index.fieldIndices));
if (operation.index.fieldIndices && Object.values(operation.index.fieldIndices).length > 0)
await tx.insert(field_indices).values(Object.values(operation.index.fieldIndices));
}
else if (operation.type == "DELETE_INDEX") {
await tx.delete(indices).where(eq(indices.id, operation.indexId));
@@ -279,6 +295,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [db]);
const databaseOpsValue = useMemo(() => ({
createDatabase,
editDatabase,
deleteDatabase,
setCurrentDatabaseId,
createTable,
editTable,
deleteTable,
@@ -299,6 +320,9 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
editFieldIndices,
data_types
}), [
createDatabase,
editDatabase,
deleteDatabase,
createTable,
editTable,
deleteTable,
@@ -324,6 +348,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
<DatabaseDataContext.Provider value={{
database: database as unknown as DatabaseType,
databases: databases as DatabaseType[],
isLoading,
getField,
}}>
@@ -1,18 +1,14 @@
import { createContext } from "react";
import { createContext } from "react";
export enum Modals {
CREATE_RELATIONSHIP = "CREATE_RELATIONSHIP"
CREATE_RELATIONSHIP = "CREATE_RELATIONSHIP",
CREATE_DATABASE = "CREATE_DATABASE" ,
OPEN_DATABASE = "OPEN_DATABASE"
}
interface ModalContextType {
focusedTableId: string | undefined;
focusedRelationshipId: string | undefined;
open: (modal: Modals, props?: any) => void
}
export const ModalContext = createContext<ModalContextType>({} as ModalContextType);
+37 -17
View File
@@ -1,35 +1,56 @@
/*
import { useCallback, useContext, useState } from "react"
import { ModalContext, Modals } from "./modal-contxet"
import Modal, { ModalProps } from "@/components/modal/modal";
import { useDisclosure } from "@heroui/react";
import CreateRelationshipModal from "@/pages/database/modals/create-relationship-modal";
import { CreateDatabaseModal } from "@/pages/database/modals/create-database-modal";
import OpenDatabaseModal from "@/pages/database/modals/open-database-modal";
interface Props { children: React.ReactNode }
interface CurrentModalProps {
modal: Modals,
modalProps: ModalProps,
content?: any
props?: any,
}
const ModalProvider: React.FC<Props> = ({ children }) => {
const { isOpen, onOpen } = useDisclosure();
export const ModalProvider: React.FC<Props> = ({ children }) => {
const [currentModal, setCurrentModal] = useState<CurrentModalProps | undefined>(undefined);
const { isOpen, onOpen, onClose } = useDisclosure();
const open = useCallback((modal: Modals, props?: any) => {
const onOpenChange = useCallback(() => {
setCurrentModal({
modal,
props
});
onOpen();
setCurrentModal(undefined);
}, [onOpen]);
}, []);
const onOpenChange = (isOpen: boolean) => {
if (!isOpen) {
onClose();
setCurrentModal(undefined);
}
}
return (
<ModalContext.Provider>
<ModalContext.Provider value={{
open
}}
>
{
currentModal &&
<Modal isOpen={isOpen} {...currentModal.modalProps} onOpenChange={onOpenChange} >
</Modal>
currentModal ? (
currentModal.modal == Modals.CREATE_RELATIONSHIP &&
<CreateRelationshipModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
currentModal.modal == Modals.CREATE_DATABASE &&
<CreateDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
||
currentModal.modal == Modals.OPEN_DATABASE &&
<OpenDatabaseModal {...currentModal.props} onOpenChange={onOpenChange} isOpen={isOpen} />
) : undefined
}
{children}
</ModalContext.Provider>
@@ -37,6 +58,5 @@ const ModalProvider: React.FC<Props> = ({ children }) => {
}
export const useModal = () => useContext(ModalContext);
export const useModal = () => useContext(ModalContext);
*/
+5
View File
@@ -129,4 +129,9 @@ svg.text-icon:hover {
.dark .bg-background-50 {
background-color: red !important;
}
.database-checkbox[data-selected="true"]{
border : 1.5px solid hsl(var(--heroui-primary-300));
}
+2 -1
View File
@@ -58,5 +58,6 @@ function excludeFields(
export {
areArraysEqual,
getTimestamp,
excludeFields
excludeFields ,
}
+3 -2
View File
@@ -22,7 +22,7 @@ export default {
},
themes: {
dark: {
colors: {
@@ -39,8 +39,9 @@ export default {
DEFAULT: "#cecfd2"
},
content1: {
DEFAULT: "#282d34"
DEFAULT: "#1c2026"
},
default: {
50: '#f2f3f5',
100: '#d6d7db',