mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Init Commit
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
.now/*
|
||||
*.css
|
||||
.changeset
|
||||
dist
|
||||
esm/*
|
||||
public/*
|
||||
tests/*
|
||||
scripts/*
|
||||
*.config.js
|
||||
.DS_Store
|
||||
node_modules
|
||||
coverage
|
||||
.next
|
||||
build
|
||||
!.commitlintrc.cjs
|
||||
!.lintstagedrc.cjs
|
||||
!jest.config.js
|
||||
!plopfile.js
|
||||
!react-shim.js
|
||||
!tsup.config.ts
|
||||
@@ -0,0 +1,92 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/eslintrc.json",
|
||||
"env": {
|
||||
"browser": false,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:prettier/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
],
|
||||
"plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"react/prop-types": "off",
|
||||
"react/jsx-uses-react": "off",
|
||||
"react/react-in-jsx-scope": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"jsx-a11y/click-events-have-key-events": "warn",
|
||||
"jsx-a11y/interactive-supports-focus": "warn",
|
||||
"prettier/prettier": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"unused-imports/no-unused-vars": "off",
|
||||
"unused-imports/no-unused-imports": "warn",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"args": "after-used",
|
||||
"ignoreRestSiblings": false,
|
||||
"argsIgnorePattern": "^_.*?$"
|
||||
}
|
||||
],
|
||||
"import/order": [
|
||||
"warn",
|
||||
{
|
||||
"groups": [
|
||||
"type",
|
||||
"builtin",
|
||||
"object",
|
||||
"external",
|
||||
"internal",
|
||||
"parent",
|
||||
"sibling",
|
||||
"index"
|
||||
],
|
||||
"pathGroups": [
|
||||
{
|
||||
"pattern": "~/**",
|
||||
"group": "external",
|
||||
"position": "after"
|
||||
}
|
||||
],
|
||||
"newlines-between": "always"
|
||||
}
|
||||
],
|
||||
"react/self-closing-comp": "warn",
|
||||
"react/jsx-sort-props": [
|
||||
"warn",
|
||||
{
|
||||
"callbacksLast": true,
|
||||
"shorthandFirst": true,
|
||||
"noSortAlphabetically": false,
|
||||
"reservedFirst": true
|
||||
}
|
||||
],
|
||||
"padding-line-between-statements": [
|
||||
"warn",
|
||||
{"blankLine": "always", "prev": "*", "next": "return"},
|
||||
{"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"},
|
||||
{
|
||||
"blankLine": "any",
|
||||
"prev": ["const", "let", "var"],
|
||||
"next": ["const", "let", "var"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
package-lock.json
|
||||
bun.lockb
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Next UI
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,50 @@
|
||||
# Vite & HeroUI Template
|
||||
|
||||
This is a template for creating applications using Vite and HeroUI (v2).
|
||||
|
||||
[Try it on CodeSandbox](https://githubbox.com/frontio-ai/vite-template)
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- [Vite](https://vitejs.dev/guide/)
|
||||
- [HeroUI](https://heroui.com)
|
||||
- [Tailwind CSS](https://tailwindcss.com)
|
||||
- [Tailwind Variants](https://tailwind-variants.org)
|
||||
- [TypeScript](https://www.typescriptlang.org)
|
||||
- [Framer Motion](https://www.framer.com/motion)
|
||||
|
||||
## How to Use
|
||||
|
||||
To clone the project, run the following command:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/frontio-ai/vite-template.git
|
||||
```
|
||||
|
||||
### Install dependencies
|
||||
|
||||
You can use one of them `npm`, `yarn`, `pnpm`, `bun`, Example using `npm`:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
### Run the development server
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Setup pnpm (optional)
|
||||
|
||||
If you are using `pnpm`, you need to add the following code to your `.npmrc` file:
|
||||
|
||||
```bash
|
||||
public-hoist-pattern[]=*@heroui/*
|
||||
```
|
||||
|
||||
After modifying the `.npmrc` file, you need to run `pnpm install` again to ensure that the dependencies are installed correctly.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/frontio-ai/vite-template/blob/main/LICENSE).
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
+28
@@ -0,0 +1,28 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + HeroUI</title>
|
||||
<meta key="title" content="Vite + HeroUI" property="og:title" />
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
property="og:description"
|
||||
/>
|
||||
<meta
|
||||
content="Make beautiful websites regardless of your design experience."
|
||||
name="description"
|
||||
/>
|
||||
<meta
|
||||
key="viewport"
|
||||
content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"
|
||||
name="viewport"
|
||||
/>
|
||||
<link href="/favicon.ico" rel="icon" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "vite-template",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"lint": "eslint -c .eslintrc.json ./src/**/**/*.{ts,tsx} --fix",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@heroui/react": "^2.7.6",
|
||||
"@radix-ui/react-tooltip": "^1.2.3",
|
||||
"@react-aria/visually-hidden": "3.8.21",
|
||||
"@react-types/shared": "3.28.0",
|
||||
"@xyflow/react": "^12.6.0",
|
||||
"framer-motion": "11.15.0",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"lucide-react": "^0.501.0",
|
||||
"react": "18.3.1",
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-router-dom": "6.23.0",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwind-variants": "0.3.0",
|
||||
"tailwindcss": "3.4.16"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.5.7",
|
||||
"@types/react": "18.3.3",
|
||||
"@types/react-dom": "18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.11.0",
|
||||
"@typescript-eslint/parser": "8.11.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "5.2.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"postcss": "8.4.38",
|
||||
"prettier": "3.3.3",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "^5.2.0",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
+18
@@ -0,0 +1,18 @@
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import "@/styles/globals.css"
|
||||
import { TooltipProvider } from "./components/tooltip/tooltip";
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import useAppRoutes from "./routes/app-route";
|
||||
|
||||
function App() {
|
||||
const appRoutes = useAppRoutes() ;
|
||||
return (
|
||||
<ReactFlowProvider>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{appRoutes}
|
||||
</TooltipProvider>
|
||||
</ReactFlowProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,56 @@
|
||||
import { colorOptions } from "@/lib/colors";
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger } from "@heroui/react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
interface ColorPickerProps {
|
||||
defaultColor?: string;
|
||||
onChange?: (color: string) => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ColorPicker: React.FC<ColorPickerProps> = ({ defaultColor, onChange }) => {
|
||||
const [color , setColor] = useState<string>( defaultColor as string ) ;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const onSelect = useCallback((color: string) => {
|
||||
setIsOpen(false);
|
||||
onChange && onChange(color) ;
|
||||
setColor ( color)
|
||||
}, [onChange])
|
||||
|
||||
return (
|
||||
<Popover placement="bottom" radius="sm" isOpen={isOpen} onOpenChange={(open) => setIsOpen(open)}>
|
||||
<PopoverTrigger>
|
||||
<Button size="sm"
|
||||
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
|
||||
isIconOnly
|
||||
style={{
|
||||
backgroundColor : color
|
||||
}}
|
||||
>
|
||||
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{colorOptions.map(color => (
|
||||
<div
|
||||
key={color}
|
||||
className="size-8 cursor-pointer rounded-md border-2 border-muted transition-shadow hover:shadow-md"
|
||||
style={{
|
||||
backgroundColor: color
|
||||
}}
|
||||
onClick={() => onSelect(color)}
|
||||
>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default ColorPicker;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { Dropdown, DropdownMenu as HeroDropdownMenu, DropdownItem, DropdownTrigger, Button, useDisclosure, Popover, PopoverTrigger, PopoverContent, Divider, DropdownSection } from "@heroui/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import SubmenuDropdown from "./submenu-dropdown";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface MenuDropdownProps {
|
||||
title?: string;
|
||||
children?: MenuDropdownProps[];
|
||||
|
||||
theme?: "default" | "danger",
|
||||
isDisabled?: boolean,
|
||||
divide?: boolean,
|
||||
shortcut?: string,
|
||||
isOpen?: boolean
|
||||
}
|
||||
|
||||
|
||||
|
||||
const DropdownMenu: React.FC<MenuDropdownProps> = ({ title, children, isOpen }) => {
|
||||
|
||||
|
||||
|
||||
const disabledChilds: string[] = useMemo(() => {
|
||||
return children ? children?.filter((child: MenuDropdownProps) => child.isDisabled && child.title).map((child: MenuDropdownProps) => child.title as string) : []
|
||||
}, [children]);
|
||||
|
||||
|
||||
return <Dropdown radius="sm" >
|
||||
{
|
||||
title &&
|
||||
<DropdownTrigger >
|
||||
<Button size="sm" variant="light" >
|
||||
<span className=" text-left flex justify-between text-small font-semibold">
|
||||
{title}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
}
|
||||
<HeroDropdownMenu disabledKeys={disabledChilds} >
|
||||
|
||||
{
|
||||
children ? children?.map((child: MenuDropdownProps) => (
|
||||
|
||||
<DropdownItem
|
||||
key={child.title as string}
|
||||
className={child.theme == "danger" ? "text-danger" : ""}
|
||||
color={child.theme}
|
||||
shortcut={child.shortcut}
|
||||
>
|
||||
{
|
||||
!child.children ?
|
||||
<div>
|
||||
{child.title}
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-default-200"></div>
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<div >
|
||||
<SubmenuDropdown {...child} />
|
||||
{
|
||||
child.divide &&
|
||||
<div className="w-full h-[0.5px] absolute bottom-[-0.5px] left-0 bg-default-200"></div>
|
||||
}
|
||||
</div>
|
||||
|
||||
}
|
||||
</DropdownItem>
|
||||
|
||||
)) : []
|
||||
}
|
||||
|
||||
</HeroDropdownMenu>
|
||||
</Dropdown>
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default DropdownMenu;
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
import DropdownMenu, { MenuDropdownProps } from "./menu-dropdown";
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
const menu: MenuDropdownProps[] = [
|
||||
{
|
||||
title: "File",
|
||||
|
||||
children: [{
|
||||
title: "New"
|
||||
},
|
||||
{
|
||||
title: "Open",
|
||||
shortcut: "Ctnl + O"
|
||||
}, {
|
||||
title: "Save",
|
||||
shortcut: "Ctnl + S",
|
||||
divide: true
|
||||
}, {
|
||||
title: "Import",
|
||||
divide: true,
|
||||
children: [{
|
||||
title: ".json",
|
||||
}, {
|
||||
title: ".dbml"
|
||||
}, {
|
||||
title: "MySql"
|
||||
}, {
|
||||
title: "Postgresql"
|
||||
}]
|
||||
}, {
|
||||
title: "Export SQL",
|
||||
children: [{
|
||||
title: "Generic",
|
||||
}, {
|
||||
title: "MySql"
|
||||
}, {
|
||||
title: "Postgresql"
|
||||
}]
|
||||
}, {
|
||||
title: "Export ORM Models",
|
||||
divide: true
|
||||
}, {
|
||||
title: "Delete Project",
|
||||
theme: "danger"
|
||||
}]
|
||||
}, {
|
||||
title: "Edit",
|
||||
|
||||
children: [{
|
||||
title: "Undo",
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
title: "Redo",
|
||||
|
||||
isDisabled: true,
|
||||
}, {
|
||||
title: "Clear",
|
||||
|
||||
}]
|
||||
} , {
|
||||
title : "View" ,
|
||||
children : [{
|
||||
title : "Hide Controller" ,
|
||||
shortcut : "Ctnl + B" ,
|
||||
divide : true
|
||||
} , {
|
||||
title : "Zoom on scroll" ,
|
||||
divide : true ,
|
||||
children : [{
|
||||
title : "On"
|
||||
} , {
|
||||
title : "Off"
|
||||
}] ,
|
||||
|
||||
} , {
|
||||
title : "Theme" ,
|
||||
|
||||
children : [{
|
||||
title : "Light"
|
||||
} , {
|
||||
title : "Dark"
|
||||
}]
|
||||
}]
|
||||
} , {
|
||||
title : "Help" ,
|
||||
children : [{
|
||||
title : "Show Docs" ,
|
||||
} , {
|
||||
title : "Join Discord"
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
interface MenuProps {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Menu: React.FC<MenuProps> = ({ }) => {
|
||||
return menu.map(menuItem => (
|
||||
<DropdownMenu {...menuItem} />
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
export default Menu;
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Button, Popover, PopoverContent, PopoverTrigger, useDisclosure } from "@heroui/react";
|
||||
import { MenuDropdownProps } from "./menu-dropdown";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
import DropdownMenu from "./menu-dropdown";
|
||||
|
||||
|
||||
|
||||
const SubmenuDropdown: React.FC<MenuDropdownProps> = ({ title, children }) => {
|
||||
const { isOpen, onOpen, onClose } = useDisclosure();
|
||||
|
||||
return (
|
||||
<div onMouseEnter={onOpen} onMouseLeave={onClose} >
|
||||
<Popover placement="right" isOpen={isOpen} radius="sm" offset={90} >
|
||||
<PopoverTrigger>
|
||||
<Button className="w-full h-6 bg-transparent p-0" size="sm" value={"light"}>
|
||||
<span className="w-full text-left flex justify-between text-small font-normal">
|
||||
{title}
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<DropdownMenu
|
||||
|
||||
children={children}
|
||||
/>
|
||||
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
};
|
||||
|
||||
export default SubmenuDropdown;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NavbarBrand, NavbarContent, Navbar as NavbarContainer, Image, Dropdown, DropdownTrigger, Button, DropdownMenu, DropdownItem } from "@heroui/react";
|
||||
import Menu from "../menu/menu";
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const Navbar: React.FC<Props> = ({ }) => {
|
||||
return (
|
||||
|
||||
<nav className="h-12 fixed z-50 bg-background w-full flex items-center p-4 border-b border-default-200">
|
||||
<img
|
||||
src="/stackrender.png"
|
||||
width={22}
|
||||
alt="logo"
|
||||
|
||||
|
||||
/>
|
||||
<h3 className="font-semibold ml-2 text-slate-900 text-sm">StackRender</h3>
|
||||
<div className="ml-4">
|
||||
<Menu/>
|
||||
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default Navbar;
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip/tooltip";
|
||||
import { Divider } from "@heroui/react";
|
||||
import React from "react";
|
||||
|
||||
|
||||
|
||||
export interface SidebarItemProps {
|
||||
type?: "item" | "divider"
|
||||
title?: string;
|
||||
icon?: React.ReactNode;
|
||||
href?: string;
|
||||
isActive? : boolean
|
||||
}
|
||||
|
||||
|
||||
const SidebarItem: React.FC<SidebarItemProps> = ({ title, icon, href , type = "item" , isActive}) => {
|
||||
|
||||
if (type == "item")
|
||||
|
||||
|
||||
return (
|
||||
<Tooltip delayDuration={0} >
|
||||
<TooltipTrigger asChild>
|
||||
<Link to={href as string} className="p-2 rounded-md hover:bg-default-200 sidebar-item" data-active={isActive}>
|
||||
{icon}
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="left"
|
||||
align="center"
|
||||
>
|
||||
{title}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
)
|
||||
else {
|
||||
return <Divider className="bg-default-200" />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default SidebarItem
|
||||
@@ -0,0 +1,116 @@
|
||||
import { BookOpen, EarthLock, GitCommitHorizontal, Github, Settings, Sparkles, Table, TableProperties, Waypoints, Workflow, X, Zap } from "lucide-react";
|
||||
import SidebarItem, { SidebarItemProps } from "./sidebar-item";
|
||||
import { useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
interface SidebarProps {
|
||||
children?: React.ReactNode
|
||||
}
|
||||
|
||||
const sidebarItemClass : string = "text-gray-700 size-4 data-[active=true]:text-primary" ;
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ }) => {
|
||||
const { t , i18n } = useTranslation() ;
|
||||
console.log ( i18n.language)
|
||||
|
||||
console.log ()
|
||||
const location = useLocation() ;
|
||||
|
||||
const sidebarItems: SidebarItemProps[] = useMemo(() => [
|
||||
{
|
||||
title: t("sidebar.tables"),
|
||||
icon: <TableProperties className={sidebarItemClass}></TableProperties>,
|
||||
href: "/database/tables",
|
||||
isActive : location.pathname.endsWith("/database/tables")
|
||||
},
|
||||
{
|
||||
title: t("sidebar.relationships"),
|
||||
icon: <Workflow className={sidebarItemClass}></Workflow>,
|
||||
href: "/database/relationships",
|
||||
isActive : location.pathname.endsWith("/database/relationships")
|
||||
},
|
||||
|
||||
{
|
||||
title: "AI",
|
||||
icon: <Sparkles className={sidebarItemClass}></Sparkles>,
|
||||
href: "/database/bot",
|
||||
isActive : location.pathname.endsWith("/database/bot")
|
||||
},
|
||||
{
|
||||
type: "divider"
|
||||
},
|
||||
{
|
||||
title: "API",
|
||||
icon: <Zap className={sidebarItemClass}></Zap>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
{
|
||||
title: "Authentication",
|
||||
icon: <EarthLock className={sidebarItemClass}></EarthLock>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
{
|
||||
title: "Controllers",
|
||||
icon: <Waypoints className={sidebarItemClass}></Waypoints>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
{
|
||||
type: "divider"
|
||||
},
|
||||
{
|
||||
title: "Git",
|
||||
icon: <GitCommitHorizontal className={sidebarItemClass}></GitCommitHorizontal>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
{
|
||||
type: "divider"
|
||||
},
|
||||
{
|
||||
title: "Project Setting",
|
||||
icon: <Settings className={sidebarItemClass}></Settings>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
], [location])
|
||||
|
||||
const bottomSidebarItems: SidebarItemProps[] = useMemo(() => [
|
||||
{
|
||||
title: "Github",
|
||||
icon: <Github className={sidebarItemClass}></Github>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
{
|
||||
title: "Docs",
|
||||
icon: <BookOpen className={sidebarItemClass}></BookOpen>,
|
||||
href: "/database/ai-bot",
|
||||
},
|
||||
], []) ;
|
||||
|
||||
return (
|
||||
|
||||
<aside className="h-full z-[20] flex flex-col items-between py-2 justify-between sticky top-0 duration-500 w-12 bg-sidebar border-r pt-[56px]">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
{
|
||||
sidebarItems.map((item: SidebarItemProps) => (
|
||||
<SidebarItem
|
||||
{...item}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div className="flex flex-col items-center gap-2 ">
|
||||
{
|
||||
bottomSidebarItems.map((item: SidebarItemProps) => (
|
||||
<SidebarItem
|
||||
{...item}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default Sidebar;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import * as TooltipPrimitive from '@radix-ui/react-tooltip';
|
||||
import { cn } from '@heroui/react';
|
||||
|
||||
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
|
||||
className={cn(
|
||||
'z-50 overflow-hidden rounded-md bg-foreground px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
@@ -0,0 +1,40 @@
|
||||
import { en, enLanguage } from "./languages/en";
|
||||
import { fr, frLanguage } from "./languages/fr";
|
||||
import { Language } from "./types";
|
||||
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||
import { ar, arLanguage } from "./languages/ar";
|
||||
|
||||
|
||||
|
||||
export const languages: Language[] = [
|
||||
enLanguage,
|
||||
frLanguage ,
|
||||
arLanguage
|
||||
];
|
||||
|
||||
|
||||
const resources = {
|
||||
en,
|
||||
fr,
|
||||
ar
|
||||
};
|
||||
|
||||
|
||||
|
||||
i18n.use(LanguageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources ,
|
||||
lng: 'en', // 👈 set default language to French
|
||||
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
fallbackLng: enLanguage.code,
|
||||
debug: false,
|
||||
});
|
||||
|
||||
export { i18n };
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Language } from "../types";
|
||||
|
||||
|
||||
export const ar = {
|
||||
|
||||
translation: {
|
||||
sidebar: {
|
||||
tables: "الجداول",
|
||||
relationships: "العلاقات"
|
||||
},
|
||||
db_controller: {
|
||||
filter : "تصفية" ,
|
||||
add_table : "إضافة جدول" ,
|
||||
add_field : "إضافة حقل" ,
|
||||
add_index : "إضافة فهرس" ,
|
||||
add_relationship : "إضافة علاقة" ,
|
||||
show_code : "عرض الكود" ,
|
||||
fields : "الحقول" ,
|
||||
indexes : "الفهارس" ,
|
||||
note : "ملاحظة" ,
|
||||
name : "الاسم" ,
|
||||
type : "النوع" ,
|
||||
primary_key : "المفتاح الأساسي" ,
|
||||
nullable : "يسمح بالقيمة الفارغة" ,
|
||||
select_fields : "اختر الحقول" ,
|
||||
unique : "فريد" ,
|
||||
table_note : "ملاحظة الجدول" ,
|
||||
collapse : "إخفاء الكل" ,
|
||||
primary_table : "الجدول الأساسي" ,
|
||||
referenced_table : "الجدول المشار إليه" ,
|
||||
cardinality :{
|
||||
name : "العلاقة" ,
|
||||
one_to_one : "واحد إلى واحد" ,
|
||||
one_to_many : "واحد إلى متعدد" ,
|
||||
many_to_one : "متعدد إلى واحد" ,
|
||||
many_to_many : "متعدد إلى متعدد" ,
|
||||
},
|
||||
delete : "حذف" ,
|
||||
} ,
|
||||
table: {
|
||||
double_click: "انقر مرتين للتعديل"
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const arLanguage: Language = {
|
||||
name: 'Arabic',
|
||||
nativeName: 'العربية',
|
||||
code: 'ar',
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Language } from "../types";
|
||||
|
||||
|
||||
export const en = {
|
||||
|
||||
translation : {
|
||||
sidebar : {
|
||||
tables : "Tables" ,
|
||||
relationships : "Relationships"
|
||||
} ,
|
||||
db_controller : {
|
||||
filter : "Filter" ,
|
||||
add_table : "Add Table" ,
|
||||
add_field : "Add Field" ,
|
||||
add_index : "Add Index" ,
|
||||
add_relationship : "Add Relationship" ,
|
||||
show_code : "Show code" ,
|
||||
fields : "Fields" ,
|
||||
indexes : "Indexes" ,
|
||||
note : "Note" ,
|
||||
name : "Name" ,
|
||||
type : "Type" ,
|
||||
primary_key : "Primary Key" ,
|
||||
nullable : "Nullable" ,
|
||||
select_fields : "Select fields" ,
|
||||
unique : "Unique" ,
|
||||
table_note : "Table note" ,
|
||||
collapse : "Collapse All" ,
|
||||
primary_table : "Primary Table" ,
|
||||
referenced_table : "Referenced Table" ,
|
||||
cardinality :{
|
||||
name : "Cardinality" ,
|
||||
one_to_one : "One to One" ,
|
||||
one_to_many : "One to Many" ,
|
||||
many_to_one : "Many to One" ,
|
||||
many_to_many : "Many to Many" ,
|
||||
} ,
|
||||
delete : "Delete" ,
|
||||
|
||||
},
|
||||
table : {
|
||||
double_click : "Double click to edit"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const enLanguage : Language = {
|
||||
name : "English" ,
|
||||
nativeName: 'English',
|
||||
code: 'en',
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Language } from "../types";
|
||||
|
||||
|
||||
export const fr = {
|
||||
translation: {
|
||||
sidebar: {
|
||||
tables: "Tableaux",
|
||||
relationships: "Relations"
|
||||
} ,
|
||||
db_controller : {
|
||||
filter : "Filtrer" ,
|
||||
add_table : "Ajouter une table" ,
|
||||
add_field : "Ajouter un champ" ,
|
||||
add_index : "Ajouter un index" ,
|
||||
add_relationship : "Ajouter une relation" ,
|
||||
show_code : "Afficher le code" ,
|
||||
fields : "Champs" ,
|
||||
indexes : "Index" ,
|
||||
note : "Note" ,
|
||||
name : "Nom" ,
|
||||
type : "Type" ,
|
||||
primary_key : "Clé primaire" ,
|
||||
nullable : "Nullable" ,
|
||||
select_fields : "Sélectionner des champs" ,
|
||||
unique : "Unique" ,
|
||||
table_note : "Note de la table" ,
|
||||
collapse : "Tout réduire" ,
|
||||
primary_table : "Table primaire" ,
|
||||
referenced_table : "Table référencée" ,
|
||||
cardinality :{
|
||||
name : "Cardinalité" ,
|
||||
one_to_one : "Un à un" ,
|
||||
one_to_many : "Un à plusieurs" ,
|
||||
many_to_one : "Plusieurs à un" ,
|
||||
many_to_many : "Plusieurs à plusieurs" ,
|
||||
},
|
||||
delete : "Supprimer" ,
|
||||
} ,
|
||||
table: {
|
||||
double_click: "Double-cliquez pour modifier"
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export const frLanguage: Language = {
|
||||
name: "French",
|
||||
nativeName: 'Français',
|
||||
code: 'fr',
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
export type Language = {
|
||||
name: string;
|
||||
nativeName: string;
|
||||
code: string;
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
export const colorOptions = [
|
||||
"#fd7f6f", // Warm coral red
|
||||
"#7eb0d5", // Soft sky blue
|
||||
"#b2e061", // Fresh lime green
|
||||
"#bd7ebe", // Soft lavender pink
|
||||
"#ffb55a", // Vibrant orange
|
||||
"#ffee65", // Warm yellow
|
||||
"#beb9db", // Light purple grey
|
||||
"#fdcce5", // Light blush pink
|
||||
"#8bd3c7", // Minty teal
|
||||
"#f4a1a1", // Soft peachy red
|
||||
"#9bd78c", // Fresh pastel green
|
||||
"#d1a9e0" // Light lilac purple
|
||||
|
||||
|
||||
];
|
||||
|
||||
export const randomColor = () => {
|
||||
return colorOptions[Math.floor(Math.random() * colorOptions.length)];
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface DataType {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { DataType } from "../data/data-types";
|
||||
|
||||
export interface Field {
|
||||
id: string;
|
||||
name: string;
|
||||
type: DataType;
|
||||
primaryKey: boolean;
|
||||
unique: boolean;
|
||||
nullable: boolean;
|
||||
createdAt: number;
|
||||
default?: string;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
export interface Relationship {
|
||||
id: string;
|
||||
name: string;
|
||||
sourceSchema?: string;
|
||||
sourceTableId: string;
|
||||
targetSchema?: string;
|
||||
targetTableId: string;
|
||||
sourceFieldId: string;
|
||||
targetFieldId: string;
|
||||
sourceCardinality: Cardinality;
|
||||
targetCardinality: Cardinality;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
|
||||
export type RelationshipType =
|
||||
| 'one_to_one'
|
||||
| 'one_to_many'
|
||||
| 'many_to_one'
|
||||
| 'many_to_many';
|
||||
export type Cardinality = 'one' | 'many';
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Field } from "./field";
|
||||
|
||||
export interface Table {
|
||||
id: string;
|
||||
name: string;
|
||||
x: number;
|
||||
y: number;
|
||||
fields: Field[];
|
||||
width?: number;
|
||||
order?: number;
|
||||
color: string;
|
||||
createdAt: number;
|
||||
comments?: string;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "./App.tsx";
|
||||
import { Provider } from "./provider.tsx";
|
||||
import "./index.css";
|
||||
import "./i18/index.ts" ;
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider>
|
||||
<App />
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
|
||||
import Sidebar from "@/components/sidebar/sidebar";
|
||||
import DatabasePage from "../database/database-page";
|
||||
import Navbar from "@/components/navbar/navbar";
|
||||
import { Outlet } from "react-router-dom";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
const DashboardPage: React.FC<Props> = ({ }) => {
|
||||
return (
|
||||
<div className="flex flex-col justify-between h-full h-screen">
|
||||
<Navbar />
|
||||
<div className="flex">
|
||||
<Sidebar />
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,211 @@
|
||||
import { Table as TableType } from "@/lib/interfaces/table";
|
||||
import { addEdge, applyEdgeChanges, applyNodeChanges, Background, Controls, MiniMap, ReactFlow, useEdgesState, useNodesState, useReactFlow } from "@xyflow/react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import Table from "./table/table";
|
||||
|
||||
import '@xyflow/react/dist/style.css';
|
||||
import { Relationship } from "./table/relationship";
|
||||
import DBController from "./db-controller/db-controller";
|
||||
|
||||
interface DatabaseProps {
|
||||
initialTables?: TableType
|
||||
}
|
||||
|
||||
|
||||
const initialNodes = [
|
||||
{
|
||||
id: '1', type: "table", position: { x: 500, y: 500 }, data: {
|
||||
|
||||
table: {
|
||||
id: "1",
|
||||
name: "users",
|
||||
fields: [
|
||||
{
|
||||
id: 'e5n46eojbyxpg65pb1sesysi2',
|
||||
name: 'id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '7nbmg2bt6dzltr8rfouxctdlc',
|
||||
name: 'reservation_id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '3znekob0pqusyvdoq14nhlvgo',
|
||||
name: 'comment',
|
||||
type: {
|
||||
id: 'character_varying',
|
||||
name: 'character varying',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
style: { width: 224 }
|
||||
},
|
||||
|
||||
{
|
||||
id: '2', type: "table", position: { x: 200, y: 500 }, data: {
|
||||
|
||||
table: {
|
||||
id: "1",
|
||||
name: "products",
|
||||
fields: [
|
||||
{
|
||||
id: 'e5n46eojbyxpg65pb1sesysi2',
|
||||
name: 'id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: true,
|
||||
unique: true,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '7nbmg2bt6dzltr8rfouxctdlc',
|
||||
name: 'user_id',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'f9kcpflp010xe5ad3rmexla63',
|
||||
name: 'rating',
|
||||
type: {
|
||||
id: 'integer',
|
||||
name: 'integer',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: false,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: '3znekob0pqusyvdoq14nhlvgo',
|
||||
name: 'comment',
|
||||
type: {
|
||||
id: 'character_varying',
|
||||
name: 'character varying',
|
||||
},
|
||||
primaryKey: false,
|
||||
unique: false,
|
||||
nullable: true,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
style: { width: 224 }
|
||||
},
|
||||
];
|
||||
|
||||
const initialEdges: any[] = [];
|
||||
|
||||
|
||||
const edgeTypes = {
|
||||
'relationship-edge': Relationship,
|
||||
};
|
||||
|
||||
const DatabasePage: React.FC<DatabaseProps> = ({ initialTables }) => {
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
|
||||
|
||||
|
||||
const nodeTypes = useMemo(() => ({ table: Table }), []);
|
||||
const onConnect = useCallback((params: any) => setEdges((eds) => addEdge(params, eds)), [setEdges]);
|
||||
|
||||
|
||||
return (
|
||||
<div className="w-full h-screen flex">
|
||||
<DBController/>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
fitView
|
||||
className="w-full h-full"
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onConnect={onConnect}
|
||||
defaultEdgeOptions={{
|
||||
animated: false,
|
||||
type: 'relationship-edge',
|
||||
}}
|
||||
|
||||
panOnDrag={true}
|
||||
zoomOnScroll={true}
|
||||
nodeTypes={nodeTypes}
|
||||
edgeTypes={edgeTypes}
|
||||
|
||||
>
|
||||
|
||||
<Controls />
|
||||
<Background className="bg-default/10" />
|
||||
</ReactFlow>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default DatabasePage;
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Outlet } from "react-router-dom"
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const DBController : React.FC<Props> = ({ }) => {
|
||||
return(
|
||||
<div className="w-[640px] h-full bg-background border-r p-2 pt-[52px] ">
|
||||
<Outlet/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DBController
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Button, Select, SelectItem } from "@heroui/react";
|
||||
import { ChevronsLeftRightEllipsis, FileMinus2, FileOutput, SquareArrowLeft, SquareArrowRight, Trash2 } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const cardinalities = [
|
||||
{ key: "one2one", label: "One to One" },
|
||||
{ key: "one2many", label: "One to Many" },
|
||||
{ key: "many2one", label: "Many to One" },
|
||||
{ key: "many2many", label: "Many 2 many" },
|
||||
|
||||
];
|
||||
|
||||
|
||||
|
||||
const RelationshipAccordionBody: React.FC<Props> = ({ }) => {
|
||||
|
||||
const {t } = useTranslation() ;
|
||||
return (
|
||||
<div className="w-full p-2 space-y-4">
|
||||
<div className="flex">
|
||||
<div className="w-full space-y-1">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileOutput className="size-4" />
|
||||
{t("db_controller.primary_table")}
|
||||
</label>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm">
|
||||
users(id)
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
users(id)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-1">
|
||||
<label className="font-medium flex text-slate-700 flex items-center gap-1 text-sm">
|
||||
<FileMinus2 className="size-4" />
|
||||
{t("db_controller.referenced_table")}
|
||||
|
||||
</label>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<span className="truncate text-left text-sm ">
|
||||
products(user_id)
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
products(user_id)
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="flex text-sm font-medium flex text-slate-700 items-center gap-1">
|
||||
<ChevronsLeftRightEllipsis className="size-4 " />
|
||||
{t('db_controller.cardinality.name')}
|
||||
</label>
|
||||
<Select
|
||||
className="w-full"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
<SelectItem key={"one2one"}>{t("db_controller.cardinality.one_to_one")}</SelectItem>
|
||||
|
||||
<SelectItem key={"one2many"}>{t("db_controller.cardinality.one_to_many")}</SelectItem>
|
||||
|
||||
<SelectItem key={"many2one"}>{t("db_controller.cardinality.many_to_one")}</SelectItem>
|
||||
|
||||
<SelectItem key={"many2many"}>{t("db_controller.cardinality.many_to_many")}</SelectItem>
|
||||
|
||||
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
variant="solid"
|
||||
className="bg-transparent text-xs"
|
||||
radius="sm"
|
||||
size="sm"
|
||||
>
|
||||
<Trash2 className="mr-1 size-3.5 text-danger" />
|
||||
<div className="text-danger font-semibold">
|
||||
{t("db_controller.delete")}
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default RelationshipAccordionBody;
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Button, cn, Input } from "@heroui/react";
|
||||
import { Check, ChevronRight, EllipsisVertical, Focus, Pencil } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
|
||||
|
||||
interface RelationshipAccordionHeaderProps {
|
||||
isOpen?: boolean;
|
||||
}
|
||||
|
||||
|
||||
const RelationshipAccordionHeader: React.FC<RelationshipAccordionHeaderProps> = ({ isOpen }) => {
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 flex p-2 items-center" >
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
isOpen ? "rotate-[90deg]" : ""
|
||||
)}>
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</div>
|
||||
|
||||
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
placeholder={"Usres"}
|
||||
autoFocus
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onBlur={() => setEditMode(false)}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
size="sm"
|
||||
onPress={() => setEditMode(false)}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-4 text-icon" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
{
|
||||
|
||||
!editMode && <>
|
||||
|
||||
<label
|
||||
className="w-full truncate px-2 py-1 text-sm font-semibold text-black"
|
||||
>
|
||||
users one or many posts
|
||||
</label>
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<Focus className="size-4 text-icon" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => setEditMode(true)}
|
||||
>
|
||||
<Pencil className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-slate-500" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default RelationshipAccordionHeader;
|
||||
@@ -0,0 +1,104 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
|
||||
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
|
||||
import { Code, List, ListCollapse, Table, Workflow } from "lucide-react"
|
||||
import { 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";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const RelationshipController: React.FC<Props> = ({ }) => {
|
||||
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
|
||||
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4 py-1">
|
||||
<div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-8 p-0"
|
||||
isIconOnly
|
||||
onPress={() =>
|
||||
//setShowDBML((value) => !value)
|
||||
console.log("hello world ")
|
||||
}
|
||||
>
|
||||
<ListCollapse className="size-4" />
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.collapse")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
//ref={filterInputRef}
|
||||
type="text"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="bordered"
|
||||
placeholder={t("db_controller.filter")}
|
||||
//placeholder={t('side_panel.tables_section.filter')}
|
||||
className="h-8 w-full focus-visible:ring-0 "
|
||||
//value={filterText}
|
||||
//onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
radius="sm"
|
||||
color="primary"
|
||||
startContent={
|
||||
<Workflow className="h-4 w-4 " />
|
||||
}
|
||||
className="h-8 p-2 text-xs font-semibold "
|
||||
//onClick={handleCreateTable}
|
||||
>{t("db_controller.add_relationship")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className=" flex-1 overflow-auto">
|
||||
|
||||
<Accordion
|
||||
hideIndicator
|
||||
dividerProps={{
|
||||
className : "bg-default-200"
|
||||
}}
|
||||
>
|
||||
{items.map(item => (
|
||||
<AccordionItem
|
||||
key={item}
|
||||
aria-label={item}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200",
|
||||
base: "rounded-md p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<RelationshipAccordionHeader/>
|
||||
}
|
||||
>
|
||||
<RelationshipAccordionBody/>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RelationshipController
|
||||
|
||||
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Autocomplete, AutocompleteItem, Button, Input } from "@heroui/react";
|
||||
import { EllipsisVertical, Grip, GripVertical, KeyRound } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
|
||||
interface Props {
|
||||
id : string
|
||||
}
|
||||
export const animals = [
|
||||
{ label: "Cat", key: "cat", description: "The second most popular pet in the world" },
|
||||
{ label: "Dog", key: "dog", description: "The most popular pet in the world" },
|
||||
{ label: "Elephant", key: "elephant", description: "The largest land animal" },
|
||||
{ label: "Lion", key: "lion", description: "The king of the jungle" },
|
||||
{ label: "Tiger", key: "tiger", description: "The largest cat species" },
|
||||
{ label: "Giraffe", key: "giraffe", description: "The tallest land animal" },
|
||||
{
|
||||
label: "Dolphin",
|
||||
key: "dolphin",
|
||||
description: "A widely distributed and diverse group of aquatic mammals",
|
||||
},
|
||||
{ label: "Penguin", key: "penguin", description: "A group of aquatic flightless birds" },
|
||||
{ label: "Zebra", key: "zebra", description: "A several species of African equids" },
|
||||
{
|
||||
label: "Shark",
|
||||
key: "shark",
|
||||
description: "A group of elasmobranch fish characterized by a cartilaginous skeleton",
|
||||
},
|
||||
{
|
||||
label: "Whale",
|
||||
key: "whale",
|
||||
description: "Diverse group of fully aquatic placental marine mammals",
|
||||
},
|
||||
{ label: "Otter", key: "otter", description: "A carnivorous mammal in the subfamily Lutrinae" },
|
||||
{ label: "Crocodile", key: "crocodile", description: "A large semiaquatic reptile" },
|
||||
];
|
||||
|
||||
|
||||
const FieldItem: React.FC<Props> = ({id }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id });
|
||||
|
||||
const style = {
|
||||
transform: CSS.Transform.toString(transform),
|
||||
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex w-full gap-1 items-center " style={style} ref={setNodeRef} {...attributes}>
|
||||
<div {...listeners}>
|
||||
<GripVertical className="size-4 text-icon" />
|
||||
</div>
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
placeholder={t("db_controller.name")}
|
||||
value={id}
|
||||
/>
|
||||
<Autocomplete
|
||||
className="w-full"
|
||||
defaultItems={animals}
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
|
||||
placeholder={t("db_controller.type")}
|
||||
>
|
||||
{(item) => <AutocompleteItem key={item.key}>{item.label}</AutocompleteItem>}
|
||||
</Autocomplete>
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
N
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.nullable")}?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-2 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
<KeyRound className="size-4" />
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.primary_key")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default FieldItem;
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
import { Button } from "@heroui/react";
|
||||
import { Plus } from "lucide-react";
|
||||
import FieldItem from "./field-item";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import { closestCenter, DndContext, PointerSensor, useSensor, useSensors } from "@dnd-kit/core";
|
||||
import { arrayMove, SortableContext, verticalListSortingStrategy } from "@dnd-kit/sortable";
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const FieldList: React.FC<Props> = ({ }) => {
|
||||
const { t } = useTranslation();
|
||||
const [fields, setFields] = useState<string[]>(["Field 1", "Field 2", "Field 3", "Field 4"])
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor)
|
||||
);
|
||||
|
||||
function handleDragEnd(event: any) {
|
||||
const { active, over } = event;
|
||||
|
||||
if (active.id !== over?.id) {
|
||||
setFields((items) => {
|
||||
const oldIndex = items.indexOf(active.id);
|
||||
const newIndex = items.indexOf(over.id);
|
||||
|
||||
return arrayMove(items, oldIndex, newIndex);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-full space-y-2 no-select">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<SortableContext
|
||||
items={fields}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{
|
||||
fields.map((field: string) => (
|
||||
<FieldItem id={field}/>
|
||||
|
||||
))
|
||||
}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</DndContext>
|
||||
<Button
|
||||
variant="flat"
|
||||
radius="sm"
|
||||
startContent={
|
||||
<Plus className="size-4 text-icon" />
|
||||
}
|
||||
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
>
|
||||
{t("db_controller.add_field")}
|
||||
</Button>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default FieldList;
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Button, Select, SelectItem } from "@heroui/react";
|
||||
import { EllipsisVertical } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
|
||||
|
||||
export const animals = [
|
||||
{ key: "cat", label: "Cat" },
|
||||
{ key: "dog", label: "Dog" },
|
||||
{ key: "elephant", label: "Elephant" },
|
||||
{ key: "lion", label: "Lion" },
|
||||
{ key: "tiger", label: "Tiger" },
|
||||
{ key: "giraffe", label: "Giraffe" },
|
||||
{ key: "dolphin", label: "Dolphin" },
|
||||
{ key: "penguin", label: "Penguin" },
|
||||
{ key: "zebra", label: "Zebra" },
|
||||
{ key: "shark", label: "Shark" },
|
||||
{ key: "whale", label: "Whale" },
|
||||
{ key: "otter", label: "Otter" },
|
||||
{ key: "crocodile", label: "Crocodile" },
|
||||
];
|
||||
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
const IndexItem: React.FC<Props> = ({ }) => {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex gap-2 w-full">
|
||||
<Select
|
||||
className="w-full"
|
||||
placeholder={t("db_controller.select_fields")}
|
||||
selectionMode="multiple"
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
>
|
||||
{animals.map((animal) => (
|
||||
<SelectItem key={animal.key}>{animal.label}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
<div className="flex gap-2 ml-2">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button className="p-1 px-3 transition-all hover:bg-default rounded duration-200" >
|
||||
<span className="text-icon text-sm">
|
||||
U
|
||||
</span>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.unique")}?
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default IndexItem;
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { Button, Select, SelectItem } from "@heroui/react";
|
||||
import IndexItem from "./index-item";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface Props {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const IndexesList: React.FC<Props> = ({ }) => {
|
||||
const { t} = useTranslation() ;
|
||||
return (
|
||||
<div className="w-full flex-col space-y-2">
|
||||
<div>
|
||||
<IndexItem />
|
||||
</div>
|
||||
<Button
|
||||
variant="flat"
|
||||
radius="sm"
|
||||
startContent={
|
||||
<Plus className="size-4 text-icon" />
|
||||
}
|
||||
className="h-8 p-2 text-xs bg-transparent hover:bg-default text-gray font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
>
|
||||
{t("db_controller.add_index")}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default IndexesList;
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import { Table } from "@/lib/interfaces/table"
|
||||
import { Accordion, AccordionItem, Button, cn, Divider, Textarea } from "@heroui/react";
|
||||
import { ChevronLeft, ChevronRight, FileKey, FileText, FileType, Key, MessageSquareQuote } from "lucide-react";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import ColorPicker from "@/components/color-picker/color-picker";
|
||||
import FieldList from "./field/field-list";
|
||||
import IndexesList from "./index/indexes-list";
|
||||
|
||||
|
||||
export interface TableAccordionBodyProps {
|
||||
table?: Table ,
|
||||
}
|
||||
|
||||
const TableAccordionBody: React.FC<TableAccordionBodyProps> = ({ table }) => {
|
||||
|
||||
const [selectedKeys, setSelectedKeys] = useState(new Set(["fields"]));
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onColorChange = useCallback((color : string) => {
|
||||
console.log ( color)
|
||||
} ,[])
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Accordion
|
||||
variant={"light"} selectionMode="multiple" selectedKeys={selectedKeys} onSelectionChange={setSelectedKeys as any}>
|
||||
<AccordionItem key="fields" aria-label="Fields"
|
||||
indicator={({ isOpen }) => (
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
isOpen ? "rotate-[-90deg]" : ""
|
||||
)}>
|
||||
<ChevronLeft className="size-4 text-icon" />
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
trigger: "hover:bg-default h-6 "
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
<FileType className="size-4 " />
|
||||
<label className="text-sm w-full cursor-pointer">
|
||||
{t("db_controller.fields")}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FieldList />
|
||||
</AccordionItem>
|
||||
<AccordionItem key="indexes" aria-label="Indexes"
|
||||
indicator={({ isOpen }) => (
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
isOpen ? "rotate-[-90deg]" : ""
|
||||
)}>
|
||||
<ChevronLeft className="size-4 text-icon" />
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
trigger: "hover:bg-default"
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
<FileKey className="size-4 " />
|
||||
<label className="text-sm w-full cursor-pointer">
|
||||
{t("db_controller.indexes")}
|
||||
</label>
|
||||
</div>
|
||||
}>
|
||||
<IndexesList/>
|
||||
</AccordionItem>
|
||||
<AccordionItem key="note" aria-label="Note"
|
||||
indicator={({ isOpen }) => (
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
isOpen ? "rotate-[-90deg]" : ""
|
||||
)}>
|
||||
<ChevronLeft className="size-4 text-icon" />
|
||||
</div>
|
||||
)}
|
||||
classNames={{
|
||||
trigger: "hover:bg-default"
|
||||
}}
|
||||
subtitle={
|
||||
<div className="flex gap-2 items-center font-medium p-1 w-full hover:underline text-slate-500 hover:text-slate-600 transition-all duration-200">
|
||||
<MessageSquareQuote className="size-4" />
|
||||
<label className="text-sm w-full cursor-pointer">
|
||||
{t("db_controller.note")}
|
||||
</label>
|
||||
</div>
|
||||
}>
|
||||
<Textarea variant="bordered" className="w-full " label={t("db_controller.table_note")} classNames={{
|
||||
inputWrapper: "bg-default/80",
|
||||
|
||||
}} />
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
<hr className="bg-default-200" />
|
||||
<div className="flex p-2 pb-0 items-start">
|
||||
<div className="w-full">
|
||||
<ColorPicker
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="font-semibold p-4"
|
||||
startContent={
|
||||
<FileKey className="size-4 " />
|
||||
}
|
||||
>
|
||||
{t("db_controller.add_index")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="font-semibold p-4"
|
||||
startContent={
|
||||
<FileType className="size-4 " />
|
||||
}
|
||||
>
|
||||
{t("db_controller.add_field")}
|
||||
</Button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default TableAccordionBody;
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/tooltip/tooltip";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Table } from "@/lib/interfaces/table"
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { Button, cn, Divider, Input } from "@heroui/react";
|
||||
import { Check, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, EllipsisVertical, Focus, Grip, GripVertical, Pencil } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface TableAccordionHeaderProps {
|
||||
table?: Table,
|
||||
isOpen?: boolean,
|
||||
id: string
|
||||
}
|
||||
const TableAccordionHeader: React.FC<TableAccordionHeaderProps> = ({ table, isOpen, id }) => {
|
||||
const style = {
|
||||
borderLeft: "6px solid " + randomColor(),
|
||||
};
|
||||
const { t } = useTranslation();
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<div className="group w-full flex h-12 gap-1 border-l-4 flex p-2 items-center"
|
||||
style={style}
|
||||
>
|
||||
<div className={cn(
|
||||
'tarnsition-all duration-200',
|
||||
isOpen ? "rotate-[90deg]" : ""
|
||||
)}>
|
||||
<ChevronRight className="size-4 text-icon" />
|
||||
</div>
|
||||
<div className=" w-[1px] h-full bg-default-200">
|
||||
|
||||
</div>
|
||||
{
|
||||
!editMode &&
|
||||
<div className="w-full px-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<label
|
||||
className="w-full text-editable truncate px-2 py-1 text-sm font-semibold text-black"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{id}
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("table.double_click")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
editMode && <>
|
||||
<Input
|
||||
placeholder={"Usres"}
|
||||
autoFocus
|
||||
size="sm"
|
||||
variant="bordered"
|
||||
onBlur={() => setEditMode(false)}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-blue-400 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
size="sm"
|
||||
onClick={() => setEditMode(false)}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-4 text-icon" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
{
|
||||
|
||||
!editMode && <>
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<Focus className="size-4 text-icon" />
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => setEditMode(true)}
|
||||
>
|
||||
<Pencil className="size-4 text-icon" />
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
>
|
||||
<EllipsisVertical className="size-4 text-slate-500" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default TableAccordionHeader;
|
||||
@@ -0,0 +1,103 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip"
|
||||
import { Accordion, AccordionItem, Button, Input } from "@heroui/react"
|
||||
import { ChevronDown, Code, EllipsisVertical, Focus, Grid, Grip, List, Pencil, Table } from "lucide-react"
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useState } from "react";
|
||||
import TableAccordionHeader from "./table-accordion-item/table-accordion-header";
|
||||
import TableAccordionBody from "./table-accordion-item/table-accordion-body";
|
||||
|
||||
interface Props {}
|
||||
|
||||
|
||||
const TablesController: React.FC<Props> = ({ }) => {
|
||||
|
||||
const [items, setItems] = useState(["Item 1", "Item 2", "Item 3", "Item 4"]);
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between gap-4 py-1">
|
||||
<div>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-8 p-0"
|
||||
isIconOnly
|
||||
onPress={() =>
|
||||
//setShowDBML((value) => !value)
|
||||
console.log("hello world ")
|
||||
}
|
||||
>
|
||||
{false ? (
|
||||
<List className="size-4" />
|
||||
) : (
|
||||
<Code className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("db_controller.show_code")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
//ref={filterInputRef}
|
||||
type="text"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
variant="bordered"
|
||||
placeholder={t("db_controller.filter")}
|
||||
className="h-8 w-full focus-visible:ring-0"
|
||||
//value={filterText}
|
||||
//onChange={(e) => setFilterText(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="solid"
|
||||
color="primary"
|
||||
radius="sm"
|
||||
startContent={
|
||||
<Table className="h-4 w-4 " />
|
||||
}
|
||||
className="h-8 p-2 text-xs font-semibold"
|
||||
//onClick={handleCreateTable}
|
||||
> {t("db_controller.add_table")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className=" flex-1 overflow-auto">
|
||||
|
||||
<Accordion
|
||||
hideIndicator
|
||||
variant="splitted"
|
||||
>
|
||||
{items.map(item => (
|
||||
<AccordionItem
|
||||
key={item}
|
||||
aria-label={item}
|
||||
classNames={{
|
||||
trigger: "w-full hover:bg-default transition-all duration-200",
|
||||
base: "rounded-md shadow p-0 overflow-hidden",
|
||||
}}
|
||||
subtitle={
|
||||
<TableAccordionHeader id={item} />
|
||||
}
|
||||
>
|
||||
<TableAccordionBody />
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
export default TablesController
|
||||
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Field as FieldType } from "@/lib/interfaces/field";
|
||||
import { Button, cn, select } from "@heroui/react";
|
||||
import { Handle, Position, useConnection } from "@xyflow/react";
|
||||
import { Check } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
|
||||
|
||||
interface Props {
|
||||
field: FieldType,
|
||||
showHandles?: boolean
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Field: React.FC<Props> = ({ field, showHandles }) => {
|
||||
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
const connection = useConnection();
|
||||
|
||||
return (
|
||||
<div className="group relative flex h-8 items-center justify-between gap-1 border-t px-3 text-sm last:rounded-b-[6px] hover:bg-slate-100 dark:hover:bg-slate-800 transition-all duration-200 ease-in-out ">
|
||||
{
|
||||
!editMode &&
|
||||
<>
|
||||
<label
|
||||
className=" truncate text-sm "
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{field.name}
|
||||
</label>
|
||||
<span className="content-center truncate text-right text-xs text-muted-foreground text-default-600" >
|
||||
{field.type.name.split(' ')[0]}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
{
|
||||
editMode &&
|
||||
<>
|
||||
<input
|
||||
// ref={inputRef}
|
||||
onBlur={() => setEditMode(false)}
|
||||
placeholder={field.name}
|
||||
autoFocus
|
||||
type="text"
|
||||
// value={fieldName}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
// onChange={(e) => setFieldName(e.target.value)}
|
||||
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
onPress={() => setEditMode(false)}
|
||||
>
|
||||
<Check className="size-4" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 ",
|
||||
!showHandles ? "invisible" : "visible"
|
||||
)} >
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Left}
|
||||
id={"left-"+ field.id}
|
||||
className="w-4 h-4 border-3 bg-primary"
|
||||
/>
|
||||
<Handle
|
||||
type="source"
|
||||
position={Position.Right}
|
||||
className="w-4 h-4 border-3 bg-primary"
|
||||
|
||||
id={"right-"+ field.id}
|
||||
|
||||
/>
|
||||
</div>
|
||||
{
|
||||
<div className={
|
||||
cn(
|
||||
"absolute w-full left-0 ",
|
||||
!connection.inProgress ? "invisible" : "visible"
|
||||
)} >
|
||||
<Handle
|
||||
id={`${"target"}_${field.id}`}
|
||||
className={
|
||||
true
|
||||
? '!absolute !left-0 !top-0 !h-full !w-full !transform-none !rounded-none !border-none !opacity-0'
|
||||
: `!invisible`
|
||||
}
|
||||
position={Position.Left}
|
||||
type="target"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(Field);
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Relationship as RelationshipType } from "@/lib/interfaces/relationship";
|
||||
import { cn } from "@heroui/react";
|
||||
import { Edge, EdgeProps, getSmoothStepPath, Position, useReactFlow } from "@xyflow/react";
|
||||
import { useMemo } from "react";
|
||||
|
||||
|
||||
|
||||
|
||||
export type RelationshipProps = Edge<{
|
||||
relationship: RelationshipType,
|
||||
selected?: boolean
|
||||
}, 'relationship-edge'>
|
||||
|
||||
export const Relationship: React.FC<EdgeProps<RelationshipProps>> = ({
|
||||
id,
|
||||
sourceX,
|
||||
sourceY,
|
||||
targetX,
|
||||
targetY,
|
||||
source,
|
||||
target,
|
||||
selected,
|
||||
data,
|
||||
}) => {
|
||||
const { getInternalNode, getEdge } = useReactFlow();
|
||||
//const { openRelationshipFromSidebar, selectSidebarSection } = useLayout();
|
||||
//const { checkIfRelationshipRemoved, checkIfNewRelationship } = useDiff();
|
||||
|
||||
const sourceNode = getInternalNode(source);
|
||||
const targetNode = getInternalNode(target);
|
||||
const edge = getEdge(id);
|
||||
|
||||
const sourceHandle: 'left' | 'right' = edge?.sourceHandle?.startsWith?.(
|
||||
"right-"
|
||||
)
|
||||
? 'right'
|
||||
: 'left';
|
||||
|
||||
const sourceWidth = sourceNode?.measured.width ?? 0;
|
||||
const sourceLeftX =
|
||||
sourceHandle === 'left' ? sourceX + 3 : sourceX - sourceWidth - 10;
|
||||
const sourceRightX =
|
||||
sourceHandle === 'left' ? sourceX + sourceWidth + 9 : sourceX;
|
||||
|
||||
const targetWidth = targetNode?.measured.width ?? 0;
|
||||
const targetLeftX = targetX - 1;
|
||||
const targetRightX = targetX + targetWidth + 10;
|
||||
|
||||
const { sourceSide, targetSide } = useMemo(() => {
|
||||
const distances = {
|
||||
leftToLeft: Math.abs(sourceLeftX - targetLeftX),
|
||||
leftToRight: Math.abs(sourceLeftX - targetRightX),
|
||||
rightToLeft: Math.abs(sourceRightX - targetLeftX),
|
||||
rightToRight: Math.abs(sourceRightX - targetRightX),
|
||||
};
|
||||
|
||||
const minDistance = Math.min(
|
||||
distances.leftToLeft,
|
||||
distances.leftToRight,
|
||||
distances.rightToLeft,
|
||||
distances.rightToRight
|
||||
);
|
||||
|
||||
const minDistanceKey = Object.keys(distances).find(
|
||||
(key) => distances[key as keyof typeof distances] === minDistance
|
||||
) as keyof typeof distances;
|
||||
|
||||
switch (minDistanceKey) {
|
||||
case 'leftToRight':
|
||||
return { sourceSide: 'left', targetSide: 'right' };
|
||||
case 'rightToLeft':
|
||||
return { sourceSide: 'right', targetSide: 'left' };
|
||||
case 'rightToRight':
|
||||
return { sourceSide: 'right', targetSide: 'right' };
|
||||
default:
|
||||
return { sourceSide: 'left', targetSide: 'left' };
|
||||
}
|
||||
}, [sourceLeftX, sourceRightX, targetLeftX, targetRightX]);
|
||||
|
||||
const [edgePath] = useMemo(
|
||||
() =>
|
||||
getSmoothStepPath({
|
||||
sourceX: sourceSide === 'left' ? sourceLeftX : sourceRightX,
|
||||
sourceY,
|
||||
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
|
||||
targetY,
|
||||
borderRadius: 14,
|
||||
sourcePosition:
|
||||
sourceSide === 'left' ? Position.Left : Position.Right,
|
||||
targetPosition:
|
||||
targetSide === 'left' ? Position.Left : Position.Right,
|
||||
// offset: (edgeNumber + 1) * 14,
|
||||
}),
|
||||
[
|
||||
sourceSide,
|
||||
targetSide,
|
||||
sourceLeftX,
|
||||
sourceRightX,
|
||||
targetLeftX,
|
||||
targetRightX,
|
||||
sourceY,
|
||||
targetY,
|
||||
// edgeNumber,
|
||||
]
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<path
|
||||
id={id}
|
||||
d={edgePath}
|
||||
//markerStart={`url(#${sourceMarker})`}
|
||||
//markerEnd={`url(#${targetMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
'react-flow__edge-path',
|
||||
`!stroke-2 ${selected ? '!stroke-pink-600' : '!stroke-slate-400'}`,
|
||||
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
console.log("hello world");
|
||||
// openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d={edgePath}
|
||||
fill="none"
|
||||
strokeOpacity={0}
|
||||
strokeWidth={20}
|
||||
// eslint-disable-next-line tailwindcss/no-custom-classname
|
||||
className="react-flow__edge-interaction"
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
// openRelationshipInEditor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>)
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { Table as TableType } from "@/lib/interfaces/table";
|
||||
|
||||
import { Handle, Node, NodeProps, NodeResizer, Position } from "@xyflow/react";
|
||||
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Button, Card, CardBody, CardHeader, cn, Divider, Input } from "@heroui/react";
|
||||
import {
|
||||
ChevronsLeftRight,
|
||||
ChevronsRightLeft,
|
||||
Table2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Check,
|
||||
CircleDotDashed,
|
||||
SquareDot,
|
||||
SquarePlus,
|
||||
SquareMinus,
|
||||
Divide,
|
||||
} from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
|
||||
import { Field } from "@/lib/interfaces/field";
|
||||
import FieldComponent from "./field";
|
||||
|
||||
|
||||
export const MAX_TABLE_SIZE = 450;
|
||||
export const MID_TABLE_SIZE = 337;
|
||||
export const MIN_TABLE_SIZE = 224;
|
||||
export const TABLE_MINIMIZED_FIELDS = 10;
|
||||
|
||||
export type TableProps = Node<{
|
||||
table: TableType, isOverlapping?: boolean;
|
||||
highlightOverlappingTables?: boolean;
|
||||
}>
|
||||
|
||||
const Table: React.FC<NodeProps<TableProps>> = React.memo(({
|
||||
selected,
|
||||
dragging,
|
||||
id,
|
||||
data: { table, },
|
||||
}) => {
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<div className={cn(
|
||||
"w-full h-full bg-background rounded-lg entity-card border-2 noselect ",
|
||||
selected
|
||||
? ' border-2 border-primary-500'
|
||||
: '',
|
||||
|
||||
)}>
|
||||
<div
|
||||
className="h-2 rounded-t-[6px] "
|
||||
style={{ backgroundColor: "rgb(255, 107, 138)" }}
|
||||
></div>
|
||||
|
||||
|
||||
<div className="group gap-2 flex h-9 items-center justify-between bg-default px-2 dark:bg-default">
|
||||
<Table2 className="size-3.5 shrink-0 text-gray-600 dark:text-primary" />
|
||||
{
|
||||
editMode && <>
|
||||
<input
|
||||
placeholder={table.name}
|
||||
autoFocus
|
||||
onBlur={() => setEditMode(false)}
|
||||
type="text"
|
||||
className="rounded-md px-2 py-0.5 w-full border-[0.5px] border-blue-400 bg-slate-100 focus-visible:ring-0 dark:bg-slate-900 text-sm "
|
||||
/>
|
||||
<Button
|
||||
variant="light"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
size="sm"
|
||||
onClick={() => setEditMode(false)}
|
||||
isIconOnly
|
||||
>
|
||||
<Check className="size-3" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
{
|
||||
!editMode &&
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<label
|
||||
className=" w-full text-editable truncate px-2 py-0.5 text-sm font-bold"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{table.name}
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
Double click to edit
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
|
||||
<div className="hidden shrink-0 flex-row group-hover:flex">
|
||||
|
||||
<Button
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
isIconOnly
|
||||
>
|
||||
<CircleDotDashed className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
variant="light"
|
||||
size="sm"
|
||||
className="size-6 p-0 text-slate-500 hover:bg-primary-foreground hover:text-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-200"
|
||||
isIconOnly
|
||||
>
|
||||
{table.width !== MAX_TABLE_SIZE ? (
|
||||
<ChevronsLeftRight className="size-4" />
|
||||
) : (
|
||||
<ChevronsRightLeft className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
className="transition-[max-height] duration-200 ease-in-out"
|
||||
>
|
||||
{table.fields.map((field: Field) => (
|
||||
<FieldComponent
|
||||
key={field.id}
|
||||
field={field}
|
||||
showHandles={selected}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
});
|
||||
|
||||
Table.displayName = 'table';
|
||||
|
||||
export default Table;
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
focused = { false}
|
||||
tableNodeId={id}
|
||||
field={field}
|
||||
highlighted={selectedRelEdges.some(
|
||||
(edge) =>
|
||||
edge.data?.relationship
|
||||
.sourceFieldId === field.id ||
|
||||
edge.data?.relationship
|
||||
.targetFieldId === field.id
|
||||
)}
|
||||
visible={visibleFields.includes(field)}
|
||||
isConnectable={!table.isView}
|
||||
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { NavigateOptions } from "react-router-dom";
|
||||
|
||||
import { HeroUIProvider } from "@heroui/system";
|
||||
import { useHref, useNavigate } from "react-router-dom";
|
||||
|
||||
declare module "@react-types/shared" {
|
||||
interface RouterConfig {
|
||||
routerOptions: NavigateOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export function Provider({ children }: { children: React.ReactNode }) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<HeroUIProvider navigate={navigate} useHref={useHref}>
|
||||
|
||||
{children}
|
||||
</HeroUIProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
import useDashboardRoutes from './dashboard-route';
|
||||
const useAppRoutes = () => {
|
||||
const dashboardRoutes = useDashboardRoutes();
|
||||
return (
|
||||
<Routes>
|
||||
{dashboardRoutes}
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default useAppRoutes;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import DashboardPage from '@/pages/dashboard/dashboard-page';
|
||||
import DatabasePage from '@/pages/database/database-page';
|
||||
import { Route } from 'react-router-dom';
|
||||
import useDatabaseRoutes from './database-route';
|
||||
|
||||
|
||||
|
||||
|
||||
const useDashboardRoutes = () => {
|
||||
const databaseRoutes = useDatabaseRoutes() ;
|
||||
return (
|
||||
<Route path="" element={<DashboardPage />}>
|
||||
{databaseRoutes}
|
||||
</Route>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
export default useDashboardRoutes;
|
||||
@@ -0,0 +1,20 @@
|
||||
|
||||
import DashboardPage from '@/pages/dashboard/dashboard-page';
|
||||
import DatabasePage from '@/pages/database/database-page';
|
||||
import RelationshipController from '@/pages/database/db-controller/relationship-controller/relationship-controller';
|
||||
import TablesController from '@/pages/database/db-controller/tables-controller/tables-controller';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
|
||||
const useDatabaseRoutes = () => {
|
||||
|
||||
return (
|
||||
<Route path="/database" element={<DatabasePage />} >
|
||||
<Route path='tables' element={<TablesController />}></Route>
|
||||
<Route path='relationships' element={<RelationshipController />}></Route>
|
||||
<Route path='bot' element={<DatabasePage />}></Route>
|
||||
</Route>
|
||||
);
|
||||
};
|
||||
|
||||
export default useDatabaseRoutes;
|
||||
@@ -0,0 +1,51 @@
|
||||
.react-flow__attribution {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.text-editable {
|
||||
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
||||
}
|
||||
|
||||
|
||||
.react-flow__node.selected {
|
||||
transition: box-shadow 0.3s ease;
|
||||
/* Add transition */
|
||||
/* box-shadow: 0px 0px 15px 0px hsl(var(--heroui-secondary-50)), 0px 2px 30px 0px hsl(var(--heroui-secondary-50)), 0px 0px 1px 0px hsl(var(--heroui-secondary-50));
|
||||
*/
|
||||
}
|
||||
.noselect {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* Safari */
|
||||
-moz-user-select: none;
|
||||
/* Firefox */
|
||||
-ms-user-select: none;
|
||||
/* IE10+/Edge */
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
.sidebar-item[data-active="true"] svg {
|
||||
color : hsl(var(--heroui-primary-900));
|
||||
|
||||
}
|
||||
|
||||
div[data-slot="input-wrapper"] {
|
||||
outline : none ;
|
||||
border-width: 1px;
|
||||
|
||||
}
|
||||
|
||||
div[data-slot="mainWrapper"] button[data-slot="trigger"] {
|
||||
|
||||
outline : none ;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
|
||||
div[data-slot="content"] hr[role="separator"] {
|
||||
display: none ;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { SVGProps } from "react";
|
||||
|
||||
export type IconSvgProps = SVGProps<SVGSVGElement> & {
|
||||
size?: number;
|
||||
};
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,69 @@
|
||||
import { colors, heroui } from "@heroui/theme";
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
|
||||
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form).js",
|
||||
|
||||
],
|
||||
darkMode: "class",
|
||||
plugins: [heroui({
|
||||
themes: {
|
||||
dark: {
|
||||
colors: {
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(0 0% 98%)',
|
||||
foreground: 'hsl(240 5.3% 26.1%)',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "#961FFE",
|
||||
50: '#961FFE0D', // 5% opacity
|
||||
100: '#961FFE1A', // 10% opacity
|
||||
200: '#961FFE33', // 20% opacity
|
||||
300: '#961FFE4D', // 30% opacity
|
||||
400: '#961FFE66', // 40% opacity
|
||||
500: '#961FFE80', // 50% opacity
|
||||
600: '#961FFE99', // 60% opacity
|
||||
700: '#961FFEB3', // 70% opacity
|
||||
800: '#961FFFCC', // 80% opacity
|
||||
900: '#961FFEE6', // 90% opacity
|
||||
},
|
||||
},
|
||||
},
|
||||
light: {
|
||||
colors: {
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(0 0% 98%)',
|
||||
foreground: 'hsl(240 5.3% 26.1%)',
|
||||
},
|
||||
|
||||
default: {
|
||||
DEFAULT: 'hsl(210 40% 96.1%)',
|
||||
foreground: 'hsl(222.2 47.4% 11.2%)'
|
||||
},
|
||||
|
||||
icon : {
|
||||
DEFAULT : "hsl(215.4 16.3% 46.9%)"
|
||||
} ,
|
||||
|
||||
primary: {
|
||||
DEFAULT: "#961FFE",
|
||||
50: '#961FFE0D', // 5% opacity
|
||||
100: '#961FFE1A', // 10% opacity
|
||||
200: '#961FFE33', // 20% opacity
|
||||
300: '#961FFE4D', // 30% opacity
|
||||
400: '#961FFE66', // 40% opacity
|
||||
500: '#961FFE80', // 50% opacity
|
||||
600: '#961FFE99', // 60% opacity
|
||||
700: '#961FFEB3', // 70% opacity
|
||||
800: '#961FFFCC', // 80% opacity
|
||||
900: '#961FFEE6', // 90% opacity
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}),],
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"rewrites": [
|
||||
{ "source": "/(.*)", "destination": "/" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tsconfigPaths from 'vite-tsconfig-paths'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tsconfigPaths()],
|
||||
server : {
|
||||
port : 3000
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user