Compare commits
102 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
44eac7daff | ||
|
|
502472b083 | ||
|
|
52d2ea596c | ||
|
|
bd67ccfbcf | ||
|
|
62beb68fa1 | ||
|
|
09b1275475 | ||
|
|
5dd7fe75d1 | ||
|
|
2939320a15 | ||
|
|
a643852837 | ||
|
|
467ff697c9 | ||
|
|
d6919f3033 | ||
|
|
56382a9fdc | ||
|
|
e06eb2a48e | ||
|
|
543b716c77 | ||
|
|
b55d631146 | ||
|
|
ef118929ad | ||
|
|
68f48190c9 | ||
|
|
bba265ad43 | ||
|
|
cbc4e85a14 | ||
|
|
26a0a5b550 | ||
|
|
b935b7f251 | ||
|
|
a1c0cf102a | ||
|
|
ab89bad6d5 | ||
|
|
deb218423f | ||
|
|
48342471ac | ||
|
|
47bb87a88f | ||
|
|
a96c2e1078 | ||
|
|
26d95eed25 | ||
|
|
be65328f24 | ||
|
|
85fd14fa02 | ||
|
|
9c485b3b01 | ||
|
|
e993f1549c | ||
|
|
0db67ea42a | ||
|
|
b9e621bd68 | ||
|
|
93d59f8887 | ||
|
|
190e4f4ffa | ||
|
|
dc404c9d7e | ||
|
|
dd4324d64f | ||
|
|
1878083056 | ||
|
|
7b6271962a | ||
|
|
2edc8dfde8 | ||
|
|
004d530880 | ||
|
|
fd2cc9fcfc | ||
|
|
4c93326bb6 | ||
|
|
ef3d7a8b67 | ||
|
|
3b3be086b1 | ||
|
|
b424518212 | ||
|
|
99a8201398 | ||
|
|
eb9b41e4f6 | ||
|
|
fef6d3f499 | ||
|
|
14f11c27a7 | ||
|
|
2118bce0f0 | ||
|
|
88be6c1fd4 | ||
|
|
0dcc9b9568 | ||
|
|
ff3269ec05 | ||
|
|
659dc2e3e7 | ||
|
|
c36cd33180 | ||
|
|
58231c9139 | ||
|
|
1643e7bdeb | ||
|
|
42d4cbac8c | ||
|
|
7452ca6965 | ||
|
|
27aede7794 | ||
|
|
e9e2736cb2 | ||
|
|
74c1730425 | ||
|
|
94bed7fcce | ||
|
|
8abf2a7bfc | ||
|
|
ee659eaa03 | ||
|
|
7c5db0848e | ||
|
|
4b43f720e9 | ||
|
|
766b5164b8 | ||
|
|
7868ca9f42 | ||
|
|
0411742864 | ||
|
|
9831ac5a10 | ||
|
|
91c6fb9249 | ||
|
|
c155013668 | ||
|
|
1b0f293c87 | ||
|
|
df2dc03aa0 | ||
|
|
205d431c89 | ||
|
|
0abe18cdf9 | ||
|
|
a151f56b5d | ||
|
|
2b6b733261 | ||
|
|
b56b04925c | ||
|
|
635fb53c9f | ||
|
|
d6659795bc | ||
|
|
348f80568e | ||
|
|
5f9c74a9ad | ||
|
|
5409288388 | ||
|
|
2309306ef5 | ||
|
|
3574cecc7c | ||
|
|
29b8edc051 | ||
|
|
5fc10a7e64 | ||
|
|
807cd22e0c | ||
|
|
03772f6b4f | ||
|
|
885eb719de | ||
|
|
94656ec7a5 | ||
|
|
a0e966b64f | ||
|
|
a8fe491c1b | ||
|
|
ddeef3b134 | ||
|
|
d45677e92d | ||
|
|
9c7d03c285 | ||
|
|
be1b109f23 | ||
|
|
05eaf85a3d |
@@ -1,29 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
root: true,
|
|
||||||
env: { browser: true, es2020: true },
|
|
||||||
extends: [
|
|
||||||
'eslint:recommended',
|
|
||||||
'plugin:react/recommended',
|
|
||||||
'plugin:@typescript-eslint/recommended',
|
|
||||||
'plugin:react-hooks/recommended',
|
|
||||||
'plugin:css-modules/recommended',
|
|
||||||
'plugin:tailwindcss/recommended',
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
// 'plugin:jsx-a11y/recommended',
|
|
||||||
],
|
|
||||||
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: ['react-refresh', 'css-modules', 'tailwindcss', 'jsx-a11y'],
|
|
||||||
rules: {
|
|
||||||
'@typescript-eslint/consistent-type-imports': 'error',
|
|
||||||
'react-refresh/only-export-components': [
|
|
||||||
'warn',
|
|
||||||
{ allowConstantExport: true },
|
|
||||||
],
|
|
||||||
'react/no-unescaped-entities': 'off',
|
|
||||||
'react/prop-types': 'off',
|
|
||||||
},
|
|
||||||
settings: {
|
|
||||||
react: { version: 'detect' },
|
|
||||||
},
|
|
||||||
};
|
|
||||||
154
CHANGELOG.md
@@ -1,5 +1,159 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [1.9.0](https://github.com/chartdb/chartdb/compare/v1.8.1...v1.9.0) (2025-03-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **canvas:** highlight the Show-All button when No-Tables are visible in the canvas ([#612](https://github.com/chartdb/chartdb/issues/612)) ([62beb68](https://github.com/chartdb/chartdb/commit/62beb68fa1ec22ccd4fe5e59a8ceb9d3e8f6d374))
|
||||||
|
* **chart max length:** add support for edit char max length ([#613](https://github.com/chartdb/chartdb/issues/613)) ([09b1275](https://github.com/chartdb/chartdb/commit/09b12754757b9625ca287d91a92cf0d83c9e2b89))
|
||||||
|
* **chart max length:** enable edit length from data type select box ([#616](https://github.com/chartdb/chartdb/issues/616)) ([bd67ccf](https://github.com/chartdb/chartdb/commit/bd67ccfbcf66b919453ca6c0bfd71e16772b3d8e))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **cardinality:** set true as default ([#583](https://github.com/chartdb/chartdb/issues/583)) ([2939320](https://github.com/chartdb/chartdb/commit/2939320a15a9ccd9eccfe46c26e04ca1edca2420))
|
||||||
|
* **performance:** Optimize performance of field comments editing ([#610](https://github.com/chartdb/chartdb/issues/610)) ([5dd7fe7](https://github.com/chartdb/chartdb/commit/5dd7fe75d1b0378ba406c75183c5e2356730c3b4))
|
||||||
|
* remove Buckle dialog ([#617](https://github.com/chartdb/chartdb/issues/617)) ([502472b](https://github.com/chartdb/chartdb/commit/502472b08342be425e66e2b6c94e5fe37ba14aa9))
|
||||||
|
* **shorcuts:** add shortcut to toggle the theme ([#602](https://github.com/chartdb/chartdb/issues/602)) ([a643852](https://github.com/chartdb/chartdb/commit/a6438528375ab54d3ec7d80ac6b6ddd65ea8cf1e))
|
||||||
|
|
||||||
|
## [1.8.1](https://github.com/chartdb/chartdb/compare/v1.8.0...v1.8.1) (2025-03-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **add-docs:** add link to ChartDB documentation ([#597](https://github.com/chartdb/chartdb/issues/597)) ([b55d631](https://github.com/chartdb/chartdb/commit/b55d631146ff3a1f7d63c800d44b5d3d3a223c76))
|
||||||
|
* components config ([#591](https://github.com/chartdb/chartdb/issues/591)) ([cbc4e85](https://github.com/chartdb/chartdb/commit/cbc4e85a14e24a43f9ff470518f8fe2845046bdb))
|
||||||
|
* **docker config:** Environment Variable Handling and Configuration Logic ([#605](https://github.com/chartdb/chartdb/issues/605)) ([d6919f3](https://github.com/chartdb/chartdb/commit/d6919f30336cc846fe6e6505b5a5278aa14dcce6))
|
||||||
|
* **empty-state:** show diff buttons on import-dbml when triggered by empty ([#574](https://github.com/chartdb/chartdb/issues/574)) ([4834247](https://github.com/chartdb/chartdb/commit/48342471ac231922f2ca4455b74a9879127a54f1))
|
||||||
|
* **i18n:** add [FR] translation ([#579](https://github.com/chartdb/chartdb/issues/579)) ([ab89bad](https://github.com/chartdb/chartdb/commit/ab89bad6d544ba4c339a3360eeec7d29e5579511))
|
||||||
|
* **img-export:** add ChartDB watermark to exported image ([#588](https://github.com/chartdb/chartdb/issues/588)) ([b935b7f](https://github.com/chartdb/chartdb/commit/b935b7f25111d5f72b7f8d7c552a4ea5974f791e))
|
||||||
|
* **import-mssql:** fix import/export scripts to handle data correctly ([#598](https://github.com/chartdb/chartdb/issues/598)) ([e06eb2a](https://github.com/chartdb/chartdb/commit/e06eb2a48e6bd3bcf352f4bcf128214c7da4c1b1))
|
||||||
|
* **menu-backup:** update export to be backup ([#590](https://github.com/chartdb/chartdb/issues/590)) ([26a0a5b](https://github.com/chartdb/chartdb/commit/26a0a5b550ef5e47e89b00d0232dc98936f63f23))
|
||||||
|
* open create new diagram when there is no diagram ([#594](https://github.com/chartdb/chartdb/issues/594)) ([ef11892](https://github.com/chartdb/chartdb/commit/ef118929ad5d5cbfae0290061bd8ea30bd262496))
|
||||||
|
* **open diagram:** in case there is no diagram, opens the dialog ([#593](https://github.com/chartdb/chartdb/issues/593)) ([68f4819](https://github.com/chartdb/chartdb/commit/68f48190c93f155398cca15dd7af2a025de2d45f))
|
||||||
|
* **side-panel:** simplify how to add field and index ([#573](https://github.com/chartdb/chartdb/issues/573)) ([a1c0cf1](https://github.com/chartdb/chartdb/commit/a1c0cf102add4fb235e913e75078139b3961341b))
|
||||||
|
* **sql_server_export:** use sql server export ([#600](https://github.com/chartdb/chartdb/issues/600)) ([56382a9](https://github.com/chartdb/chartdb/commit/56382a9fdc5e3044f8811873dd8a79f590771896))
|
||||||
|
* **sqlite-import:** import nuallable columns correctly + add json type ([#571](https://github.com/chartdb/chartdb/issues/571)) ([deb2184](https://github.com/chartdb/chartdb/commit/deb218423f77f0c0945a93005696456f62b00ce3))
|
||||||
|
|
||||||
|
## [1.8.0](https://github.com/chartdb/chartdb/compare/v1.7.0...v1.8.0) (2025-02-13)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **dbml-import:** add error highlighting for dbml imports ([#556](https://github.com/chartdb/chartdb/issues/556)) ([190e4f4](https://github.com/chartdb/chartdb/commit/190e4f4ffa834fa621f264dc608ca3f3b393a331))
|
||||||
|
* **docker image:** add support for custom inference servers ([#543](https://github.com/chartdb/chartdb/issues/543)) ([1878083](https://github.com/chartdb/chartdb/commit/1878083056ea4db7a05cdeeb38a4f7b9f5f95bd1))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas:** add right-click option to create relationships ([#568](https://github.com/chartdb/chartdb/issues/568)) ([e993f15](https://github.com/chartdb/chartdb/commit/e993f1549c4c86bb9e7e36062db803ba6613b3b3))
|
||||||
|
* **canvas:** locate table from canvas ([#560](https://github.com/chartdb/chartdb/issues/560)) ([dc404c9](https://github.com/chartdb/chartdb/commit/dc404c9d7ee272c93aac69646bac859829a5234e))
|
||||||
|
* **docker:** add option to hide popups ([#580](https://github.com/chartdb/chartdb/issues/580)) ([a96c2e1](https://github.com/chartdb/chartdb/commit/a96c2e107838d2dc13b586923fd9dbe06598cdd8))
|
||||||
|
* **export-sql:** show create script for only filtered schemas ([#570](https://github.com/chartdb/chartdb/issues/570)) ([85fd14f](https://github.com/chartdb/chartdb/commit/85fd14fa02bb2879c36bba53369dbf2e7fa578d4))
|
||||||
|
* **i18n:** fix Ukrainian ([#554](https://github.com/chartdb/chartdb/issues/554)) ([7b62719](https://github.com/chartdb/chartdb/commit/7b6271962a99bfe5ffbd0176e714c76368ef5c41))
|
||||||
|
* **import dbml:** add import for indexes ([#566](https://github.com/chartdb/chartdb/issues/566)) ([0db67ea](https://github.com/chartdb/chartdb/commit/0db67ea42a5f9585ca1d246db7a7ff0239bec0ba))
|
||||||
|
* **import-query:** improve the cleanup for messy json input ([#562](https://github.com/chartdb/chartdb/issues/562)) ([93d59f8](https://github.com/chartdb/chartdb/commit/93d59f8887765098d040a3184aaee32112f67267))
|
||||||
|
* **index unique:** extract unique toggle for faster editing ([#559](https://github.com/chartdb/chartdb/issues/559)) ([dd4324d](https://github.com/chartdb/chartdb/commit/dd4324d64f7638ada5c022a2ab38bd8e6986af25))
|
||||||
|
* **mssql-import:** improve script readability by adding edition comment ([#572](https://github.com/chartdb/chartdb/issues/572)) ([be65328](https://github.com/chartdb/chartdb/commit/be65328f24b0361638b9e2edb39eaa9906e77f67))
|
||||||
|
* **realtionships section:** add the schema to source/target tables ([#561](https://github.com/chartdb/chartdb/issues/561)) ([b9e621b](https://github.com/chartdb/chartdb/commit/b9e621bd680730a0ffbf1054d735bfa418711cae))
|
||||||
|
* **sqlserver-import:** open ssms guide when max chars ([#565](https://github.com/chartdb/chartdb/issues/565)) ([9c485b3](https://github.com/chartdb/chartdb/commit/9c485b3b01a131bf551c7e95916b0c416f6aa0b5))
|
||||||
|
* **table actions:** fix size of table actions ([#578](https://github.com/chartdb/chartdb/issues/578)) ([26d95ee](https://github.com/chartdb/chartdb/commit/26d95eed25d86452d9168a9d93a301ba50d934e3))
|
||||||
|
|
||||||
|
## [1.7.0](https://github.com/chartdb/chartdb/compare/v1.6.1...v1.7.0) (2025-02-03)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **dbml-editor:** add dbml editor in side pannel ([#534](https://github.com/chartdb/chartdb/issues/534)) ([88be6c1](https://github.com/chartdb/chartdb/commit/88be6c1fd4a7e1f20937e8204c14d8fc1c2665b4))
|
||||||
|
* **import-dbml:** add import dbml functionality ([#549](https://github.com/chartdb/chartdb/issues/549)) ([b424518](https://github.com/chartdb/chartdb/commit/b424518212290a870fdb7c420a303f65f5901429))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **canvas edit:** add option to edit names in canvas ([#536](https://github.com/chartdb/chartdb/issues/536)) ([0dcc9b9](https://github.com/chartdb/chartdb/commit/0dcc9b9568cfe749d44d2e93cb365ba3d3a1e71c))
|
||||||
|
* **dbml-editor:** add shortcuts to dbml and filter: [#534](https://github.com/chartdb/chartdb/issues/534) ([#535](https://github.com/chartdb/chartdb/issues/535)) ([3b3be08](https://github.com/chartdb/chartdb/commit/3b3be086b1e8d5acf999f8504580d9e2f956f7da))
|
||||||
|
* **dbml:** add error handling ([#545](https://github.com/chartdb/chartdb/issues/545)) ([fef6d3f](https://github.com/chartdb/chartdb/commit/fef6d3f4996130a3769d1f25b4b1f2090293a1bf))
|
||||||
|
* **empty-state:** fix dark-mode for empty-state ([#547](https://github.com/chartdb/chartdb/issues/547)) ([99a8201](https://github.com/chartdb/chartdb/commit/99a820139861546a012d7b562ddbb9b77698151a))
|
||||||
|
* **examples:** fix employee example dbml ([#544](https://github.com/chartdb/chartdb/issues/544)) ([2118bce](https://github.com/chartdb/chartdb/commit/2118bce0f00d55eb19d22b9fa2d4964ba2533a09))
|
||||||
|
* **i18n:** translation/Ukrainian ([#529](https://github.com/chartdb/chartdb/issues/529)) ([ff3269e](https://github.com/chartdb/chartdb/commit/ff3269ec0510bbae4bc114e65a1ea86a656e8785))
|
||||||
|
* **open-diagram:** add arrow keys navigation in open diagram dialog ([#537](https://github.com/chartdb/chartdb/issues/537)) ([14f11c2](https://github.com/chartdb/chartdb/commit/14f11c27a7ad5b990131c8495148cabf12835082))
|
||||||
|
* **performance:** fix bundle size ([#551](https://github.com/chartdb/chartdb/issues/551)) ([4c93326](https://github.com/chartdb/chartdb/commit/4c93326bb6e3eaa143373c500a0c641e95a53fb9))
|
||||||
|
* **performance:** reduce bundle size ([#553](https://github.com/chartdb/chartdb/issues/553)) ([004d530](https://github.com/chartdb/chartdb/commit/004d530880a50dea6e9786eb9ae63cf592a4d852))
|
||||||
|
* **performance:** resolve error on startup ([#552](https://github.com/chartdb/chartdb/issues/552)) ([fd2cc9f](https://github.com/chartdb/chartdb/commit/fd2cc9fcfc8f4a9f0bc79def47d89114159392fb))
|
||||||
|
* **psql-import:** remove typo for import command (psql) ([#546](https://github.com/chartdb/chartdb/issues/546)) ([eb9b41e](https://github.com/chartdb/chartdb/commit/eb9b41e4f656bec1451c45763f4ea5b547aeec5c))
|
||||||
|
* **scroll:** fix scroll area ([#550](https://github.com/chartdb/chartdb/issues/550)) ([ef3d7a8](https://github.com/chartdb/chartdb/commit/ef3d7a8b67431e923b75bf8287b86bbc8abe723b))
|
||||||
|
|
||||||
|
## [1.6.1](https://github.com/chartdb/chartdb/compare/v1.6.0...v1.6.1) (2025-01-26)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* change empty state image ([#531](https://github.com/chartdb/chartdb/issues/531)) ([42d4cba](https://github.com/chartdb/chartdb/commit/42d4cbac8ce352e0e4e155d7003bfb85296b897f))
|
||||||
|
* **chat-type:** remove typo of char datatype in examples ([#530](https://github.com/chartdb/chartdb/issues/530)) ([58231c9](https://github.com/chartdb/chartdb/commit/58231c91393de30ebff817f0ebc57a5c5579f106))
|
||||||
|
* **empty_state:** customize empty state ([#533](https://github.com/chartdb/chartdb/issues/533)) ([1643e7b](https://github.com/chartdb/chartdb/commit/1643e7bdeb1bbaf081ab064e871d102c87243c0a))
|
||||||
|
* **Image Export:** importing css rules error while download image ([#524](https://github.com/chartdb/chartdb/issues/524)) ([e9e2736](https://github.com/chartdb/chartdb/commit/e9e2736cb2203702d53df9afc30b8e989a8c9953))
|
||||||
|
* **shortcuts:** add zoom all shortcut ([#528](https://github.com/chartdb/chartdb/issues/528)) ([7452ca6](https://github.com/chartdb/chartdb/commit/7452ca6965b0332a93b686c397ddf51013e42506))
|
||||||
|
* **filter-tables:** show clean filter if no-results ([#532](https://github.com/chartdb/chartdb/issues/532)) ([c36cd33](https://github.com/chartdb/chartdb/commit/c36cd33180badaa9b7f9e27c765f19cb03a50ccd))
|
||||||
|
|
||||||
|
## [1.6.0](https://github.com/chartdb/chartdb/compare/v1.5.1...v1.6.0) (2025-01-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **view-menu:** add toggle for mini map visibility ([#496](https://github.com/chartdb/chartdb/issues/496)) ([#505](https://github.com/chartdb/chartdb/issues/505)) ([8abf2a7](https://github.com/chartdb/chartdb/commit/8abf2a7bfcc36d39e60ac133b0e5e569de1bbc72))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* add loadDiagramFromData logic to chartdb provider ([#513](https://github.com/chartdb/chartdb/issues/513)) ([ee659ea](https://github.com/chartdb/chartdb/commit/ee659eaa038a94ee13801801e84152df4d79683d))
|
||||||
|
* **dependency:** upgrade react query to v7 - clean console warnings ([#504](https://github.com/chartdb/chartdb/issues/504)) ([7c5db08](https://github.com/chartdb/chartdb/commit/7c5db0848e49dfdb7e7120f77003d1e37f8d71b0))
|
||||||
|
* **i18n:** translation/Arabic ([#509](https://github.com/chartdb/chartdb/issues/509)) ([4b43f72](https://github.com/chartdb/chartdb/commit/4b43f720e90e49d5461e68d188e3865000f52497))
|
||||||
|
|
||||||
|
## [1.5.1](https://github.com/chartdb/chartdb/compare/v1.5.0...v1.5.1) (2024-12-15)
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **export:** fix SQL server field.nullable type to boolean ([#486](https://github.com/chartdb/chartdb/issues/486)) ([a151f56](https://github.com/chartdb/chartdb/commit/a151f56b5d950e0b5cc54363684ada95889024b3))
|
||||||
|
* **readme:** Update README.md - add CockroachDB ([#482](https://github.com/chartdb/chartdb/issues/482)) ([2b6b733](https://github.com/chartdb/chartdb/commit/2b6b73326155f18d6d56779c0657a3506e2d2cde))
|
||||||
|
|
||||||
|
## [1.5.0](https://github.com/chartdb/chartdb/compare/v1.4.0...v1.5.0) (2024-12-11)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **CockroachDB:** Add CockroachDB support ([#472](https://github.com/chartdb/chartdb/issues/472)) ([5409288](https://github.com/chartdb/chartdb/commit/54092883883b135f6ace51d86754b1df76603d30))
|
||||||
|
* **i18n:** translate share and dialog sections in Indonesian locale files ([#468](https://github.com/chartdb/chartdb/issues/468)) ([3574cec](https://github.com/chartdb/chartdb/commit/3574cecc7c73dcab404b82115d20e1ad0cd26b37))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **core:** fix update diagram id ([#477](https://github.com/chartdb/chartdb/issues/477)) ([348f805](https://github.com/chartdb/chartdb/commit/348f80568e0f686ee478147fdc43a5d43b5c1ebb))
|
||||||
|
* **dialogs:** fix footer position on dialogs ([#470](https://github.com/chartdb/chartdb/issues/470)) ([2309306](https://github.com/chartdb/chartdb/commit/2309306ef590783b00a2489209092107dd9a3788))
|
||||||
|
* **sql-server import:** nullable should be boolean instead of string ([#480](https://github.com/chartdb/chartdb/issues/480)) ([635fb53](https://github.com/chartdb/chartdb/commit/635fb53c9f7ebd1e5ef4d9274af041edc08f04c3))
|
||||||
|
|
||||||
|
## [1.4.0](https://github.com/chartdb/chartdb/compare/v1.3.1...v1.4.0) (2024-12-02)
|
||||||
|
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* **add templates:** add six more templates ([#452](https://github.com/chartdb/chartdb/issues/452)) ([be1b109](https://github.com/chartdb/chartdb/commit/be1b109f23e62df4cc63fa8914c2754f7809cc08))
|
||||||
|
* **add templates:** add six more templates (django-axes, laravel-activitylog, octobox, pay-rails, pixelfed, polr) ([#460](https://github.com/chartdb/chartdb/issues/460)) ([03772f6](https://github.com/chartdb/chartdb/commit/03772f6b4f99f9c4350356aa0f2a4666f4f1794d))
|
||||||
|
* **add templates:** add six more templates (reversion, screeenly, staytus, deployer, devise, talk) ([#457](https://github.com/chartdb/chartdb/issues/457)) ([ddeef3b](https://github.com/chartdb/chartdb/commit/ddeef3b134efa893e1c1e15e2f87c27157200e2d))
|
||||||
|
* **clickhouse:** add ClickHouse support ([#463](https://github.com/chartdb/chartdb/issues/463)) ([807cd22](https://github.com/chartdb/chartdb/commit/807cd22e0c739f339fa07fe1d2f043c5411ae41f))
|
||||||
|
* **i18n:** Added bangla translations ([#432](https://github.com/chartdb/chartdb/issues/432)) ([885eb71](https://github.com/chartdb/chartdb/commit/885eb719de577c2652fbed1ed287f38fcc98c148))
|
||||||
|
* **side-panel:** Add functionality of order tables by drag & drop ([#425](https://github.com/chartdb/chartdb/issues/425)) ([a0e966b](https://github.com/chartdb/chartdb/commit/a0e966b64f8070d4595d47b2fb39e8bbf427b794))
|
||||||
|
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
* **clipboard:** defensive for navigator clipboard ([#462](https://github.com/chartdb/chartdb/issues/462)) ([5fc10a7](https://github.com/chartdb/chartdb/commit/5fc10a7e649fc5877bb297b519b1b6a8b81f1323))
|
||||||
|
* **import-database:** update database type after importing into an existing generic diagra ([#456](https://github.com/chartdb/chartdb/issues/456)) ([a8fe491](https://github.com/chartdb/chartdb/commit/a8fe491c1b5a30d9f4144cefa9111dd3dfd5df1a))
|
||||||
|
* **Last Saved:** Translate the "last saved" relative date message ([#400](https://github.com/chartdb/chartdb/issues/400)) ([d45677e](https://github.com/chartdb/chartdb/commit/d45677e92d72efc6cea8f865ce46f0be6ec9961f))
|
||||||
|
* **mariadb-types:** Add uuid data type ([#459](https://github.com/chartdb/chartdb/issues/459)) ([94656ec](https://github.com/chartdb/chartdb/commit/94656ec7a5435c2da262fb3bc6a6d381d554b0c1))
|
||||||
|
* window type ([#454](https://github.com/chartdb/chartdb/issues/454)) ([9c7d03c](https://github.com/chartdb/chartdb/commit/9c7d03c285ff6f818eef3199c9b7a530d03a1fec))
|
||||||
|
|
||||||
## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26)
|
## [1.3.1](https://github.com/chartdb/chartdb/compare/v1.3.0...v1.3.1) (2024-11-26)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
10
Dockerfile
@@ -1,6 +1,9 @@
|
|||||||
FROM node:22-alpine AS builder
|
FROM node:22-alpine AS builder
|
||||||
|
|
||||||
ARG VITE_OPENAI_API_KEY
|
ARG VITE_OPENAI_API_KEY
|
||||||
|
ARG VITE_OPENAI_API_ENDPOINT
|
||||||
|
ARG VITE_LLM_MODEL_NAME
|
||||||
|
ARG VITE_HIDE_BUCKLE_DOT_DEV
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
@@ -10,9 +13,13 @@ RUN npm ci
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo "VITE_OPENAI_API_KEY=${VITE_OPENAI_API_KEY}" > .env && \
|
||||||
|
echo "VITE_OPENAI_API_ENDPOINT=${VITE_OPENAI_API_ENDPOINT}" >> .env && \
|
||||||
|
echo "VITE_LLM_MODEL_NAME=${VITE_LLM_MODEL_NAME}" >> .env && \
|
||||||
|
echo "VITE_HIDE_BUCKLE_DOT_DEV=${VITE_HIDE_BUCKLE_DOT_DEV}" >> .env
|
||||||
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Use a lightweight web server to serve the production build
|
|
||||||
FROM nginx:stable-alpine AS production
|
FROM nginx:stable-alpine AS production
|
||||||
|
|
||||||
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
COPY --from=builder /usr/src/app/dist /usr/share/nginx/html
|
||||||
@@ -20,7 +27,6 @@ COPY ./default.conf.template /etc/nginx/conf.d/default.conf.template
|
|||||||
COPY entrypoint.sh /entrypoint.sh
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
RUN chmod +x /entrypoint.sh
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
# Expose the default port for the Nginx web server
|
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
34
README.md
@@ -68,6 +68,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
|||||||
- ✅ SQL Server
|
- ✅ SQL Server
|
||||||
- ✅ MariaDB
|
- ✅ MariaDB
|
||||||
- ✅ SQLite
|
- ✅ SQLite
|
||||||
|
- ✅ CockroachDB
|
||||||
- ✅ ClickHouse
|
- ✅ ClickHouse
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
@@ -95,19 +96,44 @@ npm install
|
|||||||
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
|
VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the Docker Container
|
### Run the Docker Container
|
||||||
```bash
|
```bash
|
||||||
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
|
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 ghcr.io/chartdb/chartdb:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Build & run Docker image locally
|
#### Build and Run locally
|
||||||
```bash
|
```bash
|
||||||
docker build -t chartdb . (If you want AI capabilities, use `docker build --build-arg VITE_OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -t chartdb .`)
|
docker build -t chartdb .
|
||||||
docker run -p 8080:80 chartdb
|
docker run -e OPENAI_API_KEY=<YOUR_OPEN_AI_KEY> -p 8080:80 chartdb
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Using Custom Inference Server
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build
|
||||||
|
docker build \
|
||||||
|
--build-arg VITE_OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
|
||||||
|
--build-arg VITE_LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
|
||||||
|
-t chartdb .
|
||||||
|
|
||||||
|
# Run
|
||||||
|
docker run \
|
||||||
|
-e OPENAI_API_ENDPOINT=<YOUR_ENDPOINT> \
|
||||||
|
-e LLM_MODEL_NAME=<YOUR_MODEL_NAME> \
|
||||||
|
-p 8080:80 chartdb
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** You must configure either Option 1 (OpenAI API key) OR Option 2 (Custom endpoint and model name) for AI capabilities to work. Do not mix the two options.
|
||||||
|
|
||||||
Open your browser and navigate to `http://localhost:8080`.
|
Open your browser and navigate to `http://localhost:8080`.
|
||||||
|
|
||||||
|
Example configuration for a local vLLM server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
VITE_OPENAI_API_ENDPOINT=http://localhost:8000/v1
|
||||||
|
VITE_LLM_MODEL_NAME=Qwen/Qwen2.5-32B-Instruct-AWQ
|
||||||
|
```
|
||||||
|
|
||||||
## Try it on our website
|
## Try it on our website
|
||||||
|
|
||||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
||||||
|
|||||||
@@ -12,6 +12,9 @@
|
|||||||
},
|
},
|
||||||
"aliases": {
|
"aliases": {
|
||||||
"components": "src/components",
|
"components": "src/components",
|
||||||
"utils": "@/lib/utils"
|
"utils": "src/lib/utils",
|
||||||
|
"ui": "src/components/ui",
|
||||||
|
"lib": "src/lib",
|
||||||
|
"hooks": "src/hooks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,12 @@ server {
|
|||||||
|
|
||||||
location /config.js {
|
location /config.js {
|
||||||
default_type application/javascript;
|
default_type application/javascript;
|
||||||
return 200 "window.env = { OPENAI_API_KEY: \"$OPENAI_API_KEY\" };";
|
return 200 "window.env = {
|
||||||
|
OPENAI_API_KEY: \"$OPENAI_API_KEY\",
|
||||||
|
OPENAI_API_ENDPOINT: \"$OPENAI_API_ENDPOINT\",
|
||||||
|
LLM_MODEL_NAME: \"$LLM_MODEL_NAME\",
|
||||||
|
HIDE_BUCKLE_DOT_DEV: \"$HIDE_BUCKLE_DOT_DEV\"
|
||||||
|
};";
|
||||||
}
|
}
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
# Replace placeholders in nginx.conf
|
# Replace placeholders in nginx.conf
|
||||||
envsubst '${OPENAI_API_KEY}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
envsubst '${OPENAI_API_KEY} ${OPENAI_API_ENDPOINT} ${LLM_MODEL_NAME} ${HIDE_BUCKLE_DOT_DEV}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
# Start Nginx
|
# Start Nginx
|
||||||
nginx -g "daemon off;"
|
nginx -g "daemon off;"
|
||||||
|
|||||||
77
eslint.config.mjs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { fixupConfigRules, fixupPluginRules } from '@eslint/compat';
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh';
|
||||||
|
import cssModules from 'eslint-plugin-css-modules';
|
||||||
|
import tailwindcss from 'eslint-plugin-tailwindcss';
|
||||||
|
import jsxA11Y from 'eslint-plugin-jsx-a11y';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tsParser from '@typescript-eslint/parser';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import js from '@eslint/js';
|
||||||
|
import { FlatCompat } from '@eslint/eslintrc';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const compat = new FlatCompat({
|
||||||
|
baseDirectory: __dirname,
|
||||||
|
recommendedConfig: js.configs.recommended,
|
||||||
|
allConfig: js.configs.all,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ['**/dist', '**/.eslintrc.cjs', '**/tailwind.config.js'],
|
||||||
|
// files: ['**/*.ts', '**/*.tsx'],
|
||||||
|
},
|
||||||
|
...fixupConfigRules(
|
||||||
|
compat.extends(
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
'plugin:css-modules/recommended',
|
||||||
|
'plugin:tailwindcss/recommended',
|
||||||
|
'plugin:prettier/recommended'
|
||||||
|
)
|
||||||
|
),
|
||||||
|
{
|
||||||
|
plugins: {
|
||||||
|
'react-refresh': reactRefresh,
|
||||||
|
'css-modules': fixupPluginRules(cssModules),
|
||||||
|
tailwindcss: fixupPluginRules(tailwindcss),
|
||||||
|
'jsx-a11y': jsxA11Y,
|
||||||
|
},
|
||||||
|
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
},
|
||||||
|
|
||||||
|
parser: tsParser,
|
||||||
|
// parserOptions: {
|
||||||
|
// project: './tsconfig.json',
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
react: {
|
||||||
|
version: 'detect',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/consistent-type-imports': 'error',
|
||||||
|
|
||||||
|
'react-refresh/only-export-components': [
|
||||||
|
'warn',
|
||||||
|
{
|
||||||
|
allowConstantExport: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
'react/no-unescaped-entities': 'off',
|
||||||
|
'react/prop-types': 'off',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
5327
package-lock.json
generated
23
package.json
@@ -1,18 +1,19 @@
|
|||||||
{
|
{
|
||||||
"name": "chartdb",
|
"name": "chartdb",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.1",
|
"version": "1.9.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "npm run lint && tsc -b && vite build",
|
"build": "npm run lint && tsc -b && vite build",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:fix": "npm run lint -- --fix",
|
"lint:fix": "npm run lint -- --fix",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/openai": "^0.0.51",
|
"@ai-sdk/openai": "^0.0.51",
|
||||||
|
"@dbml/core": "^3.9.5",
|
||||||
"@dnd-kit/sortable": "^8.0.0",
|
"@dnd-kit/sortable": "^8.0.0",
|
||||||
"@monaco-editor/react": "^4.6.0",
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
@@ -28,10 +29,10 @@
|
|||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-menubar": "^1.1.1",
|
"@radix-ui/react-menubar": "^1.1.1",
|
||||||
"@radix-ui/react-popover": "^1.1.1",
|
"@radix-ui/react-popover": "^1.1.1",
|
||||||
"@radix-ui/react-scroll-area": "^1.1.0",
|
"@radix-ui/react-scroll-area": "1.2.0",
|
||||||
"@radix-ui/react-select": "^2.1.1",
|
"@radix-ui/react-select": "^2.1.1",
|
||||||
"@radix-ui/react-separator": "^1.1.0",
|
"@radix-ui/react-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.1.0",
|
"@radix-ui/react-tabs": "^1.1.0",
|
||||||
"@radix-ui/react-toast": "^1.2.1",
|
"@radix-ui/react-toast": "^1.2.1",
|
||||||
"@radix-ui/react-toggle": "^1.1.0",
|
"@radix-ui/react-toggle": "^1.1.0",
|
||||||
@@ -60,7 +61,7 @@
|
|||||||
"react-i18next": "^15.0.1",
|
"react-i18next": "^15.0.1",
|
||||||
"react-resizable-panels": "^2.0.22",
|
"react-resizable-panels": "^2.0.22",
|
||||||
"react-responsive": "^10.0.0",
|
"react-responsive": "^10.0.0",
|
||||||
"react-router-dom": "^6.26.0",
|
"react-router-dom": "^7.1.1",
|
||||||
"react-use": "^17.5.1",
|
"react-use": "^17.5.1",
|
||||||
"tailwind-merge": "^2.4.0",
|
"tailwind-merge": "^2.4.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
@@ -69,22 +70,26 @@
|
|||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/compat": "^1.2.4",
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.16.0",
|
||||||
"@types/node": "^22.1.0",
|
"@types/node": "^22.1.0",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||||
"@typescript-eslint/parser": "^7.15.0",
|
"@typescript-eslint/parser": "^8.18.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
"autoprefixer": "^10.4.20",
|
"autoprefixer": "^10.4.20",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^9.16.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-css-modules": "^2.12.0",
|
"eslint-plugin-css-modules": "^2.12.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"eslint-plugin-react": "^7.35.0",
|
"eslint-plugin-react": "^7.35.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.2",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.7",
|
"eslint-plugin-react-refresh": "^0.4.7",
|
||||||
"eslint-plugin-tailwindcss": "^3.17.4",
|
"eslint-plugin-tailwindcss": "^3.17.4",
|
||||||
|
"globals": "^15.13.0",
|
||||||
"husky": "^9.1.5",
|
"husky": "^9.1.5",
|
||||||
"postcss": "^8.4.40",
|
"postcss": "^8.4.40",
|
||||||
"prettier": "^3.3.3",
|
"prettier": "^3.3.3",
|
||||||
|
|||||||
BIN
public/buckle-animated.gif
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
public/buckle.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
src/assets/clickhouse_logo.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/clickhouse_logo_2.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/clickhouse_logo_dark.png
Normal file
|
After Width: | Height: | Size: 7.2 KiB |
BIN
src/assets/cockroachdb_logo.png
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
src/assets/cockroachdb_logo_2.png
Normal file
|
After Width: | Height: | Size: 270 KiB |
BIN
src/assets/cockroachdb_logo_dark.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 199 KiB After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/empty_state_dark.png
Normal file
|
After Width: | Height: | Size: 6.1 KiB |
BIN
src/assets/templates/cachet-db-dark.png
Normal file
|
After Width: | Height: | Size: 447 KiB |
BIN
src/assets/templates/cachet-db.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
src/assets/templates/canvas-db-dark.png
Normal file
|
After Width: | Height: | Size: 346 KiB |
BIN
src/assets/templates/canvas-db.png
Normal file
|
After Width: | Height: | Size: 379 KiB |
BIN
src/assets/templates/deployer-db-dark.png
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/templates/deployer-db.png
Normal file
|
After Width: | Height: | Size: 497 KiB |
BIN
src/assets/templates/devise-db-dark.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
src/assets/templates/devise-db.png
Normal file
|
After Width: | Height: | Size: 231 KiB |
BIN
src/assets/templates/django-axes-db-dark.png
Normal file
|
After Width: | Height: | Size: 250 KiB |
BIN
src/assets/templates/django-axes-db.png
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
src/assets/templates/doorkeeper-db-dark.png
Normal file
|
After Width: | Height: | Size: 288 KiB |
BIN
src/assets/templates/doorkeeper-db.png
Normal file
|
After Width: | Height: | Size: 319 KiB |
BIN
src/assets/templates/flipper-db-dark.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
BIN
src/assets/templates/flipper-db.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
src/assets/templates/laravel-activitylog-db-dark.png
Normal file
|
After Width: | Height: | Size: 198 KiB |
BIN
src/assets/templates/laravel-activitylog-db.png
Normal file
|
After Width: | Height: | Size: 217 KiB |
BIN
src/assets/templates/octobox-db-dark.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/templates/octobox-db.png
Normal file
|
After Width: | Height: | Size: 382 KiB |
BIN
src/assets/templates/orchid-db-dark.png
Normal file
|
After Width: | Height: | Size: 303 KiB |
BIN
src/assets/templates/orchid-db.png
Normal file
|
After Width: | Height: | Size: 340 KiB |
BIN
src/assets/templates/pay-rails-db-dark.png
Normal file
|
After Width: | Height: | Size: 352 KiB |
BIN
src/assets/templates/pay-rails-db.png
Normal file
|
After Width: | Height: | Size: 371 KiB |
BIN
src/assets/templates/pixelfed-db-dark.png
Normal file
|
After Width: | Height: | Size: 593 KiB |
BIN
src/assets/templates/pixelfed-db.png
Normal file
|
After Width: | Height: | Size: 687 KiB |
BIN
src/assets/templates/polr-db-dark.png
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
src/assets/templates/polr-db.png
Normal file
|
After Width: | Height: | Size: 278 KiB |
BIN
src/assets/templates/reversion-db-dark.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
src/assets/templates/reversion-db.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
src/assets/templates/screeenly-db-dark.png
Normal file
|
After Width: | Height: | Size: 251 KiB |
BIN
src/assets/templates/screeenly-db.png
Normal file
|
After Width: | Height: | Size: 266 KiB |
BIN
src/assets/templates/staytus-db-dark.png
Normal file
|
After Width: | Height: | Size: 424 KiB |
BIN
src/assets/templates/staytus-db.png
Normal file
|
After Width: | Height: | Size: 471 KiB |
BIN
src/assets/templates/taggit-db-dark.png
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
src/assets/templates/taggit-db.png
Normal file
|
After Width: | Height: | Size: 184 KiB |
BIN
src/assets/templates/talk-db-dark.png
Normal file
|
After Width: | Height: | Size: 229 KiB |
BIN
src/assets/templates/talk-db.png
Normal file
|
After Width: | Height: | Size: 253 KiB |
@@ -3,6 +3,7 @@ import React, { lazy, Suspense, useCallback, useEffect } from 'react';
|
|||||||
import { Spinner } from '../spinner/spinner';
|
import { Spinner } from '../spinner/spinner';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
import { useMonaco } from '@monaco-editor/react';
|
import { useMonaco } from '@monaco-editor/react';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
import { Button } from '../button/button';
|
import { Button } from '../button/button';
|
||||||
import { Copy, CopyCheck } from 'lucide-react';
|
import { Copy, CopyCheck } from 'lucide-react';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||||
@@ -11,6 +12,14 @@ import { DarkTheme } from './themes/dark';
|
|||||||
import { LightTheme } from './themes/light';
|
import { LightTheme } from './themes/light';
|
||||||
import './config.ts';
|
import './config.ts';
|
||||||
|
|
||||||
|
export const Editor = lazy(() =>
|
||||||
|
import('./code-editor').then((module) => ({
|
||||||
|
default: module.Editor,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
type EditorType = typeof Editor;
|
||||||
|
|
||||||
export interface CodeSnippetProps {
|
export interface CodeSnippetProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
code: string;
|
code: string;
|
||||||
@@ -18,14 +27,9 @@ export interface CodeSnippetProps {
|
|||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
autoScroll?: boolean;
|
autoScroll?: boolean;
|
||||||
isComplete?: boolean;
|
isComplete?: boolean;
|
||||||
|
editorProps?: React.ComponentProps<EditorType>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Editor = lazy(() =>
|
|
||||||
import('./code-editor').then((module) => ({
|
|
||||||
default: module.Editor,
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||||
({
|
({
|
||||||
className,
|
className,
|
||||||
@@ -34,10 +38,12 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language = 'sql',
|
language = 'sql',
|
||||||
autoScroll = false,
|
autoScroll = false,
|
||||||
isComplete = true,
|
isComplete = true,
|
||||||
|
editorProps,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const monaco = useMonaco();
|
const monaco = useMonaco();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
const [isCopied, setIsCopied] = React.useState(false);
|
const [isCopied, setIsCopied] = React.useState(false);
|
||||||
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
const [tooltipOpen, setTooltipOpen] = React.useState(false);
|
||||||
|
|
||||||
@@ -66,10 +72,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
}
|
}
|
||||||
}, [code, monaco, autoScroll]);
|
}, [code, monaco, autoScroll]);
|
||||||
|
|
||||||
const copyToClipboard = useCallback(() => {
|
const copyToClipboard = useCallback(async () => {
|
||||||
navigator.clipboard.writeText(code);
|
if (!navigator?.clipboard) {
|
||||||
|
toast({
|
||||||
|
title: t('copy_to_clipboard_toast.unsupported.title'),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: t(
|
||||||
|
'copy_to_clipboard_toast.unsupported.description'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(code);
|
||||||
setIsCopied(true);
|
setIsCopied(true);
|
||||||
}, [code]);
|
} catch {
|
||||||
|
setIsCopied(false);
|
||||||
|
toast({
|
||||||
|
title: t('copy_to_clipboard_toast.failed.title'),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: t(
|
||||||
|
'copy_to_clipboard_toast.failed.description'
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [code, t, toast]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -120,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
|||||||
language={language}
|
language={language}
|
||||||
loading={<Spinner />}
|
loading={<Spinner />}
|
||||||
theme={effectiveTheme}
|
theme={effectiveTheme}
|
||||||
|
{...editorProps}
|
||||||
options={{
|
options={{
|
||||||
minimap: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
scrollbar: {
|
|
||||||
vertical: 'hidden',
|
|
||||||
horizontal: 'hidden',
|
|
||||||
alwaysConsumeMouseWheel: false,
|
|
||||||
},
|
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
renderValidationDecorations: 'off',
|
renderValidationDecorations: 'off',
|
||||||
lineDecorationsWidth: 0,
|
lineDecorationsWidth: 0,
|
||||||
overviewRulerBorder: false,
|
overviewRulerBorder: false,
|
||||||
overviewRulerLanes: 0,
|
overviewRulerLanes: 0,
|
||||||
hideCursorInOverviewRuler: true,
|
hideCursorInOverviewRuler: true,
|
||||||
|
contextmenu: false,
|
||||||
|
...editorProps?.options,
|
||||||
guides: {
|
guides: {
|
||||||
indentation: false,
|
indentation: false,
|
||||||
|
...editorProps?.options?.guides,
|
||||||
|
},
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
alwaysConsumeMouseWheel: false,
|
||||||
|
...editorProps?.options?.scrollbar,
|
||||||
|
},
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
...editorProps?.options?.minimap,
|
||||||
},
|
},
|
||||||
contextmenu: false,
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{!isComplete ? (
|
{!isComplete ? (
|
||||||
|
|||||||
54
src/components/code-snippet/languages/dbml-language.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Monaco } from '@monaco-editor/react';
|
||||||
|
import { dataTypes } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||||
|
monaco.languages.register({ id: 'dbml' });
|
||||||
|
|
||||||
|
// Define themes for DBML
|
||||||
|
monaco.editor.defineTheme('dbml-dark', {
|
||||||
|
base: 'vs-dark',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'CE9178' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
|
||||||
|
{ token: 'datatype', foreground: '4EC9B0' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
monaco.editor.defineTheme('dbml-light', {
|
||||||
|
base: 'vs',
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
|
||||||
|
{ token: 'string', foreground: 'A31515' }, // Strings
|
||||||
|
{ token: 'annotation', foreground: '001080' }, // [annotations]
|
||||||
|
{ token: 'delimiter', foreground: '000000' }, // Braces {}
|
||||||
|
{ token: 'operator', foreground: '000000' }, // Operators
|
||||||
|
{ token: 'type', foreground: '267F99' }, // Data types
|
||||||
|
],
|
||||||
|
colors: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataTypesNames = dataTypes.map((dt) => dt.name);
|
||||||
|
const datatypePattern = dataTypesNames.join('|');
|
||||||
|
|
||||||
|
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||||
|
keywords: ['Table', 'Ref', 'Indexes'],
|
||||||
|
datatypes: dataTypesNames,
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/\b(Table|Ref|Indexes)\b/, 'keyword'],
|
||||||
|
[/\[.*?\]/, 'annotation'],
|
||||||
|
[/".*?"/, 'string'],
|
||||||
|
[/'.*?'/, 'string'],
|
||||||
|
[/[{}]/, 'delimiter'],
|
||||||
|
[/[<>]/, 'operator'],
|
||||||
|
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { Diagram } from '@/lib/domain/diagram';
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||||
|
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
import {
|
import {
|
||||||
databaseEditionToImageMap,
|
databaseEditionToImageMap,
|
||||||
databaseEditionToLabelMap,
|
databaseEditionToLabelMap,
|
||||||
@@ -9,39 +9,44 @@ import {
|
|||||||
databaseSecondaryLogoMap,
|
databaseSecondaryLogoMap,
|
||||||
databaseTypeToLabelMap,
|
databaseTypeToLabelMap,
|
||||||
} from '@/lib/databases';
|
} from '@/lib/databases';
|
||||||
|
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
export interface DiagramIconProps {
|
export interface DiagramIconProps
|
||||||
diagram: Diagram;
|
extends React.ComponentPropsWithoutRef<'div'> {
|
||||||
|
databaseType: DatabaseType;
|
||||||
|
databaseEdition?: DatabaseEdition;
|
||||||
|
imgClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DiagramIcon = React.forwardRef<
|
export const DiagramIcon = React.forwardRef<
|
||||||
React.ElementRef<typeof TooltipTrigger>,
|
React.ElementRef<typeof TooltipTrigger>,
|
||||||
DiagramIconProps
|
DiagramIconProps
|
||||||
>(({ diagram }, ref) =>
|
>(({ databaseType, databaseEdition, className, imgClassName }, ref) =>
|
||||||
diagram.databaseEdition ? (
|
databaseEdition ? (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="mr-1" ref={ref}>
|
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseEditionToImageMap[diagram.databaseEdition]}
|
src={databaseEditionToImageMap[databaseEdition]}
|
||||||
className="h-5 max-w-fit rounded-full"
|
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
|
||||||
alt="database"
|
alt="database"
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{databaseEditionToLabelMap[diagram.databaseEdition]}
|
{databaseEditionToLabelMap[databaseEdition]}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
) : (
|
) : (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger className="mr-2" ref={ref}>
|
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
||||||
<img
|
<img
|
||||||
src={databaseSecondaryLogoMap[diagram.databaseType]}
|
src={databaseSecondaryLogoMap[databaseType]}
|
||||||
className="h-5 max-w-fit"
|
className={cn('h-5 max-w-fit', imgClassName)}
|
||||||
alt="database"
|
alt="database"
|
||||||
/>
|
/>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
{databaseTypeToLabelMap[diagram.databaseType]}
|
{databaseTypeToLabelMap[databaseType]}
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -117,7 +117,10 @@ const DialogInternalContent = React.forwardRef<
|
|||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<ScrollArea
|
<ScrollArea
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn('flex max-h-screen flex-col overflow-y-auto', className)}
|
className={cn(
|
||||||
|
'flex flex-1 max-h-screen flex-col overflow-y-auto',
|
||||||
|
className
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -1,17 +1,36 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import EmptyStateImage from '@/assets/empty_state.png';
|
import EmptyStateImage from '@/assets/empty_state.png';
|
||||||
|
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
|
||||||
export interface EmptyStateProps {
|
export interface EmptyStateProps {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
imageClassName?: string;
|
||||||
|
titleClassName?: string;
|
||||||
|
descriptionClassName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EmptyState = forwardRef<
|
export const EmptyState = forwardRef<
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
||||||
>(({ title, description, className }, ref) => (
|
>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
className,
|
||||||
|
titleClassName,
|
||||||
|
descriptionClassName,
|
||||||
|
imageClassName,
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -19,12 +38,29 @@ export const EmptyState = forwardRef<
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img src={EmptyStateImage} alt="Empty state" className="w-32" />
|
<img
|
||||||
<Label className="text-base">{title}</Label>
|
src={
|
||||||
<Label className="text-sm font-normal text-muted-foreground">
|
effectiveTheme === 'dark'
|
||||||
|
? EmptyStateImageDark
|
||||||
|
: EmptyStateImage
|
||||||
|
}
|
||||||
|
alt="Empty state"
|
||||||
|
className={cn('mb-2 w-20', imageClassName)}
|
||||||
|
/>
|
||||||
|
<Label className={cn('text-base', titleClassName)}>
|
||||||
|
{title}
|
||||||
|
</Label>
|
||||||
|
<Label
|
||||||
|
className={cn(
|
||||||
|
'text-sm font-normal text-muted-foreground',
|
||||||
|
descriptionClassName
|
||||||
|
)}
|
||||||
|
>
|
||||||
{description}
|
{description}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
));
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
EmptyState.displayName = 'EmptyState';
|
EmptyState.displayName = 'EmptyState';
|
||||||
|
|||||||
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
|||||||
value: string;
|
value: string;
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
regex?: string;
|
||||||
|
extractRegex?: RegExp;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectBoxProps {
|
export interface SelectBoxProps {
|
||||||
options: SelectBoxOption[];
|
options: SelectBoxOption[];
|
||||||
value?: string[] | string;
|
value?: string[] | string;
|
||||||
onChange?: (values: string[] | string) => void;
|
valueSuffix?: string;
|
||||||
|
optionSuffix?: (option: SelectBoxOption) => string;
|
||||||
|
onChange?: (
|
||||||
|
values: string[] | string,
|
||||||
|
regexMatches?: string[] | string
|
||||||
|
) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
inputPlaceholder?: string;
|
inputPlaceholder?: string;
|
||||||
emptyPlaceholder?: string;
|
emptyPlaceholder?: string;
|
||||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
className,
|
className,
|
||||||
options,
|
options,
|
||||||
value,
|
value,
|
||||||
|
valueSuffix,
|
||||||
onChange,
|
onChange,
|
||||||
multiple,
|
multiple,
|
||||||
oneLine,
|
oneLine,
|
||||||
selectAll,
|
selectAll,
|
||||||
|
optionSuffix,
|
||||||
deselectAll,
|
deselectAll,
|
||||||
clearText,
|
clearText,
|
||||||
showClear,
|
showClear,
|
||||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = React.useCallback(
|
const handleSelect = React.useCallback(
|
||||||
(selectedValue: string) => {
|
(selectedValue: string, regexMatches?: string[]) => {
|
||||||
if (multiple) {
|
if (multiple) {
|
||||||
const newValue =
|
const newValue =
|
||||||
value?.includes(selectedValue) && Array.isArray(value)
|
value?.includes(selectedValue) && Array.isArray(value)
|
||||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
: [...(value ?? []), selectedValue];
|
: [...(value ?? []), selectedValue];
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
} else {
|
} else {
|
||||||
onChange?.(selectedValue);
|
onChange?.(selectedValue, regexMatches);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
(opt) => opt.value === value
|
(opt) => opt.value === value
|
||||||
)?.label
|
)?.label
|
||||||
}
|
}
|
||||||
|
{valueSuffix ? valueSuffix : ''}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
align="center"
|
align="center"
|
||||||
>
|
>
|
||||||
<Command
|
<Command
|
||||||
filter={(value, search) =>
|
filter={(value, search, keywords) => {
|
||||||
value.toLowerCase().includes(search.toLowerCase())
|
if (
|
||||||
? 1
|
keywords?.length &&
|
||||||
: 0
|
keywords.some((keyword) =>
|
||||||
|
new RegExp(keyword).test(search)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(search.toLowerCase())
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<CommandInput
|
<CommandInput
|
||||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
const isSelected =
|
const isSelected =
|
||||||
Array.isArray(value) &&
|
Array.isArray(value) &&
|
||||||
value.includes(option.value);
|
value.includes(option.value);
|
||||||
|
|
||||||
|
const isRegexMatch =
|
||||||
|
option.regex &&
|
||||||
|
new RegExp(option.regex)?.test(
|
||||||
|
searchTerm
|
||||||
|
);
|
||||||
|
|
||||||
|
const matches = option.extractRegex
|
||||||
|
? searchTerm.match(
|
||||||
|
option.extractRegex
|
||||||
|
)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
key={option.value}
|
key={option.value}
|
||||||
|
keywords={
|
||||||
|
option.regex
|
||||||
|
? [option.regex]
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
// value={option.value}
|
// value={option.value}
|
||||||
onSelect={() =>
|
onSelect={() =>
|
||||||
handleSelect(
|
handleSelect(
|
||||||
option.value
|
option.value,
|
||||||
|
matches?.map(
|
||||||
|
(match) =>
|
||||||
|
match.toString()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -327,7 +370,15 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
)}
|
)}
|
||||||
<div className="flex items-center truncate">
|
<div className="flex items-center truncate">
|
||||||
<span>
|
<span>
|
||||||
{option.label}
|
{isRegexMatch
|
||||||
|
? searchTerm
|
||||||
|
: option.label}
|
||||||
|
{!isRegexMatch &&
|
||||||
|
optionSuffix
|
||||||
|
? optionSuffix(
|
||||||
|
option
|
||||||
|
)
|
||||||
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
{option.description && (
|
{option.description && (
|
||||||
<span className="ml-1 text-xs text-muted-foreground">
|
<span className="ml-1 text-xs text-muted-foreground">
|
||||||
@@ -337,9 +388,10 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!multiple &&
|
{((!multiple &&
|
||||||
option.value ===
|
option.value ===
|
||||||
value && (
|
value) ||
|
||||||
|
isRegexMatch) && (
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className={cn(
|
className={cn(
|
||||||
'ml-auto',
|
'ml-auto',
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type ToasterToast = ToastProps & {
|
|||||||
layout?: 'row' | 'column';
|
layout?: 'row' | 'column';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const actionTypes = {
|
const actionTypes = {
|
||||||
ADD_TOAST: 'ADD_TOAST',
|
ADD_TOAST: 'ADD_TOAST',
|
||||||
UPDATE_TOAST: 'UPDATE_TOAST',
|
UPDATE_TOAST: 'UPDATE_TOAST',
|
||||||
|
|||||||
15
src/context/alert-context/alert-context.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
|
||||||
|
export interface AlertContext {
|
||||||
|
showAlert: (params: BaseAlertDialogProps) => void;
|
||||||
|
closeAlert: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const alertContext = createContext<AlertContext>({
|
||||||
|
closeAlert: emptyFn,
|
||||||
|
showAlert: emptyFn,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useAlert = () => useContext(alertContext);
|
||||||
36
src/context/alert-context/alert-provider.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import React, { useCallback, useState } from 'react';
|
||||||
|
import type { AlertContext } from './alert-context';
|
||||||
|
import { alertContext } from './alert-context';
|
||||||
|
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||||
|
|
||||||
|
export const AlertProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [showAlert, setShowAlert] = useState(false);
|
||||||
|
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||||
|
title: '',
|
||||||
|
});
|
||||||
|
const showAlertHandler: AlertContext['showAlert'] = useCallback(
|
||||||
|
(params) => {
|
||||||
|
setAlertParams(params);
|
||||||
|
setShowAlert(true);
|
||||||
|
},
|
||||||
|
[setShowAlert, setAlertParams]
|
||||||
|
);
|
||||||
|
const closeAlertHandler = useCallback(() => {
|
||||||
|
setShowAlert(false);
|
||||||
|
}, [setShowAlert]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<alertContext.Provider
|
||||||
|
value={{
|
||||||
|
showAlert: showAlertHandler,
|
||||||
|
closeAlert: closeAlertHandler,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
||||||
|
</alertContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
22
src/context/canvas-context/canvas-context.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { emptyFn } from '@/lib/utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
export interface CanvasContext {
|
||||||
|
reorderTables: (options?: { updateHistory?: boolean }) => void;
|
||||||
|
fitView: (options?: {
|
||||||
|
duration?: number;
|
||||||
|
padding?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
}) => void;
|
||||||
|
setOverlapGraph: (graph: Graph<string>) => void;
|
||||||
|
overlapGraph: Graph<string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const canvasContext = createContext<CanvasContext>({
|
||||||
|
reorderTables: emptyFn,
|
||||||
|
fitView: emptyFn,
|
||||||
|
setOverlapGraph: emptyFn,
|
||||||
|
overlapGraph: createGraph(),
|
||||||
|
});
|
||||||
85
src/context/canvas-context/canvas-provider.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import React, { type ReactNode, useCallback, useState } from 'react';
|
||||||
|
import { canvasContext } from './canvas-context';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import {
|
||||||
|
adjustTablePositions,
|
||||||
|
shouldShowTablesBySchemaFilter,
|
||||||
|
} from '@/lib/domain/db-table';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
import { findOverlappingTables } from '@/pages/editor-page/canvas/canvas-utils';
|
||||||
|
import type { Graph } from '@/lib/graph';
|
||||||
|
import { createGraph } from '@/lib/graph';
|
||||||
|
|
||||||
|
interface CanvasProviderProps {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CanvasProvider = ({ children }: CanvasProviderProps) => {
|
||||||
|
const { tables, relationships, updateTablesState, filteredSchemas } =
|
||||||
|
useChartDB();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
const [overlapGraph, setOverlapGraph] =
|
||||||
|
useState<Graph<string>>(createGraph());
|
||||||
|
|
||||||
|
const reorderTables = useCallback(
|
||||||
|
(
|
||||||
|
options: { updateHistory?: boolean } = {
|
||||||
|
updateHistory: true,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const newTables = adjustTablePositions({
|
||||||
|
relationships,
|
||||||
|
tables: tables.filter((table) =>
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||||
|
),
|
||||||
|
mode: 'all', // Use 'all' mode for manual reordering
|
||||||
|
});
|
||||||
|
|
||||||
|
const updatedOverlapGraph = findOverlappingTables({
|
||||||
|
tables: newTables,
|
||||||
|
});
|
||||||
|
|
||||||
|
updateTablesState(
|
||||||
|
(currentTables) =>
|
||||||
|
currentTables.map((table) => {
|
||||||
|
const newTable = newTables.find(
|
||||||
|
(t) => t.id === table.id
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
id: table.id,
|
||||||
|
x: newTable?.x ?? table.x,
|
||||||
|
y: newTable?.y ?? table.y,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
updateHistory: options.updateHistory ?? true,
|
||||||
|
forceOverride: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setOverlapGraph(updatedOverlapGraph);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.2,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
[filteredSchemas, relationships, tables, updateTablesState, fitView]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvasContext.Provider
|
||||||
|
value={{
|
||||||
|
reorderTables,
|
||||||
|
fitView,
|
||||||
|
setOverlapGraph,
|
||||||
|
overlapGraph,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</canvasContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -84,6 +84,7 @@ export interface ChartDBContext {
|
|||||||
options?: { updateHistory: boolean }
|
options?: { updateHistory: boolean }
|
||||||
) => Promise<void>;
|
) => Promise<void>;
|
||||||
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
||||||
|
loadDiagramFromData: (diagram: Diagram) => void;
|
||||||
updateDiagramUpdatedAt: () => Promise<void>;
|
updateDiagramUpdatedAt: () => Promise<void>;
|
||||||
clearDiagramData: () => Promise<void>;
|
clearDiagramData: () => Promise<void>;
|
||||||
deleteDiagram: () => Promise<void>;
|
deleteDiagram: () => Promise<void>;
|
||||||
@@ -246,6 +247,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
|||||||
updateDiagramName: emptyFn,
|
updateDiagramName: emptyFn,
|
||||||
updateDiagramUpdatedAt: emptyFn,
|
updateDiagramUpdatedAt: emptyFn,
|
||||||
loadDiagram: emptyFn,
|
loadDiagram: emptyFn,
|
||||||
|
loadDiagramFromData: emptyFn,
|
||||||
clearDiagramData: emptyFn,
|
clearDiagramData: emptyFn,
|
||||||
deleteDiagram: emptyFn,
|
deleteDiagram: emptyFn,
|
||||||
|
|
||||||
|
|||||||
@@ -22,14 +22,18 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
|||||||
import { useEventEmitter } from 'ahooks';
|
import { useEventEmitter } from 'ahooks';
|
||||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||||
import { storageInitialValue } from '../storage-context/storage-context';
|
import { storageInitialValue } from '../storage-context/storage-context';
|
||||||
|
import { useDiff } from '../diff-context/use-diff';
|
||||||
|
import type { DiffCalculatedEvent } from '../diff-context/diff-context';
|
||||||
|
|
||||||
export interface ChartDBProviderProps {
|
export interface ChartDBProviderProps {
|
||||||
diagram?: Diagram;
|
diagram?: Diagram;
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartDBProvider: React.FC<
|
export const ChartDBProvider: React.FC<
|
||||||
React.PropsWithChildren<ChartDBProviderProps>
|
React.PropsWithChildren<ChartDBProviderProps>
|
||||||
> = ({ children, diagram, readonly }) => {
|
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||||
|
const { hasDiff } = useDiff();
|
||||||
let db = useStorage();
|
let db = useStorage();
|
||||||
const events = useEventEmitter<ChartDBEvent>();
|
const events = useEventEmitter<ChartDBEvent>();
|
||||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||||
@@ -52,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
|||||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||||
diagram?.dependencies ?? []
|
diagram?.dependencies ?? []
|
||||||
);
|
);
|
||||||
|
const { events: diffEvents } = useDiff();
|
||||||
|
|
||||||
|
const diffCalculatedHandler = useCallback((event: DiffCalculatedEvent) => {
|
||||||
|
const { tablesAdded, fieldsAdded, relationshipsAdded } = event.data;
|
||||||
|
setTables((tables) =>
|
||||||
|
[...tables, ...(tablesAdded ?? [])].map((table) => {
|
||||||
|
const fields = fieldsAdded.get(table.id);
|
||||||
|
return fields
|
||||||
|
? { ...table, fields: [...table.fields, ...fields] }
|
||||||
|
: table;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
setRelationships((relationships) => [
|
||||||
|
...relationships,
|
||||||
|
...(relationshipsAdded ?? []),
|
||||||
|
]);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
diffEvents.useSubscription(diffCalculatedHandler);
|
||||||
|
|
||||||
const defaultSchemaName = defaultSchemas[databaseType];
|
const defaultSchemaName = defaultSchemas[databaseType];
|
||||||
|
|
||||||
|
const readonly = useMemo(
|
||||||
|
() => readonlyProp ?? hasDiff ?? false,
|
||||||
|
[readonlyProp, hasDiff]
|
||||||
|
);
|
||||||
|
|
||||||
if (readonly) {
|
if (readonly) {
|
||||||
db = storageInitialValue;
|
db = storageInitialValue;
|
||||||
}
|
}
|
||||||
@@ -310,6 +338,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
color: randomColor(),
|
color: randomColor(),
|
||||||
createdAt: Date.now(),
|
createdAt: Date.now(),
|
||||||
isView: false,
|
isView: false,
|
||||||
|
order: tables.length,
|
||||||
...attributes,
|
...attributes,
|
||||||
};
|
};
|
||||||
await addTable(table);
|
await addTable(table);
|
||||||
@@ -1334,15 +1363,9 @@ export const ChartDBProvider: React.FC<
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||||
async (diagramId: string) => {
|
useCallback(
|
||||||
const diagram = await db.getDiagram(diagramId, {
|
async (diagram) => {
|
||||||
includeRelationships: true,
|
|
||||||
includeTables: true,
|
|
||||||
includeDependencies: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (diagram) {
|
|
||||||
setDiagramId(diagram.id);
|
setDiagramId(diagram.id);
|
||||||
setDiagramName(diagram.name);
|
setDiagramName(diagram.name);
|
||||||
setDatabaseType(diagram.databaseType);
|
setDatabaseType(diagram.databaseType);
|
||||||
@@ -1354,12 +1377,8 @@ export const ChartDBProvider: React.FC<
|
|||||||
setDiagramUpdatedAt(diagram.updatedAt);
|
setDiagramUpdatedAt(diagram.updatedAt);
|
||||||
|
|
||||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||||
}
|
|
||||||
|
|
||||||
return diagram;
|
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
db,
|
|
||||||
setDiagramId,
|
setDiagramId,
|
||||||
setDiagramName,
|
setDiagramName,
|
||||||
setDatabaseType,
|
setDatabaseType,
|
||||||
@@ -1373,6 +1392,23 @@ export const ChartDBProvider: React.FC<
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadDiagram: ChartDBContext['loadDiagram'] = useCallback(
|
||||||
|
async (diagramId: string) => {
|
||||||
|
const diagram = await db.getDiagram(diagramId, {
|
||||||
|
includeRelationships: true,
|
||||||
|
includeTables: true,
|
||||||
|
includeDependencies: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diagram) {
|
||||||
|
loadDiagramFromData(diagram);
|
||||||
|
}
|
||||||
|
|
||||||
|
return diagram;
|
||||||
|
},
|
||||||
|
[db, loadDiagramFromData]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<chartDBContext.Provider
|
<chartDBContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -1391,6 +1427,7 @@ export const ChartDBProvider: React.FC<
|
|||||||
updateDiagramId,
|
updateDiagramId,
|
||||||
updateDiagramName,
|
updateDiagramName,
|
||||||
loadDiagram,
|
loadDiagram,
|
||||||
|
loadDiagramFromData,
|
||||||
updateDatabaseType,
|
updateDatabaseType,
|
||||||
updateDatabaseEdition,
|
updateDatabaseEdition,
|
||||||
clearDiagramData,
|
clearDiagramData,
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
import { emptyFn } from '@/lib/utils';
|
import { emptyFn } from '@/lib/utils';
|
||||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
|
||||||
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
|
import type { TableSchemaDialogProps } from '@/dialogs/table-schema-dialog/table-schema-dialog';
|
||||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import type { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import type { ImportDiagramDialogProps } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
|
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
|
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
|
|
||||||
export interface DialogContext {
|
export interface DialogContext {
|
||||||
// Create diagram dialog
|
// Create diagram dialog
|
||||||
@@ -14,19 +16,19 @@ export interface DialogContext {
|
|||||||
closeCreateDiagramDialog: () => void;
|
closeCreateDiagramDialog: () => void;
|
||||||
|
|
||||||
// Open diagram dialog
|
// Open diagram dialog
|
||||||
openOpenDiagramDialog: () => void;
|
openOpenDiagramDialog: (
|
||||||
|
params?: Omit<OpenDiagramDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
closeOpenDiagramDialog: () => void;
|
closeOpenDiagramDialog: () => void;
|
||||||
|
|
||||||
// Export SQL dialog
|
// Export SQL dialog
|
||||||
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
||||||
closeExportSQLDialog: () => void;
|
closeExportSQLDialog: () => void;
|
||||||
|
|
||||||
// Alert dialog
|
|
||||||
showAlert: (params: BaseAlertDialogProps) => void;
|
|
||||||
closeAlert: () => void;
|
|
||||||
|
|
||||||
// Create relationship dialog
|
// Create relationship dialog
|
||||||
openCreateRelationshipDialog: () => void;
|
openCreateRelationshipDialog: (
|
||||||
|
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
closeCreateRelationshipDialog: () => void;
|
closeCreateRelationshipDialog: () => void;
|
||||||
|
|
||||||
// Import database dialog
|
// Import database dialog
|
||||||
@@ -62,6 +64,12 @@ export interface DialogContext {
|
|||||||
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
||||||
) => void;
|
) => void;
|
||||||
closeImportDiagramDialog: () => void;
|
closeImportDiagramDialog: () => void;
|
||||||
|
|
||||||
|
// Import DBML dialog
|
||||||
|
openImportDBMLDialog: (
|
||||||
|
params?: Omit<ImportDBMLDialogProps, 'dialog'>
|
||||||
|
) => void;
|
||||||
|
closeImportDBMLDialog: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const dialogContext = createContext<DialogContext>({
|
export const dialogContext = createContext<DialogContext>({
|
||||||
@@ -71,8 +79,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeOpenDiagramDialog: emptyFn,
|
closeOpenDiagramDialog: emptyFn,
|
||||||
openExportSQLDialog: emptyFn,
|
openExportSQLDialog: emptyFn,
|
||||||
closeExportSQLDialog: emptyFn,
|
closeExportSQLDialog: emptyFn,
|
||||||
closeAlert: emptyFn,
|
|
||||||
showAlert: emptyFn,
|
|
||||||
closeCreateRelationshipDialog: emptyFn,
|
closeCreateRelationshipDialog: emptyFn,
|
||||||
openCreateRelationshipDialog: emptyFn,
|
openCreateRelationshipDialog: emptyFn,
|
||||||
openImportDatabaseDialog: emptyFn,
|
openImportDatabaseDialog: emptyFn,
|
||||||
@@ -87,4 +93,6 @@ export const dialogContext = createContext<DialogContext>({
|
|||||||
closeExportDiagramDialog: emptyFn,
|
closeExportDiagramDialog: emptyFn,
|
||||||
openImportDiagramDialog: emptyFn,
|
openImportDiagramDialog: emptyFn,
|
||||||
closeImportDiagramDialog: emptyFn,
|
closeImportDiagramDialog: emptyFn,
|
||||||
|
openImportDBMLDialog: emptyFn,
|
||||||
|
closeImportDBMLDialog: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import React, { useCallback, useState } from 'react';
|
|||||||
import type { DialogContext } from './dialog-context';
|
import type { DialogContext } from './dialog-context';
|
||||||
import { dialogContext } from './dialog-context';
|
import { dialogContext } from './dialog-context';
|
||||||
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
import { CreateDiagramDialog } from '@/dialogs/create-diagram-dialog/create-diagram-dialog';
|
||||||
|
import type { OpenDiagramDialogProps } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
import { OpenDiagramDialog } from '@/dialogs/open-diagram-dialog/open-diagram-dialog';
|
||||||
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import type { ExportSQLDialogProps } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
import { ExportSQLDialog } from '@/dialogs/export-sql-dialog/export-sql-dialog';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
import type { CreateRelationshipDialogProps } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
|
||||||
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
import { CreateRelationshipDialog } from '@/dialogs/create-relationship-dialog/create-relationship-dialog';
|
||||||
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import type { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
import { ImportDatabaseDialog } from '@/dialogs/import-database-dialog/import-database-dialog';
|
||||||
@@ -19,15 +19,39 @@ import type { ExportImageDialogProps } from '@/dialogs/export-image-dialog/expor
|
|||||||
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
import { ExportImageDialog } from '@/dialogs/export-image-dialog/export-image-dialog';
|
||||||
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
import { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-diagram-dialog';
|
||||||
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
import { ImportDiagramDialog } from '@/dialogs/import-diagram-dialog/import-diagram-dialog';
|
||||||
|
import type { ImportDBMLDialogProps } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
import { ImportDBMLDialog } from '@/dialogs/import-dbml-dialog/import-dbml-dialog';
|
||||||
|
|
||||||
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
const [openNewDiagramDialog, setOpenNewDiagramDialog] = useState(false);
|
||||||
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
const [openOpenDiagramDialog, setOpenOpenDiagramDialog] = useState(false);
|
||||||
|
const [openDiagramDialogParams, setOpenDiagramDialogParams] =
|
||||||
|
useState<Omit<OpenDiagramDialogProps, 'dialog'>>();
|
||||||
|
|
||||||
|
const openOpenDiagramDialogHandler: DialogContext['openOpenDiagramDialog'] =
|
||||||
|
useCallback(
|
||||||
|
(props) => {
|
||||||
|
setOpenDiagramDialogParams(props);
|
||||||
|
setOpenOpenDiagramDialog(true);
|
||||||
|
},
|
||||||
|
[setOpenOpenDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
const [openCreateRelationshipDialog, setOpenCreateRelationshipDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [createRelationshipDialogParams, setCreateRelationshipDialogParams] =
|
||||||
|
useState<Omit<CreateRelationshipDialogProps, 'dialog'>>();
|
||||||
|
const openCreateRelationshipDialogHandler: DialogContext['openCreateRelationshipDialog'] =
|
||||||
|
useCallback(
|
||||||
|
(params) => {
|
||||||
|
setCreateRelationshipDialogParams(params);
|
||||||
|
setOpenCreateRelationshipDialog(true);
|
||||||
|
},
|
||||||
|
[setOpenCreateRelationshipDialog]
|
||||||
|
);
|
||||||
|
|
||||||
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
const [openStarUsDialog, setOpenStarUsDialog] = useState(false);
|
||||||
|
|
||||||
// Export image dialog
|
// Export image dialog
|
||||||
@@ -88,7 +112,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
[setOpenTableSchemaDialog]
|
[setOpenTableSchemaDialog]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Export image dialog
|
// Export diagram dialog
|
||||||
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
@@ -96,35 +120,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
// Alert dialog
|
// Import DBML dialog
|
||||||
const [showAlert, setShowAlert] = useState(false);
|
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
const [importDBMLDialogParams, setImportDBMLDialogParams] =
|
||||||
title: '',
|
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
|
||||||
});
|
|
||||||
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
|
||||||
(params) => {
|
|
||||||
setAlertParams(params);
|
|
||||||
setShowAlert(true);
|
|
||||||
},
|
|
||||||
[setShowAlert, setAlertParams]
|
|
||||||
);
|
|
||||||
const closeAlertHandler = useCallback(() => {
|
|
||||||
setShowAlert(false);
|
|
||||||
}, [setShowAlert]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<dialogContext.Provider
|
<dialogContext.Provider
|
||||||
value={{
|
value={{
|
||||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||||
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
|
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||||
openExportSQLDialog: openExportSQLDialogHandler,
|
openExportSQLDialog: openExportSQLDialogHandler,
|
||||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||||
showAlert: showAlertHandler,
|
openCreateRelationshipDialog:
|
||||||
closeAlert: closeAlertHandler,
|
openCreateRelationshipDialogHandler,
|
||||||
openCreateRelationshipDialog: () =>
|
|
||||||
setOpenCreateRelationshipDialog(true),
|
|
||||||
closeCreateRelationshipDialog: () =>
|
closeCreateRelationshipDialog: () =>
|
||||||
setOpenCreateRelationshipDialog(false),
|
setOpenCreateRelationshipDialog(false),
|
||||||
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
||||||
@@ -142,18 +153,26 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||||
closeImportDiagramDialog: () =>
|
closeImportDiagramDialog: () =>
|
||||||
setOpenImportDiagramDialog(false),
|
setOpenImportDiagramDialog(false),
|
||||||
|
openImportDBMLDialog: (params) => {
|
||||||
|
setImportDBMLDialogParams(params);
|
||||||
|
setOpenImportDBMLDialog(true);
|
||||||
|
},
|
||||||
|
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||||
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
|
<OpenDiagramDialog
|
||||||
|
dialog={{ open: openOpenDiagramDialog }}
|
||||||
|
{...openDiagramDialogParams}
|
||||||
|
/>
|
||||||
<ExportSQLDialog
|
<ExportSQLDialog
|
||||||
dialog={{ open: openExportSQLDialog }}
|
dialog={{ open: openExportSQLDialog }}
|
||||||
{...exportSQLDialogParams}
|
{...exportSQLDialogParams}
|
||||||
/>
|
/>
|
||||||
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
|
||||||
<CreateRelationshipDialog
|
<CreateRelationshipDialog
|
||||||
dialog={{ open: openCreateRelationshipDialog }}
|
dialog={{ open: openCreateRelationshipDialog }}
|
||||||
|
{...createRelationshipDialogParams}
|
||||||
/>
|
/>
|
||||||
<ImportDatabaseDialog
|
<ImportDatabaseDialog
|
||||||
dialog={{ open: openImportDatabaseDialog }}
|
dialog={{ open: openImportDatabaseDialog }}
|
||||||
@@ -170,6 +189,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
/>
|
/>
|
||||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||||
|
<ImportDBMLDialog
|
||||||
|
dialog={{ open: openImportDBMLDialog }}
|
||||||
|
{...importDBMLDialogParams}
|
||||||
|
/>
|
||||||
</dialogContext.Provider>
|
</dialogContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
433
src/context/diff-context/diff-check/diff-check.ts
Normal file
@@ -0,0 +1,433 @@
|
|||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type {
|
||||||
|
ChartDBDiff,
|
||||||
|
DiffMap,
|
||||||
|
DiffObject,
|
||||||
|
FieldDiffAttribute,
|
||||||
|
} from '../types';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DBIndex } from '@/lib/domain/db-index';
|
||||||
|
|
||||||
|
export function getDiffMapKey({
|
||||||
|
diffObject,
|
||||||
|
objectId,
|
||||||
|
attribute,
|
||||||
|
}: {
|
||||||
|
diffObject: DiffObject;
|
||||||
|
objectId: string;
|
||||||
|
attribute?: string;
|
||||||
|
}): string {
|
||||||
|
return attribute
|
||||||
|
? `${diffObject}-${attribute}-${objectId}`
|
||||||
|
: `${diffObject}-${objectId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateDiff({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}): {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
} {
|
||||||
|
const newDiffs = new Map<string, ChartDBDiff>();
|
||||||
|
const changedTables = new Map<string, boolean>();
|
||||||
|
const changedFields = new Map<string, boolean>();
|
||||||
|
|
||||||
|
// Compare tables
|
||||||
|
compareTables({ diagram, newDiagram, diffMap: newDiffs, changedTables });
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare relationships
|
||||||
|
compareRelationships({ diagram, newDiagram, diffMap: newDiffs });
|
||||||
|
|
||||||
|
return { diffMap: newDiffs, changedTables, changedFields };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare tables between diagrams
|
||||||
|
function compareTables({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// Check for added tables
|
||||||
|
for (const newTable of newTables) {
|
||||||
|
if (!oldTables.find((t) => t.id === newTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: newTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'added',
|
||||||
|
tableId: newTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(newTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed tables
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
if (!newTables.find((t) => t.id === oldTable.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({ diffObject: 'table', objectId: oldTable.id }),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'removed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for table name and comments changes
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
if (oldTable.name !== newTable.name) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'name',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'name',
|
||||||
|
newValue: newTable.name,
|
||||||
|
oldValue: oldTable.name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldTable.comments !== newTable.comments) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: oldTable.id,
|
||||||
|
attribute: 'comments',
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'table',
|
||||||
|
type: 'changed',
|
||||||
|
tableId: oldTable.id,
|
||||||
|
attributes: 'comments',
|
||||||
|
newValue: newTable.comments,
|
||||||
|
oldValue: oldTable.comments,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(oldTable.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields and indexes for matching tables
|
||||||
|
function compareTableContents({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const oldTables = diagram.tables || [];
|
||||||
|
const newTables = newDiagram.tables || [];
|
||||||
|
|
||||||
|
// For each table that exists in both diagrams
|
||||||
|
for (const oldTable of oldTables) {
|
||||||
|
const newTable = newTables.find((t) => t.id === oldTable.id);
|
||||||
|
if (!newTable) continue;
|
||||||
|
|
||||||
|
// Compare fields
|
||||||
|
compareFields({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldFields: oldTable.fields,
|
||||||
|
newFields: newTable.fields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Compare indexes
|
||||||
|
compareIndexes({
|
||||||
|
tableId: oldTable.id,
|
||||||
|
oldIndexes: oldTable.indexes,
|
||||||
|
newIndexes: newTable.indexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare fields between tables
|
||||||
|
function compareFields({
|
||||||
|
tableId,
|
||||||
|
oldFields,
|
||||||
|
newFields,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldFields: DBField[];
|
||||||
|
newFields: DBField[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added fields
|
||||||
|
for (const newField of newFields) {
|
||||||
|
if (!oldFields.find((f) => f.id === newField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: newField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'added',
|
||||||
|
fieldId: newField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(newField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed fields
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
if (!newFields.find((f) => f.id === oldField.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'removed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for field changes
|
||||||
|
for (const oldField of oldFields) {
|
||||||
|
const newField = newFields.find((f) => f.id === oldField.id);
|
||||||
|
if (!newField) continue;
|
||||||
|
|
||||||
|
// Compare basic field properties
|
||||||
|
compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare field properties
|
||||||
|
function compareFieldProperties({
|
||||||
|
tableId,
|
||||||
|
oldField,
|
||||||
|
newField,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
changedFields,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldField: DBField;
|
||||||
|
newField: DBField;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
changedFields: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
const changedAttributes: FieldDiffAttribute[] = [];
|
||||||
|
|
||||||
|
if (oldField.name !== newField.name) {
|
||||||
|
changedAttributes.push('name');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.type.id !== newField.type.id) {
|
||||||
|
changedAttributes.push('type');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.primaryKey !== newField.primaryKey) {
|
||||||
|
changedAttributes.push('primaryKey');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.unique !== newField.unique) {
|
||||||
|
changedAttributes.push('unique');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.nullable !== newField.nullable) {
|
||||||
|
changedAttributes.push('nullable');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldField.comments !== newField.comments) {
|
||||||
|
changedAttributes.push('comments');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changedAttributes.length > 0) {
|
||||||
|
for (const attribute of changedAttributes) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: oldField.id,
|
||||||
|
attribute: attribute,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'field',
|
||||||
|
type: 'changed',
|
||||||
|
fieldId: oldField.id,
|
||||||
|
tableId,
|
||||||
|
attributes: attribute,
|
||||||
|
oldValue: oldField[attribute],
|
||||||
|
newValue: newField[attribute],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
changedFields.set(oldField.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare indexes between tables
|
||||||
|
function compareIndexes({
|
||||||
|
tableId,
|
||||||
|
oldIndexes,
|
||||||
|
newIndexes,
|
||||||
|
diffMap,
|
||||||
|
changedTables,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
oldIndexes: DBIndex[];
|
||||||
|
newIndexes: DBIndex[];
|
||||||
|
diffMap: DiffMap;
|
||||||
|
changedTables: Map<string, boolean>;
|
||||||
|
}) {
|
||||||
|
// Check for added indexes
|
||||||
|
for (const newIndex of newIndexes) {
|
||||||
|
if (!oldIndexes.find((i) => i.id === newIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: newIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'added',
|
||||||
|
indexId: newIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed indexes
|
||||||
|
for (const oldIndex of oldIndexes) {
|
||||||
|
if (!newIndexes.find((i) => i.id === oldIndex.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'index',
|
||||||
|
objectId: oldIndex.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'index',
|
||||||
|
type: 'removed',
|
||||||
|
indexId: oldIndex.id,
|
||||||
|
tableId,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
changedTables.set(tableId, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare relationships between diagrams
|
||||||
|
function compareRelationships({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
}) {
|
||||||
|
const oldRelationships = diagram.relationships || [];
|
||||||
|
const newRelationships = newDiagram.relationships || [];
|
||||||
|
|
||||||
|
// Check for added relationships
|
||||||
|
for (const newRelationship of newRelationships) {
|
||||||
|
if (!oldRelationships.find((r) => r.id === newRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: newRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'added',
|
||||||
|
relationshipId: newRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for removed relationships
|
||||||
|
for (const oldRelationship of oldRelationships) {
|
||||||
|
if (!newRelationships.find((r) => r.id === oldRelationship.id)) {
|
||||||
|
diffMap.set(
|
||||||
|
getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: oldRelationship.id,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
object: 'relationship',
|
||||||
|
type: 'removed',
|
||||||
|
relationshipId: oldRelationship.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
75
src/context/diff-context/diff-context.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import type { DiffMap } from './types';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import type { DBTable } from '@/lib/domain/db-table';
|
||||||
|
import type { EventEmitter } from 'ahooks/lib/useEventEmitter';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export type DiffEventType = 'diff_calculated';
|
||||||
|
|
||||||
|
export type DiffEventBase<T extends DiffEventType, D> = {
|
||||||
|
action: T;
|
||||||
|
data: D;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DiffCalculatedEvent = DiffEventBase<
|
||||||
|
'diff_calculated',
|
||||||
|
{
|
||||||
|
tablesAdded: DBTable[];
|
||||||
|
fieldsAdded: Map<string, DBField[]>;
|
||||||
|
relationshipsAdded: DBRelationship[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DiffEvent = DiffCalculatedEvent;
|
||||||
|
|
||||||
|
export interface DiffContext {
|
||||||
|
newDiagram: Diagram | null;
|
||||||
|
diffMap: DiffMap;
|
||||||
|
hasDiff: boolean;
|
||||||
|
|
||||||
|
calculateDiff: ({
|
||||||
|
diagram,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diagram: Diagram;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => void;
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
checkIfTableHasChange: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfNewTable: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
checkIfTableRemoved: ({ tableId }: { tableId: string }) => boolean;
|
||||||
|
getTableNewName: ({ tableId }: { tableId: string }) => string | null;
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange: ({
|
||||||
|
tableId,
|
||||||
|
fieldId,
|
||||||
|
}: {
|
||||||
|
tableId: string;
|
||||||
|
fieldId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfFieldRemoved: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
checkIfNewField: ({ fieldId }: { fieldId: string }) => boolean;
|
||||||
|
getFieldNewName: ({ fieldId }: { fieldId: string }) => string | null;
|
||||||
|
getFieldNewType: ({ fieldId }: { fieldId: string }) => DataType | null;
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
checkIfRelationshipRemoved: ({
|
||||||
|
relationshipId,
|
||||||
|
}: {
|
||||||
|
relationshipId: string;
|
||||||
|
}) => boolean;
|
||||||
|
|
||||||
|
events: EventEmitter<DiffEvent>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const diffContext = createContext<DiffContext | undefined>(undefined);
|
||||||
327
src/context/diff-context/diff-provider.tsx
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import type { DiffContext, DiffEvent } from './diff-context';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
import type { ChartDBDiff, DiffMap } from './types';
|
||||||
|
import { generateDiff, getDiffMapKey } from './diff-check/diff-check';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
import { useEventEmitter } from 'ahooks';
|
||||||
|
import type { DBField } from '@/lib/domain/db-field';
|
||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||||
|
|
||||||
|
export const DiffProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [newDiagram, setNewDiagram] = React.useState<Diagram | null>(null);
|
||||||
|
const [diffMap, setDiffMap] = React.useState<DiffMap>(
|
||||||
|
new Map<string, ChartDBDiff>()
|
||||||
|
);
|
||||||
|
const [tablesChanged, setTablesChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
const [fieldsChanged, setFieldsChanged] = React.useState<
|
||||||
|
Map<string, boolean>
|
||||||
|
>(new Map<string, boolean>());
|
||||||
|
|
||||||
|
const events = useEventEmitter<DiffEvent>();
|
||||||
|
|
||||||
|
const generateNewFieldsMap = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const newFieldsMap = new Map<string, DBField[]>();
|
||||||
|
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'field' && diff.type === 'added') {
|
||||||
|
const field = newDiagram?.tables
|
||||||
|
?.find((table) => table.id === diff.tableId)
|
||||||
|
?.fields.find((f) => f.id === diff.fieldId);
|
||||||
|
|
||||||
|
if (field) {
|
||||||
|
newFieldsMap.set(diff.tableId, [
|
||||||
|
...(newFieldsMap.get(diff.tableId) ?? []),
|
||||||
|
field,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return newFieldsMap;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const findNewRelationships = useCallback(
|
||||||
|
({
|
||||||
|
diffMap,
|
||||||
|
newDiagram,
|
||||||
|
}: {
|
||||||
|
diffMap: DiffMap;
|
||||||
|
newDiagram: Diagram;
|
||||||
|
}) => {
|
||||||
|
const relationships: DBRelationship[] = [];
|
||||||
|
diffMap.forEach((diff) => {
|
||||||
|
if (diff.object === 'relationship' && diff.type === 'added') {
|
||||||
|
const relationship = newDiagram?.relationships?.find(
|
||||||
|
(rel) => rel.id === diff.relationshipId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (relationship) {
|
||||||
|
relationships.push(relationship);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return relationships;
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const calculateDiff: DiffContext['calculateDiff'] = useCallback(
|
||||||
|
({ diagram, newDiagram: newDiagramArg }) => {
|
||||||
|
const {
|
||||||
|
diffMap: newDiffs,
|
||||||
|
changedTables: newChangedTables,
|
||||||
|
changedFields: newChangedFields,
|
||||||
|
} = generateDiff({ diagram, newDiagram: newDiagramArg });
|
||||||
|
|
||||||
|
setDiffMap(newDiffs);
|
||||||
|
setTablesChanged(newChangedTables);
|
||||||
|
setFieldsChanged(newChangedFields);
|
||||||
|
setNewDiagram(newDiagramArg);
|
||||||
|
|
||||||
|
events.emit({
|
||||||
|
action: 'diff_calculated',
|
||||||
|
data: {
|
||||||
|
tablesAdded:
|
||||||
|
newDiagramArg?.tables?.filter((table) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: table.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
newDiffs.has(tableKey) &&
|
||||||
|
newDiffs.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
}) ?? [],
|
||||||
|
|
||||||
|
fieldsAdded: generateNewFieldsMap({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
relationshipsAdded: findNewRelationships({
|
||||||
|
diffMap: newDiffs,
|
||||||
|
newDiagram: newDiagramArg,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setDiffMap, events, generateNewFieldsMap, findNewRelationships]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getTableNewName = useCallback<DiffContext['getTableNewName']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableNameKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(tableNameKey)) {
|
||||||
|
const diff = diffMap.get(tableNameKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableHasChange = useCallback<
|
||||||
|
DiffContext['checkIfTableHasChange']
|
||||||
|
>(({ tableId }) => tablesChanged.get(tableId) ?? false, [tablesChanged]);
|
||||||
|
|
||||||
|
const checkIfNewTable = useCallback<DiffContext['checkIfNewTable']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) && diffMap.get(tableKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfTableRemoved = useCallback<DiffContext['checkIfTableRemoved']>(
|
||||||
|
({ tableId }) => {
|
||||||
|
const tableKey = getDiffMapKey({
|
||||||
|
diffObject: 'table',
|
||||||
|
objectId: tableId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(tableKey) &&
|
||||||
|
diffMap.get(tableKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldHasChange = useCallback<
|
||||||
|
DiffContext['checkIfFieldHasChange']
|
||||||
|
>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
return fieldsChanged.get(fieldId) ?? false;
|
||||||
|
},
|
||||||
|
[fieldsChanged]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfFieldRemoved = useCallback<DiffContext['checkIfFieldRemoved']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) &&
|
||||||
|
diffMap.get(fieldKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewField = useCallback<DiffContext['checkIfNewField']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(fieldKey) && diffMap.get(fieldKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewName = useCallback<DiffContext['getFieldNewName']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'name',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getFieldNewType = useCallback<DiffContext['getFieldNewType']>(
|
||||||
|
({ fieldId }) => {
|
||||||
|
const fieldKey = getDiffMapKey({
|
||||||
|
diffObject: 'field',
|
||||||
|
objectId: fieldId,
|
||||||
|
attribute: 'type',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (diffMap.has(fieldKey)) {
|
||||||
|
const diff = diffMap.get(fieldKey);
|
||||||
|
|
||||||
|
if (diff?.type === 'changed') {
|
||||||
|
return diff.newValue as DataType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfNewRelationship = useCallback<
|
||||||
|
DiffContext['checkIfNewRelationship']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'added'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
const checkIfRelationshipRemoved = useCallback<
|
||||||
|
DiffContext['checkIfRelationshipRemoved']
|
||||||
|
>(
|
||||||
|
({ relationshipId }) => {
|
||||||
|
const relationshipKey = getDiffMapKey({
|
||||||
|
diffObject: 'relationship',
|
||||||
|
objectId: relationshipId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
diffMap.has(relationshipKey) &&
|
||||||
|
diffMap.get(relationshipKey)?.type === 'removed'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[diffMap]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<diffContext.Provider
|
||||||
|
value={{
|
||||||
|
newDiagram,
|
||||||
|
diffMap,
|
||||||
|
hasDiff: diffMap.size > 0,
|
||||||
|
|
||||||
|
calculateDiff,
|
||||||
|
|
||||||
|
// table diff
|
||||||
|
getTableNewName,
|
||||||
|
checkIfNewTable,
|
||||||
|
checkIfTableRemoved,
|
||||||
|
checkIfTableHasChange,
|
||||||
|
|
||||||
|
// field diff
|
||||||
|
checkIfFieldHasChange,
|
||||||
|
checkIfFieldRemoved,
|
||||||
|
checkIfNewField,
|
||||||
|
getFieldNewName,
|
||||||
|
getFieldNewType,
|
||||||
|
|
||||||
|
// relationship diff
|
||||||
|
checkIfNewRelationship,
|
||||||
|
checkIfRelationshipRemoved,
|
||||||
|
|
||||||
|
events,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</diffContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
53
src/context/diff-context/types.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import type { DataType } from '@/lib/data/data-types/data-types';
|
||||||
|
|
||||||
|
export type TableDiffAttribute = 'name' | 'comments';
|
||||||
|
|
||||||
|
export interface TableDiff {
|
||||||
|
object: 'table';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
tableId: string;
|
||||||
|
attributes?: TableDiffAttribute;
|
||||||
|
oldValue?: string;
|
||||||
|
newValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RelationshipDiff {
|
||||||
|
object: 'relationship';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
relationshipId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FieldDiffAttribute =
|
||||||
|
| 'name'
|
||||||
|
| 'type'
|
||||||
|
| 'primaryKey'
|
||||||
|
| 'unique'
|
||||||
|
| 'nullable'
|
||||||
|
| 'comments';
|
||||||
|
|
||||||
|
export interface FieldDiff {
|
||||||
|
object: 'field';
|
||||||
|
type: 'added' | 'removed' | 'changed';
|
||||||
|
fieldId: string;
|
||||||
|
tableId: string;
|
||||||
|
attributes?: FieldDiffAttribute;
|
||||||
|
oldValue?: string | boolean | DataType;
|
||||||
|
newValue?: string | boolean | DataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IndexDiff {
|
||||||
|
object: 'index';
|
||||||
|
type: 'added' | 'removed';
|
||||||
|
indexId: string;
|
||||||
|
tableId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChartDBDiff = TableDiff | FieldDiff | IndexDiff | RelationshipDiff;
|
||||||
|
|
||||||
|
export type DiffMap = Map<string, ChartDBDiff>;
|
||||||
|
|
||||||
|
export type DiffObject =
|
||||||
|
| TableDiff['object']
|
||||||
|
| FieldDiff['object']
|
||||||
|
| IndexDiff['object']
|
||||||
|
| RelationshipDiff['object'];
|
||||||
10
src/context/diff-context/use-diff.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { diffContext } from './diff-context';
|
||||||
|
|
||||||
|
export const useDiff = () => {
|
||||||
|
const context = useContext(diffContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useDiff must be used within an DiffProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo, useEffect, useState } from 'react';
|
||||||
import type { ExportImageContext, ImageType } from './export-image-context';
|
import type { ExportImageContext, ImageType } from './export-image-context';
|
||||||
import { exportImageContext } from './export-image-context';
|
import { exportImageContext } from './export-image-context';
|
||||||
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
||||||
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
|
|||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||||
import { useTheme } from '@/hooks/use-theme';
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import logoDark from '@/assets/logo-dark.png';
|
||||||
|
import logoLight from '@/assets/logo-light.png';
|
||||||
|
|
||||||
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { setNodes, getViewport } = useReactFlow();
|
const { setNodes, getViewport } = useReactFlow();
|
||||||
const { effectiveTheme } = useTheme();
|
const { effectiveTheme } = useTheme();
|
||||||
const { diagramName } = useChartDB();
|
const { diagramName } = useChartDB();
|
||||||
|
const [logoBase64, setLogoBase64] = useState<string>('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Convert logo to base64 on component mount
|
||||||
|
const img = new Image();
|
||||||
|
img.src = effectiveTheme === 'light' ? logoLight : logoDark;
|
||||||
|
img.onload = () => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = img.width;
|
||||||
|
canvas.height = img.height;
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.drawImage(img, 0, 0);
|
||||||
|
const base64 = canvas.toDataURL('image/png');
|
||||||
|
setLogoBase64(base64);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
const downloadImage = useCallback(
|
const downloadImage = useCallback(
|
||||||
(dataUrl: string, type: ImageType) => {
|
(dataUrl: string, type: ImageType) => {
|
||||||
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
'http://www.w3.org/2000/svg',
|
'http://www.w3.org/2000/svg',
|
||||||
'rect'
|
'rect'
|
||||||
);
|
);
|
||||||
const padding = 2000;
|
const bgPadding = 2000;
|
||||||
backgroundRect.setAttribute('x', String(-viewport.x - padding));
|
backgroundRect.setAttribute(
|
||||||
backgroundRect.setAttribute('y', String(-viewport.y - padding));
|
'x',
|
||||||
|
String(-viewport.x - bgPadding)
|
||||||
|
);
|
||||||
|
backgroundRect.setAttribute(
|
||||||
|
'y',
|
||||||
|
String(-viewport.y - bgPadding)
|
||||||
|
);
|
||||||
backgroundRect.setAttribute(
|
backgroundRect.setAttribute(
|
||||||
'width',
|
'width',
|
||||||
String(reactFlowBounds.width + 2 * padding)
|
String(reactFlowBounds.width + 2 * bgPadding)
|
||||||
);
|
);
|
||||||
backgroundRect.setAttribute(
|
backgroundRect.setAttribute(
|
||||||
'height',
|
'height',
|
||||||
String(reactFlowBounds.height + 2 * padding)
|
String(reactFlowBounds.height + 2 * bgPadding)
|
||||||
);
|
);
|
||||||
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
||||||
tempSvg.appendChild(backgroundRect);
|
tempSvg.appendChild(backgroundRect);
|
||||||
@@ -148,15 +174,9 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Handle SVG export differently
|
||||||
|
if (type === 'svg') {
|
||||||
const dataUrl = await imageCreateFn(viewportElement, {
|
const dataUrl = await imageCreateFn(viewportElement, {
|
||||||
...(type === 'jpeg' || type === 'png'
|
|
||||||
? {
|
|
||||||
backgroundColor:
|
|
||||||
effectiveTheme === 'light'
|
|
||||||
? '#ffffff'
|
|
||||||
: '#141414',
|
|
||||||
}
|
|
||||||
: {}),
|
|
||||||
width: reactFlowBounds.width,
|
width: reactFlowBounds.width,
|
||||||
height: reactFlowBounds.height,
|
height: reactFlowBounds.height,
|
||||||
style: {
|
style: {
|
||||||
@@ -166,9 +186,98 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
},
|
},
|
||||||
quality: 1,
|
quality: 1,
|
||||||
pixelRatio: scale,
|
pixelRatio: scale,
|
||||||
|
skipFonts: true,
|
||||||
|
});
|
||||||
|
downloadImage(dataUrl, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For PNG and JPEG, continue with the watermark process
|
||||||
|
const initialDataUrl = await imageCreateFn(
|
||||||
|
viewportElement,
|
||||||
|
{
|
||||||
|
backgroundColor:
|
||||||
|
effectiveTheme === 'light'
|
||||||
|
? '#ffffff'
|
||||||
|
: '#141414',
|
||||||
|
width: reactFlowBounds.width,
|
||||||
|
height: reactFlowBounds.height,
|
||||||
|
style: {
|
||||||
|
width: `${reactFlowBounds.width}px`,
|
||||||
|
height: `${reactFlowBounds.height}px`,
|
||||||
|
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
|
||||||
|
},
|
||||||
|
quality: 1,
|
||||||
|
pixelRatio: scale,
|
||||||
|
skipFonts: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create a canvas to combine the diagram and watermark
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
downloadImage(initialDataUrl, type);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set canvas size to match the export size
|
||||||
|
canvas.width = reactFlowBounds.width * scale;
|
||||||
|
canvas.height = reactFlowBounds.height * scale;
|
||||||
|
|
||||||
|
// Load the exported diagram
|
||||||
|
const diagramImage = new Image();
|
||||||
|
diagramImage.src = initialDataUrl;
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
diagramImage.onload = async () => {
|
||||||
|
// Draw the diagram
|
||||||
|
ctx.drawImage(diagramImage, 0, 0);
|
||||||
|
|
||||||
|
// Calculate logo size
|
||||||
|
const logoHeight = Math.max(
|
||||||
|
24,
|
||||||
|
Math.floor(canvas.width * 0.024)
|
||||||
|
);
|
||||||
|
const padding = Math.max(
|
||||||
|
12,
|
||||||
|
Math.floor(logoHeight * 0.5)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load and draw the logo
|
||||||
|
const logoImage = new Image();
|
||||||
|
logoImage.src = logoBase64;
|
||||||
|
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
logoImage.onload = () => {
|
||||||
|
// Calculate logo width while maintaining aspect ratio
|
||||||
|
const logoWidth =
|
||||||
|
(logoImage.width / logoImage.height) *
|
||||||
|
logoHeight;
|
||||||
|
|
||||||
|
// Draw logo in bottom-left corner
|
||||||
|
ctx.globalAlpha = 0.9;
|
||||||
|
ctx.drawImage(
|
||||||
|
logoImage,
|
||||||
|
padding,
|
||||||
|
canvas.height - logoHeight - padding,
|
||||||
|
logoWidth,
|
||||||
|
logoHeight
|
||||||
|
);
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
downloadImage(dataUrl, type);
|
// Convert canvas to data URL
|
||||||
|
const finalDataUrl = canvas.toDataURL(
|
||||||
|
type === 'png' ? 'image/png' : 'image/jpeg'
|
||||||
|
);
|
||||||
|
downloadImage(finalDataUrl, type);
|
||||||
|
resolve(null);
|
||||||
|
};
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
viewportElement.removeChild(tempSvg);
|
viewportElement.removeChild(tempSvg);
|
||||||
hideLoader();
|
hideLoader();
|
||||||
@@ -183,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setNodes,
|
setNodes,
|
||||||
showLoader,
|
showLoader,
|
||||||
effectiveTheme,
|
effectiveTheme,
|
||||||
|
logoBase64,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { useLayout } from '@/hooks/use-layout';
|
import { useLayout } from '@/hooks/use-layout';
|
||||||
|
import { useReactFlow } from '@xyflow/react';
|
||||||
|
|
||||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
const { openOpenDiagramDialog } = useDialog();
|
const { openOpenDiagramDialog } = useDialog();
|
||||||
const { updateDiagramUpdatedAt } = useChartDB();
|
const { updateDiagramUpdatedAt } = useChartDB();
|
||||||
const { toggleSidePanel } = useLayout();
|
const { toggleSidePanel } = useLayout();
|
||||||
|
const { fitView } = useReactFlow();
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
||||||
@@ -37,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
||||||
.keyCombination,
|
.keyCombination,
|
||||||
openOpenDiagramDialog,
|
() => openOpenDiagramDialog(),
|
||||||
{
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
@@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
},
|
},
|
||||||
[toggleSidePanel]
|
[toggleSidePanel]
|
||||||
);
|
);
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination,
|
||||||
|
() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500,
|
||||||
|
padding: 0.1,
|
||||||
|
maxZoom: 0.8,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[fitView]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<keyboardShortcutsContext.Provider value={{}}>
|
<keyboardShortcutsContext.Provider value={{}}>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ export enum KeyboardShortcutAction {
|
|||||||
OPEN_DIAGRAM = 'open_diagram',
|
OPEN_DIAGRAM = 'open_diagram',
|
||||||
SAVE_DIAGRAM = 'save_diagram',
|
SAVE_DIAGRAM = 'save_diagram',
|
||||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||||
|
SHOW_ALL = 'show_all',
|
||||||
|
TOGGLE_THEME = 'toggle_theme',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface KeyboardShortcut {
|
export interface KeyboardShortcut {
|
||||||
@@ -55,6 +57,20 @@ export const keyboardShortcuts: Record<
|
|||||||
keyCombinationMac: 'meta+b',
|
keyCombinationMac: 'meta+b',
|
||||||
keyCombinationWin: 'ctrl+b',
|
keyCombinationWin: 'ctrl+b',
|
||||||
},
|
},
|
||||||
|
[KeyboardShortcutAction.SHOW_ALL]: {
|
||||||
|
action: KeyboardShortcutAction.SHOW_ALL,
|
||||||
|
keyCombinationLabelMac: '⌘0',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+0',
|
||||||
|
keyCombinationMac: 'meta+0',
|
||||||
|
keyCombinationWin: 'ctrl+0',
|
||||||
|
},
|
||||||
|
[KeyboardShortcutAction.TOGGLE_THEME]: {
|
||||||
|
action: KeyboardShortcutAction.TOGGLE_THEME,
|
||||||
|
keyCombinationLabelMac: '⌘M',
|
||||||
|
keyCombinationLabelWin: 'Ctrl+M',
|
||||||
|
keyCombinationMac: 'meta+m',
|
||||||
|
keyCombinationWin: 'ctrl+m',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface KeyboardShortcutForOS {
|
export interface KeyboardShortcutForOS {
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ export interface LocalConfigContext {
|
|||||||
|
|
||||||
showDependenciesOnCanvas: boolean;
|
showDependenciesOnCanvas: boolean;
|
||||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||||
|
|
||||||
|
showMiniMapOnCanvas: boolean;
|
||||||
|
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocalConfigContext = createContext<LocalConfigContext>({
|
export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||||
@@ -44,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
schemasFilter: {},
|
schemasFilter: {},
|
||||||
setSchemasFilter: emptyFn,
|
setSchemasFilter: emptyFn,
|
||||||
|
|
||||||
showCardinality: false,
|
showCardinality: true,
|
||||||
setShowCardinality: emptyFn,
|
setShowCardinality: emptyFn,
|
||||||
|
|
||||||
hideMultiSchemaNotification: false,
|
hideMultiSchemaNotification: false,
|
||||||
@@ -58,4 +61,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
|||||||
|
|
||||||
showDependenciesOnCanvas: false,
|
showDependenciesOnCanvas: false,
|
||||||
setShowDependenciesOnCanvas: emptyFn,
|
setShowDependenciesOnCanvas: emptyFn,
|
||||||
|
|
||||||
|
showMiniMapOnCanvas: false,
|
||||||
|
setShowMiniMapOnCanvas: emptyFn,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
|||||||
const githubRepoOpenedKey = 'github_repo_opened';
|
const githubRepoOpenedKey = 'github_repo_opened';
|
||||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||||
|
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||||
|
|
||||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -30,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||||
@@ -54,6 +55,11 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
'true'
|
'true'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
|
||||||
|
React.useState<boolean>(
|
||||||
|
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
starUsDialogLastOpenKey,
|
starUsDialogLastOpenKey,
|
||||||
@@ -95,6 +101,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
);
|
);
|
||||||
}, [showDependenciesOnCanvas]);
|
}, [showDependenciesOnCanvas]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem(
|
||||||
|
showMiniMapOnCanvasKey,
|
||||||
|
showMiniMapOnCanvas.toString()
|
||||||
|
);
|
||||||
|
}, [showMiniMapOnCanvas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LocalConfigContext.Provider
|
<LocalConfigContext.Provider
|
||||||
value={{
|
value={{
|
||||||
@@ -114,6 +127,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
setStarUsDialogLastOpen,
|
setStarUsDialogLastOpen,
|
||||||
showDependenciesOnCanvas,
|
showDependenciesOnCanvas,
|
||||||
setShowDependenciesOnCanvas,
|
setShowDependenciesOnCanvas,
|
||||||
|
showMiniMapOnCanvas,
|
||||||
|
setShowMiniMapOnCanvas,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -122,6 +122,32 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
config: '++id, defaultDiagramId',
|
config: '++id, defaultDiagramId',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
db.version(8).stores({
|
||||||
|
diagrams:
|
||||||
|
'++id, name, databaseType, databaseEdition, createdAt, updatedAt',
|
||||||
|
db_tables:
|
||||||
|
'++id, diagramId, name, schema, x, y, fields, indexes, color, createdAt, width, comment, isView, isMaterializedView, order',
|
||||||
|
db_relationships:
|
||||||
|
'++id, diagramId, name, sourceSchema, sourceTableId, targetSchema, targetTableId, sourceFieldId, targetFieldId, type, createdAt',
|
||||||
|
db_dependencies:
|
||||||
|
'++id, diagramId, schema, tableId, dependentSchema, dependentTableId, createdAt',
|
||||||
|
config: '++id, defaultDiagramId',
|
||||||
|
});
|
||||||
|
|
||||||
|
db.version(9).upgrade((tx) =>
|
||||||
|
tx
|
||||||
|
.table<DBTable & { diagramId: string }>('db_tables')
|
||||||
|
.toCollection()
|
||||||
|
.modify((table) => {
|
||||||
|
for (const field of table.fields) {
|
||||||
|
if (typeof field.nullable === 'string') {
|
||||||
|
field.nullable =
|
||||||
|
(field.nullable as string).toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
db.on('ready', async () => {
|
db.on('ready', async () => {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
|
|
||||||
@@ -270,6 +296,23 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
attributes: Partial<Diagram>;
|
attributes: Partial<Diagram>;
|
||||||
}) => {
|
}) => {
|
||||||
await db.diagrams.update(id, attributes);
|
await db.diagrams.update(id, attributes);
|
||||||
|
|
||||||
|
if (attributes.id) {
|
||||||
|
await Promise.all([
|
||||||
|
db.db_tables
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(id)
|
||||||
|
.modify({ diagramId: attributes.id }),
|
||||||
|
db.db_relationships
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(id)
|
||||||
|
.modify({ diagramId: attributes.id }),
|
||||||
|
db.db_dependencies
|
||||||
|
.where('diagramId')
|
||||||
|
.equals(id)
|
||||||
|
.modify({ diagramId: attributes.id }),
|
||||||
|
]);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteDiagram: StorageContext['deleteDiagram'] = async (
|
const deleteDiagram: StorageContext['deleteDiagram'] = async (
|
||||||
@@ -345,15 +388,7 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
.equals(diagramId)
|
.equals(diagramId)
|
||||||
.toArray();
|
.toArray();
|
||||||
|
|
||||||
// Sort tables first alphabetically, then views alphabetically
|
return tables;
|
||||||
return tables.sort((a, b) => {
|
|
||||||
if (a.isView === b.isView) {
|
|
||||||
// Both are either tables or views, so sort alphabetically by name
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
}
|
|
||||||
// If one is a view and the other is not, put tables first
|
|
||||||
return a.isView ? 1 : -1;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const addRelationship: StorageContext['addRelationship'] = async ({
|
const addRelationship: StorageContext['addRelationship'] = async ({
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState, useCallback } from 'react';
|
||||||
import type { EffectiveTheme } from './theme-context';
|
import type { EffectiveTheme } from './theme-context';
|
||||||
import { ThemeContext } from './theme-context';
|
import { ThemeContext } from './theme-context';
|
||||||
import { useMediaQuery } from 'react-responsive';
|
import { useMediaQuery } from 'react-responsive';
|
||||||
import { useLocalConfig } from '@/hooks/use-local-config';
|
import { useLocalConfig } from '@/hooks/use-local-config';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import {
|
||||||
|
KeyboardShortcutAction,
|
||||||
|
keyboardShortcutsForOS,
|
||||||
|
} from '../keyboard-shortcuts-context/keyboard-shortcuts';
|
||||||
|
|
||||||
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
|||||||
}
|
}
|
||||||
}, [effectiveTheme]);
|
}, [effectiveTheme]);
|
||||||
|
|
||||||
|
const handleThemeToggle = useCallback(() => {
|
||||||
|
if (theme === 'system') {
|
||||||
|
setTheme(effectiveTheme === 'dark' ? 'light' : 'dark');
|
||||||
|
} else {
|
||||||
|
setTheme(theme === 'dark' ? 'light' : 'dark');
|
||||||
|
}
|
||||||
|
}, [theme, effectiveTheme, setTheme]);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
keyboardShortcutsForOS[KeyboardShortcutAction.TOGGLE_THEME]
|
||||||
|
.keyCombination,
|
||||||
|
handleThemeToggle,
|
||||||
|
{
|
||||||
|
preventDefault: true,
|
||||||
|
},
|
||||||
|
[handleThemeToggle]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from '@/components/alert-dialog/alert-dialog';
|
} from '@/components/alert-dialog/alert-dialog';
|
||||||
import type { AlertDialogProps } from '@radix-ui/react-alert-dialog';
|
import type { AlertDialogProps } from '@radix-ui/react-alert-dialog';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
|
||||||
export interface BaseAlertDialogProps {
|
export interface BaseAlertDialogProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -33,7 +33,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
|
|||||||
content,
|
content,
|
||||||
onClose,
|
onClose,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeAlert } = useDialog();
|
const { closeAlert } = useAlert();
|
||||||
|
|
||||||
const closeAlertHandler = useCallback(() => {
|
const closeAlertHandler = useCallback(() => {
|
||||||
onClose?.();
|
onClose?.();
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
||||||
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
||||||
|
|
||||||
|
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadScripts = async () => {
|
const loadScripts = async () => {
|
||||||
const { importMetadataScripts } = await import(
|
const { importMetadataScripts } = await import(
|
||||||
@@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
const inputValue = e.target.value;
|
const inputValue = e.target.value;
|
||||||
setScriptResult(inputValue);
|
setScriptResult(inputValue);
|
||||||
|
|
||||||
|
// Automatically open SSMS info when input length is exactly 65535
|
||||||
|
if (inputValue.length === 65535) {
|
||||||
|
setShowSSMSInfoDialog(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[setScriptResult]
|
[setScriptResult]
|
||||||
);
|
);
|
||||||
@@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
{t('new_diagram_dialog.import_database.step_1')}
|
{t('new_diagram_dialog.import_database.step_1')}
|
||||||
</div>
|
</div>
|
||||||
{databaseType === DatabaseType.SQL_SERVER && (
|
{databaseType === DatabaseType.SQL_SERVER && (
|
||||||
<SSMSInfo />
|
<SSMSInfo
|
||||||
|
open={showSSMSInfoDialog}
|
||||||
|
setOpen={setShowSSMSInfoDialog}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
||||||
@@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
|||||||
showCheckJsonButton,
|
showCheckJsonButton,
|
||||||
isCheckingJson,
|
isCheckingJson,
|
||||||
handleCheckJson,
|
handleCheckJson,
|
||||||
|
showSSMSInfoDialog,
|
||||||
|
setShowSSMSInfoDialog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const renderFooter = useCallback(() => {
|
const renderFooter = useCallback(() => {
|
||||||
|
|||||||
@@ -4,32 +4,55 @@ import {
|
|||||||
HoverCardTrigger,
|
HoverCardTrigger,
|
||||||
} from '@/components/hover-card/hover-card';
|
} from '@/components/hover-card/hover-card';
|
||||||
import { Label } from '@/components/label/label';
|
import { Label } from '@/components/label/label';
|
||||||
import { Info } from 'lucide-react';
|
import { Info, X } from 'lucide-react';
|
||||||
import React from 'react';
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||||
import SSMSInstructions from '@/assets/ssms-instructions.png';
|
import SSMSInstructions from '@/assets/ssms-instructions.png';
|
||||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
export interface SSMSInfoProps {}
|
export interface SSMSInfoProps {
|
||||||
|
open?: boolean;
|
||||||
|
setOpen?: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
export const SSMSInfo = React.forwardRef<
|
export const SSMSInfo = React.forwardRef<
|
||||||
React.ElementRef<typeof HoverCardTrigger>,
|
React.ElementRef<typeof HoverCardTrigger>,
|
||||||
SSMSInfoProps
|
SSMSInfoProps
|
||||||
>((props, ref) => {
|
>(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
|
||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (controlledOpen) {
|
||||||
|
setOpen(true);
|
||||||
|
}
|
||||||
|
}, [controlledOpen]);
|
||||||
|
|
||||||
|
const closeHandler = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
setControlledOpen?.(false);
|
||||||
|
}, [setControlledOpen]);
|
||||||
|
|
||||||
|
const isOpen = useMemo(
|
||||||
|
() => open || controlledOpen,
|
||||||
|
[open, controlledOpen]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HoverCard
|
<HoverCard
|
||||||
open={open}
|
open={isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
|
if (controlledOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setOpen(isOpen);
|
setOpen(isOpen);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<HoverCardTrigger ref={ref} {...props} asChild>
|
<HoverCardTrigger ref={ref} asChild>
|
||||||
<div
|
<div
|
||||||
className="flex flex-row items-center gap-1 text-pink-600"
|
className="flex flex-row items-center gap-1 text-pink-600"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setOpen(!open);
|
setOpen?.(!open);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Info size={14} />
|
<Info size={14} />
|
||||||
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
</HoverCardTrigger>
|
</HoverCardTrigger>
|
||||||
<HoverCardContent className="w-80">
|
<HoverCardContent className="w-80">
|
||||||
<div className="flex">
|
<div className="flex flex-col">
|
||||||
<div className="space-y-1">
|
<div className="flex items-start justify-between">
|
||||||
<h4 className="text-sm font-semibold">
|
<h4 className="text-sm font-semibold">
|
||||||
{t(
|
{t(
|
||||||
'new_diagram_dialog.import_database.ssms_instructions.title'
|
'new_diagram_dialog.import_database.ssms_instructions.title'
|
||||||
)}
|
)}
|
||||||
</h4>
|
</h4>
|
||||||
|
<button
|
||||||
|
onClick={closeHandler}
|
||||||
|
className="text-muted-foreground hover:text-foreground"
|
||||||
|
>
|
||||||
|
<X size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
<span className="font-semibold">1. </span>
|
<span className="font-semibold">1. </span>
|
||||||
{t(
|
{t(
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||||
DatabaseType.GENERIC
|
DatabaseType.GENERIC
|
||||||
);
|
);
|
||||||
const { closeCreateDiagramDialog } = useDialog();
|
const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
|
||||||
const { updateConfig } = useConfig();
|
const { updateConfig } = useConfig();
|
||||||
const [scriptResult, setScriptResult] = useState('');
|
const [scriptResult, setScriptResult] = useState('');
|
||||||
const [databaseEdition, setDatabaseEdition] = useState<
|
const [databaseEdition, setDatabaseEdition] = useState<
|
||||||
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
await updateConfig({ defaultDiagramId: diagram.id });
|
await updateConfig({ defaultDiagramId: diagram.id });
|
||||||
closeCreateDiagramDialog();
|
closeCreateDiagramDialog();
|
||||||
navigate(`/diagrams/${diagram.id}`);
|
navigate(`/diagrams/${diagram.id}`);
|
||||||
|
setTimeout(
|
||||||
|
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
|
||||||
|
700
|
||||||
|
);
|
||||||
}, [
|
}, [
|
||||||
databaseType,
|
databaseType,
|
||||||
addDiagram,
|
addDiagram,
|
||||||
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
|||||||
navigate,
|
navigate,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
diagramNumber,
|
diagramNumber,
|
||||||
|
openImportDBMLDialog,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -8,10 +8,13 @@ export interface ExampleOptionProps {}
|
|||||||
export const ExampleOption: React.FC<ExampleOptionProps> = () => {
|
export const ExampleOption: React.FC<ExampleOptionProps> = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Link href="/examples" className="text-primary hover:text-primary">
|
<Link
|
||||||
<div className="flex size-20 cursor-pointer flex-col items-center rounded-md border py-3 text-center md:size-32">
|
href="/examples"
|
||||||
<div className="flex flex-1 items-center">
|
className="col-span-3 text-primary hover:text-primary"
|
||||||
<LayoutGrid size={34} />
|
>
|
||||||
|
<div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 rounded-md border py-3 text-center">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<LayoutGrid className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col-reverse">
|
<div className="flex flex-col-reverse">
|
||||||
<div className="hidden text-sm text-primary md:flex">
|
<div className="hidden text-sm text-primary md:flex">
|
||||||
|
|||||||
@@ -1,22 +1,61 @@
|
|||||||
import React from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
import { ToggleGroup } from '@/components/toggle/toggle-group';
|
import { ToggleGroup } from '@/components/toggle/toggle-group';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import { DatabaseOption } from './database-option';
|
import { DatabaseOption } from './database-option';
|
||||||
import { ExampleOption } from './example-option';
|
import { ExampleOption } from './example-option';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
export interface SelectDatabaseContentProps {
|
export interface SelectDatabaseContentProps {
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
||||||
onContinue: () => void;
|
onContinue: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ROW_SIZE = 3;
|
||||||
|
const ROWS = 2;
|
||||||
|
const TOTAL_SLOTS = ROW_SIZE * ROWS;
|
||||||
|
const SUPPORTED_DB_TYPES: DatabaseType[] = [
|
||||||
|
DatabaseType.MYSQL,
|
||||||
|
DatabaseType.POSTGRESQL,
|
||||||
|
DatabaseType.MARIADB,
|
||||||
|
DatabaseType.SQLITE,
|
||||||
|
DatabaseType.SQL_SERVER,
|
||||||
|
DatabaseType.COCKROACHDB,
|
||||||
|
DatabaseType.CLICKHOUSE,
|
||||||
|
];
|
||||||
|
|
||||||
export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({
|
export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({
|
||||||
databaseType,
|
databaseType,
|
||||||
setDatabaseType,
|
setDatabaseType,
|
||||||
onContinue,
|
onContinue,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [currentRow, setCurrentRow] = useState(0);
|
||||||
|
const currentDatabasesTypes = useMemo(
|
||||||
|
() =>
|
||||||
|
SUPPORTED_DB_TYPES.slice(
|
||||||
|
currentRow * ROW_SIZE,
|
||||||
|
currentRow * ROW_SIZE + TOTAL_SLOTS
|
||||||
|
),
|
||||||
|
[currentRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasNextRow = useMemo(
|
||||||
|
() => (currentRow + 1) * ROW_SIZE < SUPPORTED_DB_TYPES.length,
|
||||||
|
[currentRow]
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasPreviousRow = useMemo(() => currentRow > 0, [currentRow]);
|
||||||
|
|
||||||
|
const toggleRow = () => {
|
||||||
|
if (currentRow === 0 && hasNextRow) {
|
||||||
|
setCurrentRow(currentRow + 1);
|
||||||
|
} else if (currentRow > 0) {
|
||||||
|
setCurrentRow(currentRow - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-1 items-center justify-center">
|
<div className="flex flex-1 flex-col items-center justify-center gap-4">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
value={databaseType}
|
value={databaseType}
|
||||||
onValueChange={(value: DatabaseType) => {
|
onValueChange={(value: DatabaseType) => {
|
||||||
@@ -30,12 +69,41 @@ export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({
|
|||||||
type="single"
|
type="single"
|
||||||
className="grid grid-flow-row grid-cols-3 gap-6"
|
className="grid grid-flow-row grid-cols-3 gap-6"
|
||||||
>
|
>
|
||||||
<DatabaseOption type={DatabaseType.MYSQL} />
|
{Array.from({ length: TOTAL_SLOTS }).map((_, index) =>
|
||||||
<DatabaseOption type={DatabaseType.POSTGRESQL} />
|
currentDatabasesTypes?.[index] ? (
|
||||||
<DatabaseOption type={DatabaseType.MARIADB} />
|
<DatabaseOption
|
||||||
<DatabaseOption type={DatabaseType.SQLITE} />
|
key={currentDatabasesTypes[index]}
|
||||||
<DatabaseOption type={DatabaseType.SQL_SERVER} />
|
type={currentDatabasesTypes[index]}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="col-span-3 flex flex-1 flex-col gap-1">
|
||||||
|
{hasNextRow || hasPreviousRow ? (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={toggleRow}
|
||||||
|
className="col-span-3 h-8"
|
||||||
|
>
|
||||||
|
{currentRow === 0 ? (
|
||||||
|
<div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 py-3 text-center md:h-12">
|
||||||
|
<ChevronDown className="mr-2 size-3.5" />
|
||||||
|
<span className="text-xs">
|
||||||
|
More Databases
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 py-3 text-center md:h-12">
|
||||||
|
<ChevronUp className="mr-2 size-3.5" />
|
||||||
|
<span className="text-xs">
|
||||||
|
Primary Databases
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
<ExampleOption />
|
<ExampleOption />
|
||||||
|
</div>
|
||||||
</ToggleGroup>
|
</ToggleGroup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
|
|||||||
const ErrorMessageRelationshipFieldsNotSameType =
|
const ErrorMessageRelationshipFieldsNotSameType =
|
||||||
'Relationships can only be created between fields of the same type';
|
'Relationships can only be created between fields of the same type';
|
||||||
|
|
||||||
export interface CreateRelationshipDialogProps extends BaseDialogProps {}
|
export interface CreateRelationshipDialogProps extends BaseDialogProps {
|
||||||
|
sourceTableId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const CreateRelationshipDialog: React.FC<
|
export const CreateRelationshipDialog: React.FC<
|
||||||
CreateRelationshipDialogProps
|
CreateRelationshipDialogProps
|
||||||
> = ({ dialog }) => {
|
> = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
|
||||||
const { closeCreateRelationshipDialog } = useDialog();
|
const { closeCreateRelationshipDialog } = useDialog();
|
||||||
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>();
|
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
|
||||||
|
preSelectedSourceTableId
|
||||||
|
);
|
||||||
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
|
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
|
||||||
const [referencedTableId, setReferencedTableId] = useState<
|
const [referencedTableId, setReferencedTableId] = useState<
|
||||||
string | undefined
|
string | undefined
|
||||||
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
|
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
|
||||||
const { fitView, setEdges } = useReactFlow();
|
const { fitView, setEdges } = useReactFlow();
|
||||||
const { databaseType } = useChartDB();
|
const { databaseType } = useChartDB();
|
||||||
|
const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
|
||||||
|
const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const tableOptions = useMemo(() => {
|
const tableOptions = useMemo(() => {
|
||||||
return tables.map(
|
return tables.map(
|
||||||
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
setReferencedTableId(undefined);
|
setReferencedTableId(undefined);
|
||||||
setReferencedFieldId(undefined);
|
setReferencedFieldId(undefined);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
|
setPrimaryFieldSelectOpen(false);
|
||||||
|
setReferencedTableSelectOpen(false);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (preSelectedSourceTableId) {
|
||||||
|
const table = getTable(preSelectedSourceTableId);
|
||||||
|
if (table) {
|
||||||
|
setPrimaryTableId(preSelectedSourceTableId);
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setPrimaryFieldSelectOpen(true);
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, [preSelectedSourceTableId, getTable]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCanCreateRelationship(false);
|
setCanCreateRelationship(false);
|
||||||
setErrorMessage('');
|
setErrorMessage('');
|
||||||
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
)}
|
)}
|
||||||
value={primaryTableId}
|
value={primaryTableId}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setPrimaryTableId(value as string);
|
const newTableId = value as string;
|
||||||
|
setPrimaryTableId(newTableId);
|
||||||
|
if (
|
||||||
|
newTableId !==
|
||||||
|
preSelectedSourceTableId
|
||||||
|
) {
|
||||||
setPrimaryFieldId(undefined);
|
setPrimaryFieldId(undefined);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
emptyPlaceholder={t(
|
emptyPlaceholder={t(
|
||||||
'create_relationship_dialog.no_tables_found'
|
'create_relationship_dialog.no_tables_found'
|
||||||
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
'create_relationship_dialog.primary_field_placeholder'
|
'create_relationship_dialog.primary_field_placeholder'
|
||||||
)}
|
)}
|
||||||
value={primaryFieldId}
|
value={primaryFieldId}
|
||||||
|
open={primaryFieldSelectOpen}
|
||||||
|
onOpenChange={setPrimaryFieldSelectOpen}
|
||||||
onChange={(value) =>
|
onChange={(value) =>
|
||||||
setPrimaryFieldId(value as string)
|
setPrimaryFieldId(value as string)
|
||||||
}
|
}
|
||||||
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
|
|||||||
'create_relationship_dialog.referenced_table_placeholder'
|
'create_relationship_dialog.referenced_table_placeholder'
|
||||||
)}
|
)}
|
||||||
value={referencedTableId}
|
value={referencedTableId}
|
||||||
|
open={referencedTableSelectOpen}
|
||||||
|
onOpenChange={setReferencedTableSelectOpen}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
setReferencedTableId(value as string);
|
setReferencedTableId(value as string);
|
||||||
setReferencedFieldId(undefined);
|
setReferencedFieldId(undefined);
|
||||||
|
|||||||
@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
|
|||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useChartDB } from '@/hooks/use-chartdb';
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
|
||||||
import { Spinner } from '@/components/spinner/spinner';
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
import { waitFor } from '@/lib/utils';
|
|
||||||
import { AlertCircle } from 'lucide-react';
|
import { AlertCircle } from 'lucide-react';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
||||||
|
import { useExportDiagram } from '@/hooks/use-export-diagram';
|
||||||
|
|
||||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
||||||
|
|
||||||
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
|
|||||||
dialog,
|
dialog,
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { diagramName, currentDiagram } = useChartDB();
|
const { currentDiagram } = useChartDB();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const { closeExportDiagramDialog } = useDialog();
|
const { closeExportDiagramDialog } = useDialog();
|
||||||
const [error, setError] = useState(false);
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) return;
|
if (!dialog.open) return;
|
||||||
setIsLoading(false);
|
|
||||||
setError(false);
|
setError(false);
|
||||||
}, [dialog.open]);
|
}, [dialog.open]);
|
||||||
|
|
||||||
const downloadOutput = useCallback(
|
const { exportDiagram, isExporting: isLoading } = useExportDiagram();
|
||||||
(dataUrl: string) => {
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.setAttribute('download', `ChartDB(${diagramName}).json`);
|
|
||||||
a.setAttribute('href', dataUrl);
|
|
||||||
a.click();
|
|
||||||
},
|
|
||||||
[diagramName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleExport = useCallback(async () => {
|
const handleExport = useCallback(async () => {
|
||||||
setIsLoading(true);
|
|
||||||
await waitFor(1000);
|
|
||||||
try {
|
try {
|
||||||
const json = diagramToJSONOutput(currentDiagram);
|
await exportDiagram({ diagram: currentDiagram });
|
||||||
const blob = new Blob([json], { type: 'application/json' });
|
|
||||||
const dataUrl = URL.createObjectURL(blob);
|
|
||||||
downloadOutput(dataUrl);
|
|
||||||
setIsLoading(false);
|
|
||||||
closeExportDiagramDialog();
|
closeExportDiagramDialog();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(true);
|
setError(true);
|
||||||
setIsLoading(false);
|
|
||||||
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
|
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
|
||||||
|
|
||||||
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
||||||
() =>
|
() =>
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import {
|
|||||||
} from '@/lib/data/export-metadata/export-sql-script';
|
} from '@/lib/data/export-metadata/export-sql-script';
|
||||||
import { databaseTypeToLabelMap } from '@/lib/databases';
|
import { databaseTypeToLabelMap } from '@/lib/databases';
|
||||||
import { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
|
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||||
import { Annoyed, Sparkles } from 'lucide-react';
|
import { Annoyed, Sparkles } from 'lucide-react';
|
||||||
import React, { useCallback, useEffect, useRef } from 'react';
|
import React, { useCallback, useEffect, useRef } from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
|
||||||
export interface ExportSQLDialogProps extends BaseDialogProps {
|
export interface ExportSQLDialogProps extends BaseDialogProps {
|
||||||
targetDatabaseType: DatabaseType;
|
targetDatabaseType: DatabaseType;
|
||||||
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
|||||||
targetDatabaseType,
|
targetDatabaseType,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeExportSQLDialog } = useDialog();
|
const { closeExportSQLDialog } = useDialog();
|
||||||
const { currentDiagram } = useChartDB();
|
const { currentDiagram, filteredSchemas } = useChartDB();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [script, setScript] = React.useState<string>();
|
const [script, setScript] = React.useState<string>();
|
||||||
const [error, setError] = React.useState<boolean>(false);
|
const [error, setError] = React.useState<boolean>(false);
|
||||||
@@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
|||||||
const abortControllerRef = useRef<AbortController | null>(null);
|
const abortControllerRef = useRef<AbortController | null>(null);
|
||||||
|
|
||||||
const exportSQLScript = useCallback(async () => {
|
const exportSQLScript = useCallback(async () => {
|
||||||
|
const filteredDiagram: Diagram = {
|
||||||
|
...currentDiagram,
|
||||||
|
tables: currentDiagram.tables?.filter((table) =>
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas)
|
||||||
|
),
|
||||||
|
relationships: currentDiagram.relationships?.filter((rel) => {
|
||||||
|
const sourceTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === rel.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === rel.targetTableId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
sourceTable &&
|
||||||
|
targetTable &&
|
||||||
|
shouldShowTablesBySchemaFilter(
|
||||||
|
sourceTable,
|
||||||
|
filteredSchemas
|
||||||
|
) &&
|
||||||
|
shouldShowTablesBySchemaFilter(targetTable, filteredSchemas)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
dependencies: currentDiagram.dependencies?.filter((dep) => {
|
||||||
|
const table = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === dep.tableId
|
||||||
|
);
|
||||||
|
const dependentTable = currentDiagram.tables?.find(
|
||||||
|
(t) => t.id === dep.dependentTableId
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
table &&
|
||||||
|
dependentTable &&
|
||||||
|
shouldShowTablesBySchemaFilter(table, filteredSchemas) &&
|
||||||
|
shouldShowTablesBySchemaFilter(
|
||||||
|
dependentTable,
|
||||||
|
filteredSchemas
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if (targetDatabaseType === DatabaseType.GENERIC) {
|
if (targetDatabaseType === DatabaseType.GENERIC) {
|
||||||
return Promise.resolve(exportBaseSQL(currentDiagram));
|
return Promise.resolve(exportBaseSQL(filteredDiagram));
|
||||||
} else {
|
} else {
|
||||||
return exportSQL(currentDiagram, targetDatabaseType, {
|
return exportSQL(filteredDiagram, targetDatabaseType, {
|
||||||
stream: true,
|
stream: true,
|
||||||
onResultStream: (text) =>
|
onResultStream: (text) =>
|
||||||
setScript((prev) => (prev ? prev + text : text)),
|
setScript((prev) => (prev ? prev + text : text)),
|
||||||
signal: abortControllerRef.current?.signal,
|
signal: abortControllerRef.current?.signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [targetDatabaseType, currentDiagram]);
|
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dialog.open) {
|
if (!dialog.open) {
|
||||||
@@ -70,7 +113,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
|||||||
const script = await exportSQLScript();
|
const script = await exportSQLScript();
|
||||||
setScript(script);
|
setScript(script);
|
||||||
setIsScriptLoading(false);
|
setIsScriptLoading(false);
|
||||||
} catch (e) {
|
} catch {
|
||||||
setError(true);
|
setError(true);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Dialog, DialogContent } from '@/components/dialog/dialog';
|
import { Dialog, DialogContent } from '@/components/dialog/dialog';
|
||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
import { DatabaseType } from '@/lib/domain/database-type';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { ImportDatabase } from '../common/import-database/import-database';
|
import { ImportDatabase } from '../common/import-database/import-database';
|
||||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||||
@@ -12,6 +12,7 @@ import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
|||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { useReactFlow } from '@xyflow/react';
|
import { useReactFlow } from '@xyflow/react';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useAlert } from '@/context/alert-context/alert-context';
|
||||||
|
|
||||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
||||||
databaseType: DatabaseType;
|
databaseType: DatabaseType;
|
||||||
@@ -21,7 +22,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
dialog,
|
dialog,
|
||||||
databaseType,
|
databaseType,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeImportDatabaseDialog, showAlert } = useDialog();
|
const { closeImportDatabaseDialog } = useDialog();
|
||||||
|
const { showAlert } = useAlert();
|
||||||
const {
|
const {
|
||||||
tables,
|
tables,
|
||||||
relationships,
|
relationships,
|
||||||
@@ -30,6 +32,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
addTables,
|
addTables,
|
||||||
addRelationships,
|
addRelationships,
|
||||||
diagramName,
|
diagramName,
|
||||||
|
databaseType: currentDatabaseType,
|
||||||
|
updateDatabaseType,
|
||||||
} = useChartDB();
|
} = useChartDB();
|
||||||
const [scriptResult, setScriptResult] = useState('');
|
const [scriptResult, setScriptResult] = useState('');
|
||||||
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
const { resetRedoStack, resetUndoStack } = useRedoUndoStack();
|
||||||
@@ -282,6 +286,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (currentDatabaseType === DatabaseType.GENERIC) {
|
||||||
|
await updateDatabaseType(databaseType);
|
||||||
|
}
|
||||||
|
|
||||||
setNodes((nodes) =>
|
setNodes((nodes) =>
|
||||||
nodes.map((node) => ({
|
nodes.map((node) => ({
|
||||||
...node,
|
...node,
|
||||||
@@ -297,6 +305,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
|||||||
closeImportDatabaseDialog();
|
closeImportDatabaseDialog();
|
||||||
}, [
|
}, [
|
||||||
databaseEdition,
|
databaseEdition,
|
||||||
|
currentDatabaseType,
|
||||||
|
updateDatabaseType,
|
||||||
databaseType,
|
databaseType,
|
||||||
scriptResult,
|
scriptResult,
|
||||||
tables,
|
tables,
|
||||||
|
|||||||
410
src/dialogs/import-dbml-dialog/import-dbml-dialog.tsx
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import React, {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
Suspense,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
|
import * as monaco from 'monaco-editor';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogInternalContent,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/dialog/dialog';
|
||||||
|
import { Button } from '@/components/button/button';
|
||||||
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||||
|
import { useTheme } from '@/hooks/use-theme';
|
||||||
|
import { AlertCircle } from 'lucide-react';
|
||||||
|
import { importDBMLToDiagram } from '@/lib/dbml-import';
|
||||||
|
import { useChartDB } from '@/hooks/use-chartdb';
|
||||||
|
import { Parser } from '@dbml/core';
|
||||||
|
import { useCanvas } from '@/hooks/use-canvas';
|
||||||
|
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||||
|
import { useToast } from '@/components/toast/use-toast';
|
||||||
|
import { Spinner } from '@/components/spinner/spinner';
|
||||||
|
import { debounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DBMLError {
|
||||||
|
message: string;
|
||||||
|
line: number;
|
||||||
|
column: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseDBMLError(error: unknown): DBMLError | null {
|
||||||
|
try {
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
const parsed = JSON.parse(error);
|
||||||
|
if (parsed.diags?.[0]) {
|
||||||
|
const diag = parsed.diags[0];
|
||||||
|
return {
|
||||||
|
message: diag.message,
|
||||||
|
line: diag.location.start.line,
|
||||||
|
column: diag.location.start.column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (error && typeof error === 'object' && 'diags' in error) {
|
||||||
|
const parsed = error as {
|
||||||
|
diags: Array<{
|
||||||
|
message: string;
|
||||||
|
location: { start: { line: number; column: number } };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
if (parsed.diags?.[0]) {
|
||||||
|
return {
|
||||||
|
message: parsed.diags[0].message,
|
||||||
|
line: parsed.diags[0].location.start.line,
|
||||||
|
column: parsed.diags[0].location.start.column,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing DBML error:', e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||||
|
withCreateEmptyDiagram?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ImportDBMLDialog: React.FC<ImportDBMLDialogProps> = ({
|
||||||
|
dialog,
|
||||||
|
withCreateEmptyDiagram,
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const initialDBML = `// Use DBML to define your database structure
|
||||||
|
// Simple Blog System with Comments Example
|
||||||
|
|
||||||
|
Table users {
|
||||||
|
id integer [primary key]
|
||||||
|
name varchar
|
||||||
|
email varchar
|
||||||
|
}
|
||||||
|
|
||||||
|
Table posts {
|
||||||
|
id integer [primary key]
|
||||||
|
title varchar
|
||||||
|
content text
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
Table comments {
|
||||||
|
id integer [primary key]
|
||||||
|
content text
|
||||||
|
post_id integer
|
||||||
|
user_id integer
|
||||||
|
created_at timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relationships
|
||||||
|
Ref: posts.user_id > users.id // Each post belongs to one user
|
||||||
|
Ref: comments.post_id > posts.id // Each comment belongs to one post
|
||||||
|
Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||||
|
|
||||||
|
const [dbmlContent, setDBMLContent] = useState<string>(initialDBML);
|
||||||
|
const { closeImportDBMLDialog } = useDialog();
|
||||||
|
const [errorMessage, setErrorMessage] = useState<string | undefined>();
|
||||||
|
const { effectiveTheme } = useTheme();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const {
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
} = useChartDB();
|
||||||
|
const { reorderTables } = useCanvas();
|
||||||
|
const [reorder, setReorder] = useState(false);
|
||||||
|
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
|
||||||
|
const decorationsCollection =
|
||||||
|
useRef<monaco.editor.IEditorDecorationsCollection>();
|
||||||
|
|
||||||
|
const handleEditorDidMount = (
|
||||||
|
editor: monaco.editor.IStandaloneCodeEditor
|
||||||
|
) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
decorationsCollection.current = editor.createDecorationsCollection();
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reorder) {
|
||||||
|
reorderTables({
|
||||||
|
updateHistory: false,
|
||||||
|
});
|
||||||
|
setReorder(false);
|
||||||
|
}
|
||||||
|
}, [reorder, reorderTables]);
|
||||||
|
|
||||||
|
const highlightErrorLine = useCallback((error: DBMLError) => {
|
||||||
|
if (!editorRef.current) return;
|
||||||
|
|
||||||
|
const model = editorRef.current.getModel();
|
||||||
|
if (!model) return;
|
||||||
|
|
||||||
|
const decorations = [
|
||||||
|
{
|
||||||
|
range: new monaco.Range(
|
||||||
|
error.line,
|
||||||
|
1,
|
||||||
|
error.line,
|
||||||
|
model.getLineMaxColumn(error.line)
|
||||||
|
),
|
||||||
|
options: {
|
||||||
|
isWholeLine: true,
|
||||||
|
className: 'dbml-error-line',
|
||||||
|
glyphMarginClassName: 'dbml-error-glyph',
|
||||||
|
hoverMessage: { value: error.message },
|
||||||
|
overviewRuler: {
|
||||||
|
color: '#ff0000',
|
||||||
|
position: monaco.editor.OverviewRulerLane.Right,
|
||||||
|
darkColor: '#ff0000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
decorationsCollection.current?.set(decorations);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const clearDecorations = useCallback(() => {
|
||||||
|
decorationsCollection.current?.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const validateDBML = useCallback(
|
||||||
|
async (content: string) => {
|
||||||
|
// Clear previous errors
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
clearDecorations();
|
||||||
|
|
||||||
|
if (!content.trim()) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parser = new Parser();
|
||||||
|
parser.parse(content, 'dbml');
|
||||||
|
} catch (e) {
|
||||||
|
const parsedError = parseDBMLError(e);
|
||||||
|
if (parsedError) {
|
||||||
|
setErrorMessage(
|
||||||
|
t('import_dbml_dialog.error.description') +
|
||||||
|
` (1 error found - in line ${parsedError.line})`
|
||||||
|
);
|
||||||
|
highlightErrorLine(parsedError);
|
||||||
|
} else {
|
||||||
|
setErrorMessage(
|
||||||
|
e instanceof Error ? e.message : JSON.stringify(e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[clearDecorations, highlightErrorLine, t]
|
||||||
|
);
|
||||||
|
|
||||||
|
const debouncedValidateRef = useRef<((value: string) => void) | null>(null);
|
||||||
|
|
||||||
|
// Set up debounced validation
|
||||||
|
useEffect(() => {
|
||||||
|
debouncedValidateRef.current = debounce((value: string) => {
|
||||||
|
validateDBML(value);
|
||||||
|
}, 500);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
debouncedValidateRef.current = null;
|
||||||
|
};
|
||||||
|
}, [validateDBML]);
|
||||||
|
|
||||||
|
// Trigger validation when content changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (debouncedValidateRef.current) {
|
||||||
|
debouncedValidateRef.current(dbmlContent);
|
||||||
|
}
|
||||||
|
}, [dbmlContent]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!dialog.open) {
|
||||||
|
setErrorMessage(undefined);
|
||||||
|
clearDecorations();
|
||||||
|
setDBMLContent(initialDBML);
|
||||||
|
}
|
||||||
|
}, [dialog.open, initialDBML, clearDecorations]);
|
||||||
|
|
||||||
|
const handleImport = useCallback(async () => {
|
||||||
|
if (!dbmlContent.trim() || errorMessage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||||
|
const tableIdsToRemove = tables
|
||||||
|
.filter((table) =>
|
||||||
|
importedDiagram.tables?.some(
|
||||||
|
(t) =>
|
||||||
|
t.name === table.name && t.schema === table.schema
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((table) => table.id);
|
||||||
|
// Find relationships that need to be removed
|
||||||
|
const relationshipIdsToRemove = relationships
|
||||||
|
.filter((relationship) => {
|
||||||
|
const sourceTable = tables.find(
|
||||||
|
(table) => table.id === relationship.sourceTableId
|
||||||
|
);
|
||||||
|
const targetTable = tables.find(
|
||||||
|
(table) => table.id === relationship.targetTableId
|
||||||
|
);
|
||||||
|
if (!sourceTable || !targetTable) return true;
|
||||||
|
const replacementSourceTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === sourceTable.name &&
|
||||||
|
table.schema === sourceTable.schema
|
||||||
|
);
|
||||||
|
const replacementTargetTable = importedDiagram.tables?.find(
|
||||||
|
(table) =>
|
||||||
|
table.name === targetTable.name &&
|
||||||
|
table.schema === targetTable.schema
|
||||||
|
);
|
||||||
|
return replacementSourceTable || replacementTargetTable;
|
||||||
|
})
|
||||||
|
.map((relationship) => relationship.id);
|
||||||
|
|
||||||
|
// Remove existing items
|
||||||
|
await Promise.all([
|
||||||
|
removeTables(tableIdsToRemove, { updateHistory: false }),
|
||||||
|
removeRelationships(relationshipIdsToRemove, {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Add new items
|
||||||
|
await Promise.all([
|
||||||
|
addTables(importedDiagram.tables ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
addRelationships(importedDiagram.relationships ?? [], {
|
||||||
|
updateHistory: false,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
setReorder(true);
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
title: t('import_dbml_dialog.error.title'),
|
||||||
|
variant: 'destructive',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
<div>{t('import_dbml_dialog.error.description')}</div>
|
||||||
|
{e instanceof Error ? e.message : JSON.stringify(e)}
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
dbmlContent,
|
||||||
|
closeImportDBMLDialog,
|
||||||
|
tables,
|
||||||
|
relationships,
|
||||||
|
removeTables,
|
||||||
|
removeRelationships,
|
||||||
|
addTables,
|
||||||
|
addRelationships,
|
||||||
|
errorMessage,
|
||||||
|
toast,
|
||||||
|
setReorder,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
{...dialog}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
closeImportDBMLDialog();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[80vh] max-h-screen flex-col"
|
||||||
|
showClose
|
||||||
|
>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.example_title')
|
||||||
|
: t('import_dbml_dialog.title')}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t('import_dbml_dialog.description')}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogInternalContent>
|
||||||
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<Editor
|
||||||
|
value={dbmlContent}
|
||||||
|
onChange={(value) => setDBMLContent(value || '')}
|
||||||
|
language="dbml"
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
theme={
|
||||||
|
effectiveTheme === 'dark'
|
||||||
|
? 'dbml-dark'
|
||||||
|
: 'dbml-light'
|
||||||
|
}
|
||||||
|
beforeMount={setupDBMLLanguage}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
glyphMargin: true,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'visible',
|
||||||
|
horizontal: 'visible',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className="size-full"
|
||||||
|
/>
|
||||||
|
</Suspense>
|
||||||
|
</DialogInternalContent>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="flex w-full items-center justify-between">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<DialogClose asChild>
|
||||||
|
<Button variant="secondary">
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.skip_and_empty')
|
||||||
|
: t('import_dbml_dialog.cancel')}
|
||||||
|
</Button>
|
||||||
|
</DialogClose>
|
||||||
|
{errorMessage ? (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<AlertCircle className="size-4 text-destructive" />
|
||||||
|
|
||||||
|
<span className="text-xs text-destructive">
|
||||||
|
{errorMessage ||
|
||||||
|
t(
|
||||||
|
'import_dbml_dialog.error.description'
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
disabled={!dbmlContent.trim() || !!errorMessage}
|
||||||
|
>
|
||||||
|
{withCreateEmptyDiagram
|
||||||
|
? t('import_dbml_dialog.show_example')
|
||||||
|
: t('import_dbml_dialog.import')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -22,15 +22,19 @@ import { useConfig } from '@/hooks/use-config';
|
|||||||
import { useDialog } from '@/hooks/use-dialog';
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
import { useStorage } from '@/hooks/use-storage';
|
import { useStorage } from '@/hooks/use-storage';
|
||||||
import type { Diagram } from '@/lib/domain/diagram';
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||||
|
import { useDebounce } from '@/hooks/use-debounce';
|
||||||
|
|
||||||
export interface OpenDiagramDialogProps extends BaseDialogProps {}
|
export interface OpenDiagramDialogProps extends BaseDialogProps {
|
||||||
|
canClose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||||
dialog,
|
dialog,
|
||||||
|
canClose = true,
|
||||||
}) => {
|
}) => {
|
||||||
const { closeOpenDiagramDialog } = useDialog();
|
const { closeOpenDiagramDialog } = useDialog();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
fetchDiagrams();
|
fetchDiagrams();
|
||||||
}, [listDiagrams, setDiagrams, dialog.open]);
|
}, [listDiagrams, setDiagrams, dialog.open]);
|
||||||
|
|
||||||
const openDiagram = (diagramId: string) => {
|
const openDiagram = useCallback(
|
||||||
|
(diagramId: string) => {
|
||||||
if (diagramId) {
|
if (diagramId) {
|
||||||
updateConfig({ defaultDiagramId: diagramId });
|
updateConfig({ defaultDiagramId: diagramId });
|
||||||
navigate(`/diagrams/${diagramId}`);
|
navigate(`/diagrams/${diagramId}`);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
|
[updateConfig, navigate]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleRowKeyDown = useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLTableRowElement>) => {
|
||||||
|
const element = e.target as HTMLElement;
|
||||||
|
const diagramId = element.getAttribute('data-diagram-id');
|
||||||
|
const selectionIndexAttr = element.getAttribute(
|
||||||
|
'data-selection-index'
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!diagramId || !selectionIndexAttr) return;
|
||||||
|
|
||||||
|
const selectionIndex = parseInt(selectionIndexAttr, 10);
|
||||||
|
|
||||||
|
switch (e.key) {
|
||||||
|
case 'Enter':
|
||||||
|
case ' ':
|
||||||
|
e.preventDefault();
|
||||||
|
openDiagram(diagramId);
|
||||||
|
closeOpenDiagramDialog();
|
||||||
|
break;
|
||||||
|
case 'ArrowDown': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex + 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'ArrowUp': {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
(
|
||||||
|
document.querySelector(
|
||||||
|
`[data-selection-index="${selectionIndex - 1}"]`
|
||||||
|
) as HTMLElement
|
||||||
|
)?.focus();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[openDiagram, closeOpenDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onFocusHandler = useDebounce(
|
||||||
|
(diagramId: string) => setSelectedDiagramId(diagramId),
|
||||||
|
50
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
{...dialog}
|
{...dialog}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (!open) {
|
if (!open && canClose) {
|
||||||
closeOpenDiagramDialog();
|
closeOpenDiagramDialog();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
|
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
||||||
showClose
|
showClose={canClose}
|
||||||
>
|
>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
||||||
@@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{diagrams.map((diagram) => (
|
{diagrams.map((diagram, index) => (
|
||||||
<TableRow
|
<TableRow
|
||||||
key={diagram.id}
|
key={diagram.id}
|
||||||
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
data-state={`${selectedDiagramId === diagram.id ? 'selected' : ''}`}
|
||||||
|
data-diagram-id={diagram.id}
|
||||||
|
data-selection-index={index}
|
||||||
|
tabIndex={0}
|
||||||
|
onFocus={() =>
|
||||||
|
onFocusHandler(diagram.id)
|
||||||
|
}
|
||||||
|
className="focus:bg-accent focus:outline-none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
switch (e.detail) {
|
switch (e.detail) {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -133,11 +197,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={handleRowKeyDown}
|
||||||
>
|
>
|
||||||
<TableCell className="table-cell">
|
<TableCell className="table-cell">
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<DiagramIcon
|
<DiagramIcon
|
||||||
diagram={diagram}
|
databaseType={
|
||||||
|
diagram.databaseType
|
||||||
|
}
|
||||||
|
databaseEdition={
|
||||||
|
diagram.databaseEdition
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -159,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
|||||||
</DialogInternalContent>
|
</DialogInternalContent>
|
||||||
|
|
||||||
<DialogFooter className="flex !justify-between gap-2">
|
<DialogFooter className="flex !justify-between gap-2">
|
||||||
|
{canClose ? (
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button type="button" variant="secondary">
|
<Button type="button" variant="secondary">
|
||||||
{t('open_diagram_dialog.cancel')}
|
{t('open_diagram_dialog.cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
) : (
|
||||||
|
<div />
|
||||||
|
)}
|
||||||
<DialogClose asChild>
|
<DialogClose asChild>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -73,3 +73,68 @@
|
|||||||
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
@apply dark:group-hover:bg-slate-900 group-hover:bg-slate-100 group-hover:ring-[0.5px] rounded-md cursor-pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gradient-background {
|
||||||
|
/* Fallback: Set a background color. */
|
||||||
|
background-color: #f46b24;
|
||||||
|
|
||||||
|
/* Create the gradient. */
|
||||||
|
background-image: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
#2e6579 20%,
|
||||||
|
#4fafca 20%,
|
||||||
|
#4fafca 40%,
|
||||||
|
#6dc630 40%,
|
||||||
|
#6dc630 60%,
|
||||||
|
#f9dc3a 60%,
|
||||||
|
#f9dc3a 80%,
|
||||||
|
#f46b24 80%
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Set the background size and repeat properties. */
|
||||||
|
background-size: 100%;
|
||||||
|
background-repeat: repeat;
|
||||||
|
|
||||||
|
/* Use the text as a mask for the background. */
|
||||||
|
/* This will show the gradient as a text color rather than element bg. */
|
||||||
|
/* -webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent; */
|
||||||
|
|
||||||
|
/* Animate the text when loading the element. */
|
||||||
|
/* This animates it on page load and when hovering out. */
|
||||||
|
animation: rainbow-text-simple-animation-rev 0.75s ease forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gradient-background:hover {
|
||||||
|
animation: rainbow-text-simple-animation 0.5s ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dbml-error-line {
|
||||||
|
background-color: rgba(255, 0, 0, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rainbow-text-simple-animation-rev {
|
||||||
|
0% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Move the background and make it larger. */
|
||||||
|
/* Animation shown when hovering over the text. */
|
||||||
|
@keyframes rainbow-text-simple-animation {
|
||||||
|
0% {
|
||||||
|
background-size: 100%;
|
||||||
|
}
|
||||||
|
80% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-size: 650%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
4
src/hooks/use-canvas.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { canvasContext } from '@/context/canvas-context/canvas-context';
|
||||||
|
|
||||||
|
export const useCanvas = () => useContext(canvasContext);
|
||||||
47
src/hooks/use-debounce-v2.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useRef, useCallback } from 'react';
|
||||||
|
import { debounce as utilsDebounce } from '@/lib/utils';
|
||||||
|
|
||||||
|
interface DebouncedFunction {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
(...args: any[]): void;
|
||||||
|
cancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A hook that returns a debounced version of the provided function.
|
||||||
|
* The debounced function will only be called after the specified delay
|
||||||
|
* has passed without the function being called again.
|
||||||
|
*
|
||||||
|
* @param callback The function to debounce
|
||||||
|
* @param delay The delay in milliseconds
|
||||||
|
* @returns A debounced version of the callback
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
export function useDebounce<T extends (...args: any[]) => any>(
|
||||||
|
callback: T,
|
||||||
|
delay: number
|
||||||
|
): (...args: Parameters<T>) => void {
|
||||||
|
// Use a ref to store the debounced function
|
||||||
|
const debouncedFnRef = useRef<DebouncedFunction>();
|
||||||
|
|
||||||
|
// Update the debounced function when dependencies change
|
||||||
|
useEffect(() => {
|
||||||
|
// Create the debounced function
|
||||||
|
debouncedFnRef.current = utilsDebounce(callback, delay);
|
||||||
|
|
||||||
|
// Clean up when component unmounts or dependencies change
|
||||||
|
return () => {
|
||||||
|
if (debouncedFnRef.current?.cancel) {
|
||||||
|
debouncedFnRef.current.cancel();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [callback, delay]);
|
||||||
|
|
||||||
|
// Create a stable callback that uses the ref
|
||||||
|
const debouncedCallback = useCallback((...args: Parameters<T>) => {
|
||||||
|
debouncedFnRef.current?.(...args);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return debouncedCallback;
|
||||||
|
}
|
||||||
21
src/hooks/use-debounce.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useCallback, useRef } from 'react';
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type AnyFunction = (...args: any[]) => any;
|
||||||
|
|
||||||
|
export const useDebounce = <T extends AnyFunction>(
|
||||||
|
func: T,
|
||||||
|
delay: number
|
||||||
|
): ((...args: Parameters<T>) => void) => {
|
||||||
|
const inDebounce = useRef<NodeJS.Timeout>();
|
||||||
|
|
||||||
|
const debounce = useCallback(
|
||||||
|
(...args: Parameters<T>) => {
|
||||||
|
clearTimeout(inDebounce.current);
|
||||||
|
inDebounce.current = setTimeout(() => func(...args), delay);
|
||||||
|
},
|
||||||
|
[func, delay]
|
||||||
|
);
|
||||||
|
|
||||||
|
return debounce;
|
||||||
|
};
|
||||||
40
src/hooks/use-export-diagram.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { useCallback, useState } from 'react';
|
||||||
|
import { useDialog } from '@/hooks/use-dialog';
|
||||||
|
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
||||||
|
import { waitFor } from '@/lib/utils';
|
||||||
|
import type { Diagram } from '@/lib/domain/diagram';
|
||||||
|
|
||||||
|
export const useExportDiagram = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const { closeExportDiagramDialog } = useDialog();
|
||||||
|
|
||||||
|
const downloadOutput = useCallback((name: string, dataUrl: string) => {
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.setAttribute('download', `ChartDB(${name}).json`);
|
||||||
|
a.setAttribute('href', dataUrl);
|
||||||
|
a.click();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleExport = useCallback(
|
||||||
|
async ({ diagram }: { diagram: Diagram }) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
await waitFor(1000);
|
||||||
|
try {
|
||||||
|
const json = diagramToJSONOutput(diagram);
|
||||||
|
const blob = new Blob([json], { type: 'application/json' });
|
||||||
|
const dataUrl = URL.createObjectURL(blob);
|
||||||
|
downloadOutput(diagram.name, dataUrl);
|
||||||
|
setIsLoading(false);
|
||||||
|
closeExportDiagramDialog();
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[downloadOutput, closeExportDiagramDialog]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
exportDiagram: handleExport,
|
||||||
|
isExporting: isLoading,
|
||||||
|
};
|
||||||
|
};
|
||||||