create diagram dialog, data layer

This commit is contained in:
Guy Ben-Aharon
2024-08-18 10:30:44 +03:00
parent 991bd29dc6
commit c195104a60
31 changed files with 1483 additions and 189 deletions

467
package-lock.json generated
View File

@@ -22,15 +22,18 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@xyflow/react": "^12.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
"eslint-config-airbnb-typescript": "^18.0.0",
"lucide-react": "^0.424.0",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.3.1",
"react-resizable-panels": "^2.0.22",
"react-router-dom": "^6.26.0",
@@ -422,6 +425,27 @@
"node": ">=6.9.0"
}
},
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz",
"integrity": "sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==",
"license": "MIT",
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
},
"node_modules/@emotion/memoize": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz",
"integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -1913,6 +1937,35 @@
}
}
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz",
"integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.0",
"@radix-ui/react-context": "1.1.0",
"@radix-ui/react-direction": "1.1.0",
"@radix-ui/react-primitive": "2.0.0",
"@radix-ui/react-roving-focus": "1.1.0",
"@radix-ui/react-toggle": "1.1.0",
"@radix-ui/react-use-controllable-state": "1.1.0"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.2.tgz",
@@ -2427,6 +2480,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/hast": {
"version": "2.3.10",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz",
"integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==",
"license": "MIT",
"dependencies": {
"@types/unist": "^2"
}
},
"node_modules/@types/js-cookie": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz",
@@ -2477,6 +2539,18 @@
"@types/react": "*"
}
},
"node_modules/@types/stylis": {
"version": "4.2.5",
"resolved": "https://registry.npmjs.org/@types/stylis/-/stylis-4.2.5.tgz",
"integrity": "sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==",
"license": "MIT"
},
"node_modules/@types/unist": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.18.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz",
@@ -3186,6 +3260,15 @@
"node": ">= 6"
}
},
"node_modules/camelize": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz",
"integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001649",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001649.tgz",
@@ -3222,6 +3305,36 @@
"node": ">=4"
}
},
"node_modules/character-entities": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz",
"integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-entities-legacy": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz",
"integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/character-reference-invalid": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz",
"integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -3689,6 +3802,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/comma-separated-tokens": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz",
"integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/commander": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
@@ -3740,6 +3863,15 @@
"node": ">= 8"
}
},
"node_modules/css-color-keywords": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz",
"integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==",
"license": "ISC",
"engines": {
"node": ">=4"
}
},
"node_modules/css-in-js-utils": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz",
@@ -3749,6 +3881,17 @@
"hyphenate-style-name": "^1.0.3"
}
},
"node_modules/css-to-react-native": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz",
"integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==",
"license": "MIT",
"dependencies": {
"camelize": "^1.0.0",
"css-color-keywords": "^1.0.0",
"postcss-value-parser": "^4.0.2"
}
},
"node_modules/css-tree": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
@@ -4039,6 +4182,12 @@
"integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==",
"license": "MIT"
},
"node_modules/dexie": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ==",
"license": "Apache-2.0"
},
"node_modules/didyoumean": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz",
@@ -5089,6 +5238,19 @@
"reusify": "^1.0.4"
}
},
"node_modules/fault": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz",
"integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==",
"license": "MIT",
"dependencies": {
"format": "^0.2.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -5174,6 +5336,14 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/format": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz",
"integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==",
"engines": {
"node": ">=0.4.x"
}
},
"node_modules/fraction.js": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",
@@ -5500,6 +5670,42 @@
"node": ">= 0.4"
}
},
"node_modules/hast-util-parse-selector": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz",
"integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/hastscript": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz",
"integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==",
"license": "MIT",
"dependencies": {
"@types/hast": "^2.0.0",
"comma-separated-tokens": "^1.0.0",
"hast-util-parse-selector": "^2.0.0",
"property-information": "^5.0.0",
"space-separated-tokens": "^1.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/highlight.js": {
"version": "10.7.3",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/hyphenate-style-name": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz",
@@ -5589,6 +5795,30 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/is-alphabetical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz",
"integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-alphanumerical": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz",
"integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==",
"license": "MIT",
"dependencies": {
"is-alphabetical": "^1.0.0",
"is-decimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
@@ -5735,6 +5965,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-decimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz",
"integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -5794,6 +6034,16 @@
"node": ">=0.10.0"
}
},
"node_modules/is-hexadecimal": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
@@ -6207,6 +6457,20 @@
"loose-envify": "cli.js"
}
},
"node_modules/lowlight": {
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz",
"integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==",
"license": "MIT",
"dependencies": {
"fault": "^1.0.0",
"highlight.js": "~10.7.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
@@ -6585,6 +6849,24 @@
"node": ">=6"
}
},
"node_modules/parse-entities": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz",
"integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==",
"license": "MIT",
"dependencies": {
"character-entities": "^1.0.0",
"character-entities-legacy": "^1.0.0",
"character-reference-invalid": "^1.0.0",
"is-alphanumerical": "^1.0.0",
"is-decimal": "^1.0.0",
"is-hexadecimal": "^1.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -6905,6 +7187,15 @@
"node": ">=6.0.0"
}
},
"node_modules/prismjs": {
"version": "1.29.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz",
"integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -6917,6 +7208,19 @@
"react-is": "^16.13.1"
}
},
"node_modules/property-information": {
"version": "5.6.0",
"resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz",
"integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -6958,6 +7262,24 @@
"node": ">=0.10.0"
}
},
"node_modules/react-code-blocks": {
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/react-code-blocks/-/react-code-blocks-0.1.6.tgz",
"integrity": "sha512-ENNuxG07yO+OuX1ChRje3ieefPRz6yrIpHmebQlaFQgzcAHbUfVeTINpOpoI9bSRSObeYo/OdHsporeToZ7fcg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.4",
"react-syntax-highlighter": "^15.5.0",
"styled-components": "^6.1.0",
"tslib": "^2.6.0"
},
"engines": {
"node": ">=16"
},
"peerDependencies": {
"react": ">=16"
}
},
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -7100,6 +7422,22 @@
}
}
},
"node_modules/react-syntax-highlighter": {
"version": "15.5.0",
"resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz",
"integrity": "sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.3.1",
"highlight.js": "^10.4.1",
"lowlight": "^1.17.0",
"prismjs": "^1.27.0",
"refractor": "^3.6.0"
},
"peerDependencies": {
"react": ">= 0.14.0"
}
},
"node_modules/react-universal-interface": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz",
@@ -7178,6 +7516,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/refractor": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz",
"integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==",
"license": "MIT",
"dependencies": {
"hastscript": "^6.0.0",
"parse-entities": "^2.0.0",
"prismjs": "~1.27.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/refractor/node_modules/prismjs": {
"version": "1.27.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz",
"integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
@@ -7437,6 +7799,12 @@
"node": ">=6.9"
}
},
"node_modules/shallowequal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz",
"integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -7515,6 +7883,16 @@
"node": ">=0.10.0"
}
},
"node_modules/space-separated-tokens": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz",
"integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/stack-generator": {
"version": "2.0.10",
"resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz",
@@ -7782,6 +8160,86 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/styled-components": {
"version": "6.1.12",
"resolved": "https://registry.npmjs.org/styled-components/-/styled-components-6.1.12.tgz",
"integrity": "sha512-n/O4PzRPhbYI0k1vKKayfti3C/IGcPf+DqcrOB7O/ab9x4u/zjqraneT5N45+sIe87cxrCApXM8Bna7NYxwoTA==",
"license": "MIT",
"dependencies": {
"@emotion/is-prop-valid": "1.2.2",
"@emotion/unitless": "0.8.1",
"@types/stylis": "4.2.5",
"css-to-react-native": "3.2.0",
"csstype": "3.1.3",
"postcss": "8.4.38",
"shallowequal": "1.1.0",
"stylis": "4.3.2",
"tslib": "2.6.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/styled-components"
},
"peerDependencies": {
"react": ">= 16.8.0",
"react-dom": ">= 16.8.0"
}
},
"node_modules/styled-components/node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/styled-components/node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"source-map-js": "^1.2.0"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/styled-components/node_modules/tslib": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
"license": "0BSD"
},
"node_modules/stylis": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.2.tgz",
@@ -8570,6 +9028,15 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",

View File

@@ -25,15 +25,18 @@
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2",
"@xyflow/react": "^12.0.4",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"dexie": "^4.0.8",
"eslint-config-airbnb-typescript": "^18.0.0",
"lucide-react": "^0.424.0",
"nanoid": "^5.0.7",
"react": "^18.3.1",
"react-code-blocks": "^0.1.6",
"react-dom": "^18.3.1",
"react-resizable-panels": "^2.0.22",
"react-router-dom": "^6.26.0",

BIN
src/assets/mariadb_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
src/assets/mysql_logo.png Normal file

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: 43 KiB

BIN
src/assets/sqlite_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,55 @@
import { cn } from '@/lib/utils';
import React from 'react';
import { CopyBlock, atomOneDark } from 'react-code-blocks';
import { CodeBlockProps } from 'react-code-blocks/dist/components/CodeBlock';
export interface CodeSnippetProps {
className?: string;
codeProps?: CodeBlockProps;
}
export const CodeSnippet: React.FC<CodeSnippetProps> = ({
className,
codeProps,
}) => {
return (
<div className={cn('flex flex-1', className)}>
<CopyBlock
language="sql"
text={
"WITH fk_info as ( \n\
(SELECT (@fk_info:=NULL),\n\
(SELECT (0)\n\
FROM (SELECT kcu.table_schema,\n\
kcu.table_name,\n\
kcu.column_name as fk_column,\n\
kcu.constraint_name as foreign_key_name,\n\
kcu.referenced_table_name as reference_table,\n\
kcu.referenced_column_name as reference_column,\n\
CONCAT('FOREIGN KEY (', kcu.column_name, ') REFERENCES ', \n\
kcu.referenced_table_name, '(', kcu.refssssssssssssssssssssssssssssssssserenced_column_name, ') ',\n\
'ON UPDATE ', rc.update_rule, \n\
' ON DELETE ', rc.delete_rule) AS fk_def\n\
FROM\n\
information_schema.key_column_usage kcu\n\
JOIN\n\
information_schema.referential_constraints rc\n\
ON kcu.constraint_name = rc.constraint_name\n\
AND kcu.table_name = rc.table_name\n\
WHERE\n\
kcu.referenced_table_name IS NOT NULL) as fk\n\
WHERE table_schema LIKE IFNULL(NULL, '%')\n\
AND table_schema = DATABASE()"
}
theme={atomOneDark}
customStyle={{
display: 'flex',
flex: '1',
fontSize: '14px',
width: '100%',
}}
{...codeProps}
/>
</div>
);
};

View File

@@ -29,8 +29,10 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
showClose?: boolean;
}
>(({ className, children, showClose, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
@@ -42,10 +44,12 @@ const DialogContent = React.forwardRef<
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
{showClose && (
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<Cross2Icon className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
));

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { cn } from '@/lib/utils';
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
);
}
);
Textarea.displayName = 'Textarea';
export { Textarea };

View File

@@ -0,0 +1,59 @@
import React from 'react';
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
import { type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';
import { toggleVariants } from './toggle-variants';
const ToggleGroupContext = React.createContext<
VariantProps<typeof toggleVariants>
>({
size: 'default',
variant: 'default',
});
const ToggleGroup = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Root> &
VariantProps<typeof toggleVariants>
>(({ className, variant, size, children, ...props }, ref) => (
<ToggleGroupPrimitive.Root
ref={ref}
className={cn('flex items-center justify-center gap-1', className)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size }}>
{children}
</ToggleGroupContext.Provider>
</ToggleGroupPrimitive.Root>
));
ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
const ToggleGroupItem = React.forwardRef<
React.ElementRef<typeof ToggleGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof ToggleGroupPrimitive.Item> &
VariantProps<typeof toggleVariants>
>(({ className, children, variant, size, ...props }, ref) => {
const context = React.useContext(ToggleGroupContext);
return (
<ToggleGroupPrimitive.Item
ref={ref}
className={cn(
toggleVariants({
variant: context.variant || variant,
size: context.size || size,
}),
className
)}
{...props}
>
{children}
</ToggleGroupPrimitive.Item>
);
});
ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
export { ToggleGroup, ToggleGroupItem };

View File

@@ -7,12 +7,18 @@ import { DBIndex } from '@/lib/domain/db-index';
import { DBRelationship } from '@/lib/domain/db-relationship';
export interface ChartDBContext {
diagramId: string;
diagramName: string;
databaseType: DatabaseType;
tables: DBTable[];
relationships: DBRelationship[];
// General operations
updateDiagramId: (id: string) => void;
updateDiagramName: (name: string) => void;
// Database type operations
setDatabaseType: (databaseType: DatabaseType) => void;
updateDatabaseType: (databaseType: DatabaseType) => void;
// Table operations
createTable: () => DBTable;
@@ -66,11 +72,17 @@ export interface ChartDBContext {
export const chartDBContext = createContext<ChartDBContext>({
databaseType: DatabaseType.GENERIC,
diagramName: 'New Diagram',
diagramId: '',
tables: [],
relationships: [],
// General operations
updateDiagramId: emptyFn,
updateDiagramName: emptyFn,
// Database type operations
setDatabaseType: emptyFn,
updateDatabaseType: emptyFn,
// Table operations
updateTables: emptyFn,

View File

@@ -10,6 +10,8 @@ import { DBRelationship } from '@/lib/domain/db-relationship';
export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [diagramId, setDiagramId] = React.useState('');
const [diagramName, setDiagramName] = React.useState('New Diagram');
const [databaseType, setDatabaseType] = React.useState<DatabaseType>(
DatabaseType.GENERIC
);
@@ -18,6 +20,10 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
[]
);
const updateDatabaseType = setDatabaseType;
const updateDiagramId = setDiagramId;
const updateDiagramName = setDiagramName;
const addTable = (table: DBTable) => {
setTables((tables) => [...tables, table]);
};
@@ -282,10 +288,14 @@ export const ChartDBProvider: React.FC<React.PropsWithChildren> = ({
return (
<chartDBContext.Provider
value={{
diagramId,
diagramName,
databaseType,
tables,
relationships,
setDatabaseType,
updateDiagramId,
updateDiagramName,
updateDatabaseType,
createTable,
addTable,
getTable,

View File

@@ -0,0 +1,69 @@
import { createContext } from 'react';
import { Diagram } from '@/lib/domain/diagram';
import { emptyFn } from '@/lib/utils';
import { DBRelationship } from '@/lib/domain/db-relationship';
import { DBTable } from '@/lib/domain/db-table';
export interface DataContext {
// Diagram operations
addDiagram: (params: { diagram: Diagram }) => Promise<void>;
listDiagrams: () => Promise<Diagram[]>;
getDiagram: (id: string) => Promise<Diagram | undefined>;
updateDiagram: (params: {
id: string;
diagram: Partial<Diagram>;
}) => Promise<void>;
deleteDiagram: (id: string) => Promise<void>;
// Table operations
addTable: (params: { diagramId: string; table: DBTable }) => Promise<void>;
getTable: (params: {
diagramId: string;
id: string;
}) => Promise<DBTable | undefined>;
updateTable: (params: {
id: string;
table: Partial<DBTable>;
}) => Promise<void>;
deleteTable: (params: { diagramId: string; id: string }) => Promise<void>;
listTables: (diagramId: string) => Promise<DBTable[]>;
// Relationships operations
addRelationship: (params: {
diagramId: string;
relationship: DBRelationship;
}) => Promise<void>;
getRelationship: (params: {
diagramId: string;
id: string;
}) => Promise<DBRelationship | undefined>;
updateRelationship: (params: {
id: string;
relationship: Partial<DBRelationship>;
}) => Promise<void>;
deleteRelationship: (params: {
diagramId: string;
id: string;
}) => Promise<void>;
listRelationships: (diagramId: string) => Promise<DBRelationship[]>;
}
export const dataContext = createContext<DataContext>({
addDiagram: emptyFn,
listDiagrams: emptyFn,
getDiagram: emptyFn,
updateDiagram: emptyFn,
deleteDiagram: emptyFn,
addTable: emptyFn,
getTable: emptyFn,
updateTable: emptyFn,
deleteTable: emptyFn,
listTables: emptyFn,
addRelationship: emptyFn,
getRelationship: emptyFn,
updateRelationship: emptyFn,
deleteRelationship: emptyFn,
listRelationships: emptyFn,
});

View File

@@ -0,0 +1,190 @@
import React from 'react';
import { dataContext } from './data-context';
import Dexie, { type EntityTable } from 'dexie';
import { Diagram } from '@/lib/domain/diagram';
import { DBTable } from '@/lib/domain/db-table';
import { DBRelationship } from '@/lib/domain/db-relationship';
export const DataProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const db = new Dexie('ChartDB') as Dexie & {
diagrams: EntityTable<
Diagram,
'id' // primary key "id" (for the typings only)
>;
db_tables: EntityTable<
DBTable & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
db_relationships: EntityTable<
DBRelationship & { diagramId: string },
'id' // primary key "id" (for the typings only)
>;
};
// Schema declaration:
db.version(1).stores({
diagrams: '++id, name, databaseType, tables, relationships',
db_tables:
'++id, diagramId, name, x, y, fields, indexes, color, createdAt',
db_relationships:
'++id, diagramId, name, sourceTableId, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
});
const addDiagram = async ({ diagram }: { diagram: Diagram }) => {
await db.diagrams.add(diagram);
};
const listDiagrams = async (): Promise<Diagram[]> => {
return await db.diagrams.toArray();
};
const getDiagram = async (id: string): Promise<Diagram | undefined> => {
return await db.diagrams.get(id);
};
const updateDiagram = async ({
diagram,
id,
}: {
id: string;
diagram: Partial<Diagram>;
}) => {
await db.diagrams.update(id, diagram);
};
const deleteDiagram = async (id: string) => {
await Promise.all([
db.diagrams.delete(id),
db.db_tables.where('diagramId').equals(id).delete(),
db.db_relationships.where('diagramId').equals(id).delete(),
]);
};
const addTable = async ({
diagramId,
table,
}: {
diagramId: string;
table: DBTable;
}) => {
await db.db_tables.add({
...table,
diagramId,
});
};
const getTable = async ({
id,
diagramId,
}: {
diagramId: string;
id: string;
}): Promise<DBTable | undefined> => {
return await db.db_tables.get({ id, diagramId });
};
const updateTable = async ({
id,
table,
}: {
id: string;
table: Partial<DBTable>;
}) => {
await db.db_tables.update(id, table);
};
const deleteTable = async ({
id,
diagramId,
}: {
id: string;
diagramId: string;
}) => {
await db.db_tables.where({ id, diagramId }).delete();
};
const listTables = async (diagramId: string): Promise<DBTable[]> => {
return await db.db_tables
.where('diagramId')
.equals(diagramId)
.toArray();
};
const addRelationship = async ({
diagramId,
relationship,
}: {
diagramId: string;
relationship: DBRelationship;
}) => {
await db.db_relationships.add({
...relationship,
diagramId,
});
};
const getRelationship = async ({
id,
diagramId,
}: {
diagramId: string;
id: string;
}): Promise<DBRelationship | undefined> => {
return await db.db_relationships.get({ id, diagramId });
};
const updateRelationship = async ({
id,
relationship,
}: {
id: string;
relationship: Partial<DBRelationship>;
}) => {
await db.db_relationships.update(id, relationship);
};
const deleteRelationship = async ({
id,
diagramId,
}: {
id: string;
diagramId: string;
}) => {
await db.db_relationships.where({ id, diagramId }).delete();
};
const listRelationships = async (
diagramId: string
): Promise<DBRelationship[]> => {
return await db.db_relationships
.where('diagramId')
.equals(diagramId)
.toArray();
};
return (
<dataContext.Provider
value={{
addDiagram,
listDiagrams,
getDiagram,
updateDiagram,
deleteDiagram,
addTable,
getTable,
updateTable,
deleteTable,
listTables,
addRelationship,
getRelationship,
updateRelationship,
deleteRelationship,
listRelationships,
}}
>
{children}
</dataContext.Provider>
);
};

View File

@@ -0,0 +1,13 @@
import { createContext } from 'react';
import { emptyFn } from '@/lib/utils';
export interface CreateDiagramDialogContext {
openCreateDiagramDialog: () => void;
closeCreateDiagramDialog: () => void;
}
export const createDiagramDialogContext =
createContext<CreateDiagramDialogContext>({
openCreateDiagramDialog: emptyFn,
closeCreateDiagramDialog: emptyFn,
});

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { createDiagramDialogContext } from './create-diagram-dialog-context';
import { CreateDiagramDialog } from './create-diagram-dialog';
export const CreateDiagramDialogProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const [open, setOpen] = React.useState(false);
return (
<createDiagramDialogContext.Provider
value={{
openCreateDiagramDialog: () => setOpen(true),
closeCreateDiagramDialog: () => setOpen(false),
}}
>
{children}
<CreateDiagramDialog dialog={{ open }} />
</createDiagramDialogContext.Provider>
);
};

View File

@@ -0,0 +1,257 @@
import React, { useCallback, useEffect } from 'react';
import { Button } from '@/components/button/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/dialog/dialog';
import { DialogProps } from '@radix-ui/react-dialog';
import { ToggleGroup, ToggleGroupItem } from '@/components/toggle/toggle-group';
import { DatabaseType } from '@/lib/domain/database-type';
import { getDatabaseLogo } from '@/lib/databases';
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
import { Textarea } from '@/components/textarea/textarea';
import { useData } from '@/hooks/use-data';
import { Diagram } from '@/lib/domain/diagram';
import { generateId } from '@/lib/utils';
import { useCreateDiagramDialog } from '@/hooks/use-create-diagram-dialog';
import { useNavigate } from 'react-router-dom';
enum CreateDiagramDialogStep {
SELECT_DATABASE = 'SELECT_DATABASE',
IMPORT_DATABASE = 'IMPORT_DATABASE',
}
export interface CreateDiagramDialogProps {
dialog: DialogProps;
}
export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
dialog,
}) => {
const [databaseType, setDatabaseType] = React.useState<DatabaseType>(
DatabaseType.GENERIC
);
const { closeCreateDiagramDialog } = useCreateDiagramDialog();
const [scriptResult, setScriptResult] = React.useState('');
const [step, setStep] = React.useState<CreateDiagramDialogStep>(
CreateDiagramDialogStep.SELECT_DATABASE
);
const { listDiagrams, addDiagram } = useData();
const [diagramNumber, setDiagramNumber] = React.useState<number>(1);
const navigate = useNavigate();
useEffect(() => {
const fetchDiagrams = async () => {
const diagrams = await listDiagrams();
console.log({ diagrams });
setDiagramNumber(diagrams.length + 1);
};
fetchDiagrams();
}, [listDiagrams, setDiagramNumber]);
const createNewDiagram = useCallback(async () => {
const diagram: Diagram = {
id: generateId(),
name: `Diagram ${diagramNumber}`,
databaseType,
tables: [],
relationships: [],
};
await addDiagram({ diagram });
closeCreateDiagramDialog();
navigate(`/diagrams/${diagram.id}`);
}, [
databaseType,
diagramNumber,
addDiagram,
closeCreateDiagramDialog,
navigate,
]);
const renderDatabaseOption = useCallback((type: DatabaseType) => {
const logo = getDatabaseLogo(type);
return (
<ToggleGroupItem
value={type}
aria-label="Toggle bold"
className="flex w-32 h-32"
>
<img src={logo} alt="PostgreSQL" />
</ToggleGroupItem>
);
}, []);
const renderHeader = useCallback(() => {
switch (step) {
case CreateDiagramDialogStep.SELECT_DATABASE:
return (
<DialogHeader>
<DialogTitle>What is your Database?</DialogTitle>
<DialogDescription>
Each database has its own unique features and
capabilities.
</DialogDescription>
</DialogHeader>
);
case CreateDiagramDialogStep.IMPORT_DATABASE:
return (
<DialogHeader>
<DialogTitle>Import your Database</DialogTitle>
</DialogHeader>
);
default:
return null;
}
}, [step]);
const renderContent = useCallback(() => {
switch (step) {
case CreateDiagramDialogStep.SELECT_DATABASE:
return (
<div className="flex flex-1 items-center justify-center">
<ToggleGroup
value={databaseType}
onValueChange={(value: DatabaseType) => {
if (!value) {
setDatabaseType(DatabaseType.GENERIC);
} else {
setDatabaseType(value);
setStep(
CreateDiagramDialogStep.IMPORT_DATABASE
);
}
}}
type="single"
className="grid grid-cols-3 grid-flow-row gap-6 xl:grid-cols-5"
>
{renderDatabaseOption(DatabaseType.MYSQL)}
{renderDatabaseOption(DatabaseType.POSTGRESQL)}
{renderDatabaseOption(DatabaseType.MARIADB)}
{renderDatabaseOption(DatabaseType.SQLITE)}
{renderDatabaseOption(DatabaseType.SQL_SERVER)}
</ToggleGroup>
</div>
);
case CreateDiagramDialogStep.IMPORT_DATABASE:
return (
<div className="flex flex-1 flex-col w-full gap-6">
<div className="flex flex-col gap-1">
<p className="text-sm text-muted-foreground">
1. Run this script in your database:
</p>
<CodeSnippet className="max-h-40 w-full" />
</div>
<div className="flex flex-col gap-1 h-48">
<p className="text-sm text-muted-foreground">
2. Paste the script result here:
</p>
<Textarea
className="flex-1 w-full p-2 text-sm bg-muted-1 rounded-md"
placeholder="Script result here..."
value={scriptResult}
onChange={(e) =>
setScriptResult(e.target.value)
}
/>
</div>
</div>
);
default:
return null;
}
}, [
step,
databaseType,
scriptResult,
setScriptResult,
renderDatabaseOption,
setDatabaseType,
]);
const renderFooter = useCallback(() => {
switch (step) {
case CreateDiagramDialogStep.SELECT_DATABASE:
return (
<DialogFooter className="flex !justify-between gap-2">
<DialogClose asChild>
<Button
type="button"
variant="secondary"
onClick={createNewDiagram}
>
Skip
</Button>
</DialogClose>
<DialogClose asChild>
<Button
type="button"
variant="default"
disabled={databaseType === DatabaseType.GENERIC}
>
Continue
</Button>
</DialogClose>
</DialogFooter>
);
case CreateDiagramDialogStep.IMPORT_DATABASE:
return (
<DialogFooter className="flex !justify-between gap-2">
<DialogClose asChild>
<Button
type="button"
variant="secondary"
onClick={() =>
setStep(
CreateDiagramDialogStep.SELECT_DATABASE
)
}
>
Back
</Button>
</DialogClose>
<div className="flex gap-2">
<DialogClose asChild>
<Button
type="button"
variant="secondary"
onClick={createNewDiagram}
>
Skip this step
</Button>
</DialogClose>
<DialogClose asChild>
<Button
type="button"
variant="default"
disabled={scriptResult.trim().length === 0}
onClick={createNewDiagram}
>
Finish
</Button>
</DialogClose>
</div>
</DialogFooter>
);
default:
return null;
}
}, [step, databaseType, scriptResult, createNewDiagram]);
return (
<Dialog {...dialog}>
<DialogContent
className="flex flex-col min-w-[500px] xl:min-w-[75vw] max-h-[80vh] overflow-y-auto"
showClose={false}
>
{renderHeader()}
{renderContent()}
{renderFooter()}
</DialogContent>
</Dialog>
);
};

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { createDiagramDialogContext } from '@/dialogs/create-diagram-dialog/create-diagram-dialog-context';
export const useCreateDiagramDialog = () =>
useContext(createDiagramDialogContext);

4
src/hooks/use-data.ts Normal file
View File

@@ -0,0 +1,4 @@
import { useContext } from 'react';
import { dataContext } from '@/context/data-context/data-context';
export const useData = () => useContext(dataContext);

View File

@@ -13,6 +13,9 @@ export const dataTypeMap: Record<
readonly (typeof dataTypes)[number][]
> = {
[DatabaseType.GENERIC]: genericDataTypes,
[DatabaseType.POSTGRES]: postgresDataTypes,
[DatabaseType.POSTGRESQL]: postgresDataTypes,
[DatabaseType.MYSQL]: mysqlDataTypes,
[DatabaseType.SQL_SERVER]: [],
[DatabaseType.MARIADB]: [],
[DatabaseType.SQLITE]: [],
} as const;

View File

@@ -1,117 +1,118 @@
import { minimizeQuery } from './minimize-query';
const rawPostgresQuery = `
WITH fk_info AS (
select array_to_string(array_agg(CONCAT('{"schema":"', schema_name, '"',
',"table":"', table_name, '"',
',"column":"', fk_column, '"',
',"foreign_key_name":"', foreign_key_name, '"',
',"reference_table":"', reference_table, '"',
',"reference_column":"', reference_column, '"',
',"fk_def":"', fk_def,
'"}')), ',') as fk_metadata
from (
SELECT
'public' as schema_name,
conname AS foreign_key_name,
conrelid::regclass AS table_name,
(regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[1] AS fk_column,
(regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[2] AS reference_table,
(regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[3] AS reference_column,
pg_get_constraintdef(oid) as fk_def
FROM
pg_constraint
WHERE
contype = 'f'
AND connamespace = 'public'::regnamespace) as x
), pk_info AS (
SELECT 'public' as schema_name,
conrelid::regclass::text AS pk_table,
array_length(string_to_array(substring(pg_get_constraintdef(oid) FROM '\((.*?)\)'), ','), 1) AS field_count,
regexp_split_to_table(substring(pg_get_constraintdef(oid) FROM '\((.*)\)'),',') AS pk_column,
pg_get_constraintdef(oid) as pk_def
FROM pg_constraint c
where connamespace = 'public'::regnamespace
AND c.contype = 'p'
),
indexes_cols as (
select tnsp.nspname as schema_name,
trel.relname as table_name,
pg_relation_size(tnsp.nspname || '.' || '"' || irel.relname || '"') as index_size,
irel.relname as index_name,
am.amname as index_type,
a.attname as col_name,
(case when i.indisunique = true then 'true' else 'false' end) as is_unique,
irel.reltuples as cardinality,
1 + Array_position(i.indkey, a.attnum) as column_position,
case o.OPTION & 1 when 1 then 'DESC' else 'ASC' end as direction,
CASE WHEN indpred IS NOT NULL THEN 'true' ELSE 'false' END as is_partial_index
from pg_index as i
join pg_class as trel on trel.oid = i.indrelid
join pg_namespace as tnsp on trel.relnamespace = tnsp.oid
join pg_class as irel on irel.oid = i.indexrelid
join pg_am as am on irel.relam = am.oid
cross join lateral unnest (i.indkey)
with ordinality as c (colnum, ordinality) left join lateral unnest (i.indoption)
with ordinality as o (option, ordinality)
on c.ordinality = o.ordinality join pg_attribute as a on trel.oid = a.attrelid and a.attnum = c.colnum
where tnsp.nspname not like 'pg_%'
group by tnsp.nspname, trel.relname, irel.relname, am.amname, i.indisunique, i.indexrelid, irel.reltuples, a.attname, array_position(i.indkey, a.attnum), o.OPTION, i.indpred
),
cols as (
select array_to_string(array_agg(CONCAT('{"schema":"', cols.table_schema,
'","table":"', cols.table_name,
'","name":"', cols.column_name,
'","type":"', replace(cols.data_type, '"', ''),
'","ordinal_position":"', cols.ordinal_position,
'","nullable":', case when (cols.IS_NULLABLE = 'YES') then 'true' else 'false' end,
',"is_pk":', case when (pk_column is not null) then 'true' else 'false' end,
',"collation":"', coalesce(cols.COLLATION_NAME, ''), '"}')), ',') as cols_metadata
from information_schema.columns cols
left join pk_info as pk
on pk.schema_name = cols.table_schema and pk_table = cols.table_name and pk_column = cols.column_name
where cols.table_schema not in ('information_schema', 'pg_catalog')
), indexes_metadata as (
select array_to_string(array_agg(CONCAT('{"schema":"', schema_name,
'","table":"', table_name,
'","name":"', index_name,
'","column":"', replace(col_name :: text, '"', E'"'),
'","index_type":"', index_type,
'","cardinality":', cardinality,
',"size":', index_size,
',"unique":', is_unique,
',"is_partial_index":', is_partial_index,
',"direction":"', lower(direction),
'"}')), ',') as indexes_metadata
from indexes_cols x
), tbls as (
select array_to_string(array_agg(CONCAT('{', '"schema":"', TABLE_SCHEMA, '",', '"table":"', TABLE_NAME, '",', '"rows":',
coalesce((select s.n_live_tup
from pg_stat_user_tables s
where tbls.TABLE_SCHEMA = s.schemaname and tbls.TABLE_NAME = s.relname),
0), ', "type":"', TABLE_TYPE, '",', '"engine":"",', '"collation":""}')),
',') as tbls_metadata
from information_schema.tables tbls
where tbls.TABLE_SCHEMA not in ('information_schema', 'pg_catalog')
), config as (
select array_to_string(
array_agg(CONCAT('{"name":"', conf.name, '","value":"', replace(conf.setting, '"', E'"'), '"}')),
',') as config_metadata
from pg_settings conf
), views as
(
select array_to_string(array_agg(CONCAT('{"schema":"', views.schemaname, '","view_name":"', viewname, '"}')),
',') as views_metadata
from pg_views views
where views.schemaname not in ('information_schema', 'pg_catalog')
)
select CONCAT('{ "fk_info": [', coalesce(fk_metadata, ''),
'], "columns": [', coalesce(cols_metadata, ''),
'], "indexes": [', coalesce(indexes_metadata, ''),
'], "tables":[', coalesce(tbls_metadata, ''),
'], "views":[', coalesce(views_metadata, ''),
'], "server_name": "', '', '", "version": "', '',
'"}') as " "
from cols,indexes_metadata, tbls, config, views, fk_info;
`;
const rawPostgresQuery = '';
// const rawPostgresQuery = `
// WITH fk_info AS (
// select array_to_string(array_agg(CONCAT('{"schema":"', schema_name, '"',
// ',"table":"', table_name, '"',
// ',"column":"', fk_column, '"',
// ',"foreign_key_name":"', foreign_key_name, '"',
// ',"reference_table":"', reference_table, '"',
// ',"reference_column":"', reference_column, '"',
// ',"fk_def":"', fk_def,
// '"}')), ',') as fk_metadata
// from (
// SELECT
// 'public' as schema_name,
// conname AS foreign_key_name,
// conrelid::regclass AS table_name,
// (regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[1] AS fk_column,
// (regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[2] AS reference_table,
// (regexp_matches(pg_get_constraintdef(oid), 'FOREIGN KEY \((\w+)\) REFERENCES (\w+)\((\w+)\)', 'g'))[3] AS reference_column,
// pg_get_constraintdef(oid) as fk_def
// FROM
// pg_constraint
// WHERE
// contype = 'f'
// AND connamespace = 'public'::regnamespace) as x
// ), pk_info AS (
// SELECT 'public' as schema_name,
// conrelid::regclass::text AS pk_table,
// array_length(string_to_array(substring(pg_get_constraintdef(oid) FROM '\((.*?)\)'), ','), 1) AS field_count,
// regexp_split_to_table(substring(pg_get_constraintdef(oid) FROM '\((.*)\)'),',') AS pk_column,
// pg_get_constraintdef(oid) as pk_def
// FROM pg_constraint c
// where connamespace = 'public'::regnamespace
// AND c.contype = 'p'
// ),
// indexes_cols as (
// select tnsp.nspname as schema_name,
// trel.relname as table_name,
// pg_relation_size(tnsp.nspname || '.' || '"' || irel.relname || '"') as index_size,
// irel.relname as index_name,
// am.amname as index_type,
// a.attname as col_name,
// (case when i.indisunique = true then 'true' else 'false' end) as is_unique,
// irel.reltuples as cardinality,
// 1 + Array_position(i.indkey, a.attnum) as column_position,
// case o.OPTION & 1 when 1 then 'DESC' else 'ASC' end as direction,
// CASE WHEN indpred IS NOT NULL THEN 'true' ELSE 'false' END as is_partial_index
// from pg_index as i
// join pg_class as trel on trel.oid = i.indrelid
// join pg_namespace as tnsp on trel.relnamespace = tnsp.oid
// join pg_class as irel on irel.oid = i.indexrelid
// join pg_am as am on irel.relam = am.oid
// cross join lateral unnest (i.indkey)
// with ordinality as c (colnum, ordinality) left join lateral unnest (i.indoption)
// with ordinality as o (option, ordinality)
// on c.ordinality = o.ordinality join pg_attribute as a on trel.oid = a.attrelid and a.attnum = c.colnum
// where tnsp.nspname not like 'pg_%'
// group by tnsp.nspname, trel.relname, irel.relname, am.amname, i.indisunique, i.indexrelid, irel.reltuples, a.attname, array_position(i.indkey, a.attnum), o.OPTION, i.indpred
// ),
// cols as (
// select array_to_string(array_agg(CONCAT('{"schema":"', cols.table_schema,
// '","table":"', cols.table_name,
// '","name":"', cols.column_name,
// '","type":"', replace(cols.data_type, '"', ''),
// '","ordinal_position":"', cols.ordinal_position,
// '","nullable":', case when (cols.IS_NULLABLE = 'YES') then 'true' else 'false' end,
// ',"is_pk":', case when (pk_column is not null) then 'true' else 'false' end,
// ',"collation":"', coalesce(cols.COLLATION_NAME, ''), '"}')), ',') as cols_metadata
// from information_schema.columns cols
// left join pk_info as pk
// on pk.schema_name = cols.table_schema and pk_table = cols.table_name and pk_column = cols.column_name
// where cols.table_schema not in ('information_schema', 'pg_catalog')
// ), indexes_metadata as (
// select array_to_string(array_agg(CONCAT('{"schema":"', schema_name,
// '","table":"', table_name,
// '","name":"', index_name,
// '","column":"', replace(col_name :: text, '"', E'"'),
// '","index_type":"', index_type,
// '","cardinality":', cardinality,
// ',"size":', index_size,
// ',"unique":', is_unique,
// ',"is_partial_index":', is_partial_index,
// ',"direction":"', lower(direction),
// '"}')), ',') as indexes_metadata
// from indexes_cols x
// ), tbls as (
// select array_to_string(array_agg(CONCAT('{', '"schema":"', TABLE_SCHEMA, '",', '"table":"', TABLE_NAME, '",', '"rows":',
// coalesce((select s.n_live_tup
// from pg_stat_user_tables s
// where tbls.TABLE_SCHEMA = s.schemaname and tbls.TABLE_NAME = s.relname),
// 0), ', "type":"', TABLE_TYPE, '",', '"engine":"",', '"collation":""}')),
// ',') as tbls_metadata
// from information_schema.tables tbls
// where tbls.TABLE_SCHEMA not in ('information_schema', 'pg_catalog')
// ), config as (
// select array_to_string(
// array_agg(CONCAT('{"name":"', conf.name, '","value":"', replace(conf.setting, '"', E'"'), '"}')),
// ',') as config_metadata
// from pg_settings conf
// ), views as
// (
// select array_to_string(array_agg(CONCAT('{"schema":"', views.schemaname, '","view_name":"', viewname, '"}')),
// ',') as views_metadata
// from pg_views views
// where views.schemaname not in ('information_schema', 'pg_catalog')
// )
// select CONCAT('{ "fk_info": [', coalesce(fk_metadata, ''),
// '], "columns": [', coalesce(cols_metadata, ''),
// '], "indexes": [', coalesce(indexes_metadata, ''),
// '], "tables":[', coalesce(tbls_metadata, ''),
// '], "views":[', coalesce(views_metadata, ''),
// '], "server_name": "', '', '", "version": "', '',
// '"}') as " "
// from cols,indexes_metadata, tbls, config, views, fk_info;
// `;
export const postgresQuery = minimizeQuery(rawPostgresQuery);

23
src/lib/databases.ts Normal file
View File

@@ -0,0 +1,23 @@
import MysqlLogo from '@/assets/mysql_logo.png';
import PostgresqlLogo from '@/assets/postgresql_logo.png';
import MariaDBLogo from '@/assets/mariadb_logo.png';
import SqliteLogo from '@/assets/sqlite_logo.png';
import SqlServerLogo from '@/assets/sql_server_logo.png';
import { DatabaseType } from './domain/database-type';
export const getDatabaseLogo = (databaseType: DatabaseType) => {
switch (databaseType) {
case DatabaseType.MYSQL:
return MysqlLogo;
case DatabaseType.POSTGRESQL:
return PostgresqlLogo;
case DatabaseType.MARIADB:
return MariaDBLogo;
case DatabaseType.SQLITE:
return SqliteLogo;
case DatabaseType.SQL_SERVER:
return SqlServerLogo;
default:
return PostgresqlLogo;
}
};

View File

@@ -1,5 +1,8 @@
export enum DatabaseType {
GENERIC = 'generic',
POSTGRES = 'postgres',
POSTGRESQL = 'postgresql',
MYSQL = 'mysql',
SQL_SERVER = 'sql_server',
MARIADB = 'mariadb',
SQLITE = 'sqlite',
}

11
src/lib/domain/diagram.ts Normal file
View File

@@ -0,0 +1,11 @@
import { DatabaseType } from './database-type';
import { DBRelationship } from './db-relationship';
import { DBTable } from './db-table';
export interface Diagram {
id: string;
name: string;
databaseType: DatabaseType;
tables: DBTable[];
relationships: DBRelationship[];
}

View File

@@ -1,7 +1,7 @@
import { type ClassValue, clsx } from 'clsx';
import { customAlphabet } from 'nanoid';
const randomId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz-', 23);
const randomId = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 25);
const randonNumber = customAlphabet('1234567890', 18);
import { twMerge } from 'tailwind-merge';

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import { TopNavbar } from './top-navbar/top-navbar';
import { Toolbar } from './toolbar/toolbar';
import {
@@ -8,8 +8,19 @@ import {
} from '@/components/resizable/resizable';
import { SidePanel } from './side-panel/side-panel';
import { Canvas } from './canvas/canvas';
import { useParams } from 'react-router-dom';
import { useCreateDiagramDialog } from '@/hooks/use-create-diagram-dialog';
export const EditorPage: React.FC = () => {
const { openCreateDiagramDialog } = useCreateDiagramDialog();
const { diagramId } = useParams<{ diagramId: string }>();
useEffect(() => {
if (!diagramId) {
openCreateDiagramDialog();
}
}, [diagramId, openCreateDiagramDialog]);
return (
<section className="bg-background h-screen w-screen flex flex-col">
<TopNavbar />

View File

@@ -40,7 +40,9 @@ export const RelationshipListItemHeader: React.FC<
const editRelationshipName = useCallback(() => {
if (!editMode) return;
if (relationshipName.trim()) {
updateRelationship(relationship.id, { name: relationshipName });
updateRelationship(relationship.id, {
name: relationshipName.trim(),
});
}
setEditMode(false);

View File

@@ -40,7 +40,7 @@ export const TableListItemHeader: React.FC<TableListItemHeaderProps> = ({
const editTableName = useCallback(() => {
if (!editMode) return;
if (tableName.trim()) {
updateTable(table.id, { name: tableName });
updateTable(table.id, { name: tableName.trim() });
}
setEditMode(false);

View File

@@ -1,7 +1,6 @@
import React from 'react';
import React, { useCallback } from 'react';
import {
Menubar,
MenubarCheckboxItem,
MenubarContent,
MenubarItem,
MenubarMenu,
@@ -13,10 +12,39 @@ import {
MenubarTrigger,
} from '@/components/menubar/menubar';
import { Label } from '@/components/label/label';
import { Button } from '@/components/button/button';
import { Check, Pencil } from 'lucide-react';
import { Input } from '@/components/input/input';
import { useChartDB } from '@/hooks/use-chartdb';
import { useClickAway, useKeyPressEvent } from 'react-use';
export interface TopNavbarProps {}
export const TopNavbar: React.FC<TopNavbarProps> = () => {
const { diagramName, updateDiagramName } = useChartDB();
const [editMode, setEditMode] = React.useState(false);
const [editedDiagramName, setEditedDiagramName] =
React.useState(diagramName);
const inputRef = React.useRef<HTMLInputElement>(null);
const editDiagramName = useCallback(() => {
if (!editMode) return;
if (editedDiagramName.trim()) {
updateDiagramName(editedDiagramName.trim());
}
setEditMode(false);
}, [editedDiagramName, updateDiagramName, editMode]);
useClickAway(inputRef, editDiagramName);
useKeyPressEvent('Enter', editDiagramName);
const enterEditMode = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.stopPropagation();
setEditMode(true);
};
return (
<nav className="flex flex-row items-center justify-between px-4">
<div className="flex flex-1 justify-start gap-x-3">
@@ -27,17 +55,27 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
<MenubarTrigger>File</MenubarTrigger>
<MenubarContent>
<MenubarItem>
New Tab{' '}
New
<MenubarShortcut>T</MenubarShortcut>
</MenubarItem>
<MenubarItem>
New Window{' '}
New Window
<MenubarShortcut>N</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled>
New Incognito Window
</MenubarItem>
<MenubarItem>Open</MenubarItem>
<MenubarItem>Save</MenubarItem>
<MenubarItem>Save as</MenubarItem>
<MenubarSeparator />
<MenubarItem>Export</MenubarItem>
<MenubarSub>
<MenubarSubTrigger>
Export as
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>PNG</MenubarItem>
<MenubarItem>JPG</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarSub>
<MenubarSubTrigger>Share</MenubarSubTrigger>
<MenubarSubContent>
@@ -47,10 +85,7 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
</MenubarSubContent>
</MenubarSub>
<MenubarSeparator />
<MenubarItem>
Print...{' '}
<MenubarShortcut>P</MenubarShortcut>
</MenubarItem>
<MenubarItem>Exit</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
@@ -63,57 +98,63 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
Redo <MenubarShortcut>Z</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarSub>
<MenubarSubTrigger>Find</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem>
Search the web
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Find...</MenubarItem>
<MenubarItem>Find Next</MenubarItem>
<MenubarItem>Find Previous</MenubarItem>
</MenubarSubContent>
</MenubarSub>
<MenubarItem>
Find <MenubarShortcut>F</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem>Cut</MenubarItem>
<MenubarItem>Copy</MenubarItem>
<MenubarItem>Paste</MenubarItem>
<MenubarItem>Clear</MenubarItem>
</MenubarContent>
</MenubarMenu>
<MenubarMenu>
<MenubarTrigger>View</MenubarTrigger>
<MenubarTrigger>Help</MenubarTrigger>
<MenubarContent>
<MenubarCheckboxItem>
Always Show Bookmarks Bar
</MenubarCheckboxItem>
<MenubarCheckboxItem checked>
Always Show Full URLs
</MenubarCheckboxItem>
<MenubarSeparator />
<MenubarItem inset>
Reload <MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarItem disabled inset>
Force Reload{' '}
<MenubarShortcut>R</MenubarShortcut>
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>
Toggle Fullscreen
</MenubarItem>
<MenubarSeparator />
<MenubarItem inset>Hide Sidebar</MenubarItem>
<MenubarItem>Report a bug</MenubarItem>
<MenubarItem>Visit ChartDB</MenubarItem>
</MenubarContent>
</MenubarMenu>
</Menubar>
</div>
</div>
<div className="flex flex-1 justify-center">
<Label>Diagrams/</Label>
<Label contentEditable suppressContentEditableWarning>
aaa
</Label>
<div className="flex flex-row flex-1 justify-center items-center group">
<div className="flex">
<Label>Diagrams/</Label>
</div>
<div className="flex flex-row items-center gap-1">
{editMode ? (
<>
<Input
ref={inputRef}
autoFocus
type="text"
placeholder={diagramName}
value={editedDiagramName}
onClick={(e) => e.stopPropagation()}
onChange={(e) =>
setEditedDiagramName(e.target.value)
}
className="h-7 focus-visible:ring-0"
/>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={editDiagramName}
>
<Check />
</Button>
</>
) : (
<>
<Label>{diagramName}</Label>
<Button
variant="ghost"
className="hover:bg-primary-foreground p-2 w-7 h-7 text-slate-500 hover:text-slate-700 hidden group-hover:flex"
onClick={enterEditMode}
>
<Pencil />
</Button>
</>
)}
</div>
</div>
<div className="flex flex-1 justify-end"></div>
</nav>

View File

@@ -4,18 +4,24 @@ import { NotFoundPage } from './pages/not-found-page/not-found-page';
import { EditorPage } from './pages/editor-page/editor-page';
import { ChartDBProvider } from './context/chartdb-context/chartdb-provider';
import { ReactFlowProvider } from '@xyflow/react';
import { DataProvider } from './context/data-context/data-provider';
import { CreateDiagramDialogProvider } from './dialogs/create-diagram-dialog/create-diagram-dialog-provider';
const routes: RouteObject[] = [
{
path: '/',
...['', 'diagrams/:diagramId'].map((path) => ({
path,
element: (
<ChartDBProvider>
<ReactFlowProvider>
<EditorPage />
</ReactFlowProvider>
</ChartDBProvider>
<DataProvider>
<ChartDBProvider>
<CreateDiagramDialogProvider>
<ReactFlowProvider>
<EditorPage />
</ReactFlowProvider>
</CreateDiagramDialogProvider>
</ChartDBProvider>
</DataProvider>
),
},
})),
{
path: '*',
element: <NotFoundPage />,