Compare commits
89 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 |
@@ -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' },
|
||||
},
|
||||
};
|
||||
133
CHANGELOG.md
@@ -1,5 +1,138 @@
|
||||
# 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)
|
||||
|
||||
|
||||
|
||||
12
Dockerfile
@@ -1,6 +1,9 @@
|
||||
FROM node:22-alpine AS builder
|
||||
|
||||
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
|
||||
|
||||
@@ -10,9 +13,13 @@ RUN npm ci
|
||||
|
||||
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
|
||||
|
||||
# Use a lightweight web server to serve the production build
|
||||
FROM nginx:stable-alpine AS production
|
||||
|
||||
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
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Expose the default port for the Nginx web server
|
||||
EXPOSE 80
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
26
README.md
@@ -68,6 +68,7 @@ ChartDB is currently in Public Beta. Star and watch this repository to get notif
|
||||
- ✅ SQL Server
|
||||
- ✅ MariaDB
|
||||
- ✅ SQLite
|
||||
- ✅ CockroachDB
|
||||
- ✅ ClickHouse
|
||||
|
||||
## Getting Started
|
||||
@@ -106,8 +107,33 @@ docker build -t 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`.
|
||||
|
||||
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
|
||||
|
||||
1. Go to [ChartDB.io](https://chartdb.io?ref=github_readme_2)
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "new-york",
|
||||
"rsc": false,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/globals.css",
|
||||
"baseColor": "slate",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "src/components",
|
||||
"utils": "src/lib/utils",
|
||||
"ui": "src/components/ui",
|
||||
"lib": "src/lib",
|
||||
"hooks": "src/hooks"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,12 @@ server {
|
||||
|
||||
location /config.js {
|
||||
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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# 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
|
||||
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',
|
||||
},
|
||||
},
|
||||
];
|
||||
20759
package-lock.json
generated
23
package.json
@@ -1,18 +1,19 @@
|
||||
{
|
||||
"name": "chartdb",
|
||||
"private": true,
|
||||
"version": "1.4.0",
|
||||
"version": "1.9.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"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",
|
||||
"preview": "vite preview",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/openai": "^0.0.51",
|
||||
"@dbml/core": "^3.9.5",
|
||||
"@dnd-kit/sortable": "^8.0.0",
|
||||
"@monaco-editor/react": "^4.6.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
@@ -28,10 +29,10 @@
|
||||
"@radix-ui/react-label": "^2.1.0",
|
||||
"@radix-ui/react-menubar": "^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-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-toast": "^1.2.1",
|
||||
"@radix-ui/react-toggle": "^1.1.0",
|
||||
@@ -60,7 +61,7 @@
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-resizable-panels": "^2.0.22",
|
||||
"react-responsive": "^10.0.0",
|
||||
"react-router-dom": "^6.26.0",
|
||||
"react-router-dom": "^7.1.1",
|
||||
"react-use": "^17.5.1",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
@@ -69,22 +70,26 @@
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/compat": "^1.2.4",
|
||||
"@eslint/eslintrc": "^3.2.0",
|
||||
"@eslint/js": "^9.16.0",
|
||||
"@types/node": "^22.1.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^7.15.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.18.0",
|
||||
"@typescript-eslint/parser": "^8.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint": "^9.16.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-css-modules": "^2.12.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.9.0",
|
||||
"eslint-plugin-prettier": "^5.2.1",
|
||||
"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-tailwindcss": "^3.17.4",
|
||||
"globals": "^15.13.0",
|
||||
"husky": "^9.1.5",
|
||||
"postcss": "^8.4.40",
|
||||
"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/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 |
@@ -12,6 +12,14 @@ import { DarkTheme } from './themes/dark';
|
||||
import { LightTheme } from './themes/light';
|
||||
import './config.ts';
|
||||
|
||||
export const Editor = lazy(() =>
|
||||
import('./code-editor').then((module) => ({
|
||||
default: module.Editor,
|
||||
}))
|
||||
);
|
||||
|
||||
type EditorType = typeof Editor;
|
||||
|
||||
export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
@@ -19,14 +27,9 @@ export interface CodeSnippetProps {
|
||||
loading?: boolean;
|
||||
autoScroll?: 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(
|
||||
({
|
||||
className,
|
||||
@@ -35,6 +38,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
language = 'sql',
|
||||
autoScroll = false,
|
||||
isComplete = true,
|
||||
editorProps,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const monaco = useMonaco();
|
||||
@@ -83,7 +87,7 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setIsCopied(true);
|
||||
} catch (error) {
|
||||
} catch {
|
||||
setIsCopied(false);
|
||||
toast({
|
||||
title: t('copy_to_clipboard_toast.failed.title'),
|
||||
@@ -144,27 +148,32 @@ export const CodeSnippet: React.FC<CodeSnippetProps> = React.memo(
|
||||
language={language}
|
||||
loading={<Spinner />}
|
||||
theme={effectiveTheme}
|
||||
{...editorProps}
|
||||
options={{
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
readOnly: true,
|
||||
automaticLayout: true,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
alwaysConsumeMouseWheel: false,
|
||||
},
|
||||
scrollBeyondLastLine: false,
|
||||
renderValidationDecorations: 'off',
|
||||
lineDecorationsWidth: 0,
|
||||
overviewRulerBorder: false,
|
||||
overviewRulerLanes: 0,
|
||||
hideCursorInOverviewRuler: true,
|
||||
contextmenu: false,
|
||||
...editorProps?.options,
|
||||
guides: {
|
||||
indentation: false,
|
||||
...editorProps?.options?.guides,
|
||||
},
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
alwaysConsumeMouseWheel: false,
|
||||
...editorProps?.options?.scrollbar,
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
...editorProps?.options?.minimap,
|
||||
},
|
||||
contextmenu: false,
|
||||
}}
|
||||
/>
|
||||
{!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 type { Diagram } from '@/lib/domain/diagram';
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip/tooltip';
|
||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import {
|
||||
databaseEditionToImageMap,
|
||||
databaseEditionToLabelMap,
|
||||
@@ -9,39 +9,44 @@ import {
|
||||
databaseSecondaryLogoMap,
|
||||
databaseTypeToLabelMap,
|
||||
} from '@/lib/databases';
|
||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
export interface DiagramIconProps {
|
||||
diagram: Diagram;
|
||||
export interface DiagramIconProps
|
||||
extends React.ComponentPropsWithoutRef<'div'> {
|
||||
databaseType: DatabaseType;
|
||||
databaseEdition?: DatabaseEdition;
|
||||
imgClassName?: string;
|
||||
}
|
||||
|
||||
export const DiagramIcon = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipTrigger>,
|
||||
DiagramIconProps
|
||||
>(({ diagram }, ref) =>
|
||||
diagram.databaseEdition ? (
|
||||
>(({ databaseType, databaseEdition, className, imgClassName }, ref) =>
|
||||
databaseEdition ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="mr-1" ref={ref}>
|
||||
<TooltipTrigger className={cn('mr-1', className)} ref={ref} asChild>
|
||||
<img
|
||||
src={databaseEditionToImageMap[diagram.databaseEdition]}
|
||||
className="h-5 max-w-fit rounded-full"
|
||||
src={databaseEditionToImageMap[databaseEdition]}
|
||||
className={cn('h-5 max-w-fit rounded-full', imgClassName)}
|
||||
alt="database"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{databaseEditionToLabelMap[diagram.databaseEdition]}
|
||||
{databaseEditionToLabelMap[databaseEdition]}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip>
|
||||
<TooltipTrigger className="mr-2" ref={ref}>
|
||||
<TooltipTrigger className={cn('mr-2', className)} ref={ref} asChild>
|
||||
<img
|
||||
src={databaseSecondaryLogoMap[diagram.databaseType]}
|
||||
className="h-5 max-w-fit"
|
||||
src={databaseSecondaryLogoMap[databaseType]}
|
||||
className={cn('h-5 max-w-fit', imgClassName)}
|
||||
alt="database"
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{databaseTypeToLabelMap[diagram.databaseType]}
|
||||
{databaseTypeToLabelMap[databaseType]}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
@@ -117,7 +117,10 @@ const DialogInternalContent = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ScrollArea
|
||||
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}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -1,30 +1,66 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import EmptyStateImage from '@/assets/empty_state.png';
|
||||
import EmptyStateImageDark from '@/assets/empty_state_dark.png';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
|
||||
export interface EmptyStateProps {
|
||||
title: string;
|
||||
description: string;
|
||||
imageClassName?: string;
|
||||
titleClassName?: string;
|
||||
descriptionClassName?: string;
|
||||
}
|
||||
|
||||
export const EmptyState = forwardRef<
|
||||
HTMLDivElement,
|
||||
React.HTMLAttributes<HTMLDivElement> & EmptyStateProps
|
||||
>(({ title, description, className }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center justify-center space-y-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img src={EmptyStateImage} alt="Empty state" className="w-32" />
|
||||
<Label className="text-base">{title}</Label>
|
||||
<Label className="text-sm font-normal text-muted-foreground">
|
||||
{description}
|
||||
</Label>
|
||||
</div>
|
||||
));
|
||||
>(
|
||||
(
|
||||
{
|
||||
title,
|
||||
description,
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
imageClassName,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const { effectiveTheme } = useTheme();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'flex flex-1 flex-col items-center justify-center space-y-1',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<img
|
||||
src={
|
||||
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}
|
||||
</Label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
EmptyState.displayName = 'EmptyState';
|
||||
|
||||
@@ -24,12 +24,19 @@ export interface SelectBoxOption {
|
||||
value: string;
|
||||
label: string;
|
||||
description?: string;
|
||||
regex?: string;
|
||||
extractRegex?: RegExp;
|
||||
}
|
||||
|
||||
export interface SelectBoxProps {
|
||||
options: SelectBoxOption[];
|
||||
value?: string[] | string;
|
||||
onChange?: (values: string[] | string) => void;
|
||||
valueSuffix?: string;
|
||||
optionSuffix?: (option: SelectBoxOption) => string;
|
||||
onChange?: (
|
||||
values: string[] | string,
|
||||
regexMatches?: string[] | string
|
||||
) => void;
|
||||
placeholder?: string;
|
||||
inputPlaceholder?: string;
|
||||
emptyPlaceholder?: string;
|
||||
@@ -55,10 +62,12 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
className,
|
||||
options,
|
||||
value,
|
||||
valueSuffix,
|
||||
onChange,
|
||||
multiple,
|
||||
oneLine,
|
||||
selectAll,
|
||||
optionSuffix,
|
||||
deselectAll,
|
||||
clearText,
|
||||
showClear,
|
||||
@@ -86,7 +95,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
);
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(selectedValue: string) => {
|
||||
(selectedValue: string, regexMatches?: string[]) => {
|
||||
if (multiple) {
|
||||
const newValue =
|
||||
value?.includes(selectedValue) && Array.isArray(value)
|
||||
@@ -94,7 +103,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
: [...(value ?? []), selectedValue];
|
||||
onChange?.(newValue);
|
||||
} else {
|
||||
onChange?.(selectedValue);
|
||||
onChange?.(selectedValue, regexMatches);
|
||||
setIsOpen(false);
|
||||
}
|
||||
},
|
||||
@@ -199,6 +208,7 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
(opt) => opt.value === value
|
||||
)?.label
|
||||
}
|
||||
{valueSuffix ? valueSuffix : ''}
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
@@ -239,11 +249,22 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
align="center"
|
||||
>
|
||||
<Command
|
||||
filter={(value, search) =>
|
||||
value.toLowerCase().includes(search.toLowerCase())
|
||||
filter={(value, search, keywords) => {
|
||||
if (
|
||||
keywords?.length &&
|
||||
keywords.some((keyword) =>
|
||||
new RegExp(keyword).test(search)
|
||||
)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return value
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase())
|
||||
? 1
|
||||
: 0
|
||||
}
|
||||
: 0;
|
||||
}}
|
||||
>
|
||||
<div className="relative">
|
||||
<CommandInput
|
||||
@@ -302,14 +323,36 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
const isSelected =
|
||||
Array.isArray(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 (
|
||||
<CommandItem
|
||||
className="flex items-center"
|
||||
key={option.value}
|
||||
keywords={
|
||||
option.regex
|
||||
? [option.regex]
|
||||
: undefined
|
||||
}
|
||||
// value={option.value}
|
||||
onSelect={() =>
|
||||
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">
|
||||
<span>
|
||||
{option.label}
|
||||
{isRegexMatch
|
||||
? searchTerm
|
||||
: option.label}
|
||||
{!isRegexMatch &&
|
||||
optionSuffix
|
||||
? optionSuffix(
|
||||
option
|
||||
)
|
||||
: ''}
|
||||
</span>
|
||||
{option.description && (
|
||||
<span className="ml-1 text-xs text-muted-foreground">
|
||||
@@ -337,19 +388,20 @@ export const SelectBox = React.forwardRef<HTMLInputElement, SelectBoxProps>(
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{!multiple &&
|
||||
{((!multiple &&
|
||||
option.value ===
|
||||
value && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
value) ||
|
||||
isRegexMatch) && (
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto',
|
||||
option.value ===
|
||||
value
|
||||
? 'opacity-100'
|
||||
: 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
|
||||
@@ -14,6 +14,7 @@ type ToasterToast = ToastProps & {
|
||||
layout?: 'row' | 'column';
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const actionTypes = {
|
||||
ADD_TOAST: 'ADD_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 }
|
||||
) => Promise<void>;
|
||||
loadDiagram: (diagramId: string) => Promise<Diagram | undefined>;
|
||||
loadDiagramFromData: (diagram: Diagram) => void;
|
||||
updateDiagramUpdatedAt: () => Promise<void>;
|
||||
clearDiagramData: () => Promise<void>;
|
||||
deleteDiagram: () => Promise<void>;
|
||||
@@ -246,6 +247,7 @@ export const chartDBContext = createContext<ChartDBContext>({
|
||||
updateDiagramName: emptyFn,
|
||||
updateDiagramUpdatedAt: emptyFn,
|
||||
loadDiagram: emptyFn,
|
||||
loadDiagramFromData: emptyFn,
|
||||
clearDiagramData: emptyFn,
|
||||
deleteDiagram: emptyFn,
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import { defaultSchemas } from '@/lib/data/default-schemas';
|
||||
import { useEventEmitter } from 'ahooks';
|
||||
import type { DBDependency } from '@/lib/domain/db-dependency';
|
||||
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 {
|
||||
diagram?: Diagram;
|
||||
@@ -30,7 +32,8 @@ export interface ChartDBProviderProps {
|
||||
|
||||
export const ChartDBProvider: React.FC<
|
||||
React.PropsWithChildren<ChartDBProviderProps>
|
||||
> = ({ children, diagram, readonly }) => {
|
||||
> = ({ children, diagram, readonly: readonlyProp }) => {
|
||||
const { hasDiff } = useDiff();
|
||||
let db = useStorage();
|
||||
const events = useEventEmitter<ChartDBEvent>();
|
||||
const { setSchemasFilter, schemasFilter } = useLocalConfig();
|
||||
@@ -53,9 +56,33 @@ export const ChartDBProvider: React.FC<
|
||||
const [dependencies, setDependencies] = useState<DBDependency[]>(
|
||||
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 readonly = useMemo(
|
||||
() => readonlyProp ?? hasDiff ?? false,
|
||||
[readonlyProp, hasDiff]
|
||||
);
|
||||
|
||||
if (readonly) {
|
||||
db = storageInitialValue;
|
||||
}
|
||||
@@ -1336,15 +1363,9 @@ 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) {
|
||||
const loadDiagramFromData: ChartDBContext['loadDiagramFromData'] =
|
||||
useCallback(
|
||||
async (diagram) => {
|
||||
setDiagramId(diagram.id);
|
||||
setDiagramName(diagram.name);
|
||||
setDatabaseType(diagram.databaseType);
|
||||
@@ -1356,23 +1377,36 @@ export const ChartDBProvider: React.FC<
|
||||
setDiagramUpdatedAt(diagram.updatedAt);
|
||||
|
||||
events.emit({ action: 'load_diagram', data: { diagram } });
|
||||
},
|
||||
[
|
||||
setDiagramId,
|
||||
setDiagramName,
|
||||
setDatabaseType,
|
||||
setDatabaseEdition,
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
events,
|
||||
]
|
||||
);
|
||||
|
||||
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,
|
||||
setDiagramId,
|
||||
setDiagramName,
|
||||
setDatabaseType,
|
||||
setDatabaseEdition,
|
||||
setTables,
|
||||
setRelationships,
|
||||
setDependencies,
|
||||
setDiagramCreatedAt,
|
||||
setDiagramUpdatedAt,
|
||||
events,
|
||||
]
|
||||
[db, loadDiagramFromData]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -1393,6 +1427,7 @@ export const ChartDBProvider: React.FC<
|
||||
updateDiagramId,
|
||||
updateDiagramName,
|
||||
loadDiagram,
|
||||
loadDiagramFromData,
|
||||
updateDatabaseType,
|
||||
updateDatabaseEdition,
|
||||
clearDiagramData,
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { createContext } from 'react';
|
||||
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 { ImportDatabaseDialogProps } from '@/dialogs/import-database-dialog/import-database-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 { ExportDiagramDialogProps } from '@/dialogs/export-diagram-dialog/export-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 {
|
||||
// Create diagram dialog
|
||||
@@ -14,19 +16,19 @@ export interface DialogContext {
|
||||
closeCreateDiagramDialog: () => void;
|
||||
|
||||
// Open diagram dialog
|
||||
openOpenDiagramDialog: () => void;
|
||||
openOpenDiagramDialog: (
|
||||
params?: Omit<OpenDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeOpenDiagramDialog: () => void;
|
||||
|
||||
// Export SQL dialog
|
||||
openExportSQLDialog: (params: Omit<ExportSQLDialogProps, 'dialog'>) => void;
|
||||
closeExportSQLDialog: () => void;
|
||||
|
||||
// Alert dialog
|
||||
showAlert: (params: BaseAlertDialogProps) => void;
|
||||
closeAlert: () => void;
|
||||
|
||||
// Create relationship dialog
|
||||
openCreateRelationshipDialog: () => void;
|
||||
openCreateRelationshipDialog: (
|
||||
params?: Omit<CreateRelationshipDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeCreateRelationshipDialog: () => void;
|
||||
|
||||
// Import database dialog
|
||||
@@ -62,6 +64,12 @@ export interface DialogContext {
|
||||
params: Omit<ImportDiagramDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeImportDiagramDialog: () => void;
|
||||
|
||||
// Import DBML dialog
|
||||
openImportDBMLDialog: (
|
||||
params?: Omit<ImportDBMLDialogProps, 'dialog'>
|
||||
) => void;
|
||||
closeImportDBMLDialog: () => void;
|
||||
}
|
||||
|
||||
export const dialogContext = createContext<DialogContext>({
|
||||
@@ -71,8 +79,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeOpenDiagramDialog: emptyFn,
|
||||
openExportSQLDialog: emptyFn,
|
||||
closeExportSQLDialog: emptyFn,
|
||||
closeAlert: emptyFn,
|
||||
showAlert: emptyFn,
|
||||
closeCreateRelationshipDialog: emptyFn,
|
||||
openCreateRelationshipDialog: emptyFn,
|
||||
openImportDatabaseDialog: emptyFn,
|
||||
@@ -87,4 +93,6 @@ export const dialogContext = createContext<DialogContext>({
|
||||
closeExportDiagramDialog: emptyFn,
|
||||
openImportDiagramDialog: emptyFn,
|
||||
closeImportDiagramDialog: emptyFn,
|
||||
openImportDBMLDialog: emptyFn,
|
||||
closeImportDBMLDialog: emptyFn,
|
||||
});
|
||||
|
||||
@@ -2,12 +2,12 @@ import React, { useCallback, useState } from 'react';
|
||||
import type { DialogContext } from './dialog-context';
|
||||
import { dialogContext } from './dialog-context';
|
||||
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 type { ExportSQLDialogProps } 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 type { BaseAlertDialogProps } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import { BaseAlertDialog } from '@/dialogs/base-alert-dialog/base-alert-dialog';
|
||||
import type { CreateRelationshipDialogProps } 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 { 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 { ExportDiagramDialog } from '@/dialogs/export-diagram-dialog/export-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> = ({
|
||||
children,
|
||||
}) => {
|
||||
const [openNewDiagramDialog, setOpenNewDiagramDialog] = 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] =
|
||||
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);
|
||||
|
||||
// Export image dialog
|
||||
@@ -88,7 +112,7 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
[setOpenTableSchemaDialog]
|
||||
);
|
||||
|
||||
// Export image dialog
|
||||
// Export diagram dialog
|
||||
const [openExportDiagramDialog, setOpenExportDiagramDialog] =
|
||||
useState(false);
|
||||
|
||||
@@ -96,35 +120,22 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const [openImportDiagramDialog, setOpenImportDiagramDialog] =
|
||||
useState(false);
|
||||
|
||||
// Alert dialog
|
||||
const [showAlert, setShowAlert] = useState(false);
|
||||
const [alertParams, setAlertParams] = useState<BaseAlertDialogProps>({
|
||||
title: '',
|
||||
});
|
||||
const showAlertHandler: DialogContext['showAlert'] = useCallback(
|
||||
(params) => {
|
||||
setAlertParams(params);
|
||||
setShowAlert(true);
|
||||
},
|
||||
[setShowAlert, setAlertParams]
|
||||
);
|
||||
const closeAlertHandler = useCallback(() => {
|
||||
setShowAlert(false);
|
||||
}, [setShowAlert]);
|
||||
// Import DBML dialog
|
||||
const [openImportDBMLDialog, setOpenImportDBMLDialog] = useState(false);
|
||||
const [importDBMLDialogParams, setImportDBMLDialogParams] =
|
||||
useState<Omit<ImportDBMLDialogProps, 'dialog'>>();
|
||||
|
||||
return (
|
||||
<dialogContext.Provider
|
||||
value={{
|
||||
openCreateDiagramDialog: () => setOpenNewDiagramDialog(true),
|
||||
closeCreateDiagramDialog: () => setOpenNewDiagramDialog(false),
|
||||
openOpenDiagramDialog: () => setOpenOpenDiagramDialog(true),
|
||||
openOpenDiagramDialog: openOpenDiagramDialogHandler,
|
||||
closeOpenDiagramDialog: () => setOpenOpenDiagramDialog(false),
|
||||
openExportSQLDialog: openExportSQLDialogHandler,
|
||||
closeExportSQLDialog: () => setOpenExportSQLDialog(false),
|
||||
showAlert: showAlertHandler,
|
||||
closeAlert: closeAlertHandler,
|
||||
openCreateRelationshipDialog: () =>
|
||||
setOpenCreateRelationshipDialog(true),
|
||||
openCreateRelationshipDialog:
|
||||
openCreateRelationshipDialogHandler,
|
||||
closeCreateRelationshipDialog: () =>
|
||||
setOpenCreateRelationshipDialog(false),
|
||||
openImportDatabaseDialog: openImportDatabaseDialogHandler,
|
||||
@@ -142,18 +153,26 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
openImportDiagramDialog: () => setOpenImportDiagramDialog(true),
|
||||
closeImportDiagramDialog: () =>
|
||||
setOpenImportDiagramDialog(false),
|
||||
openImportDBMLDialog: (params) => {
|
||||
setImportDBMLDialogParams(params);
|
||||
setOpenImportDBMLDialog(true);
|
||||
},
|
||||
closeImportDBMLDialog: () => setOpenImportDBMLDialog(false),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
<CreateDiagramDialog dialog={{ open: openNewDiagramDialog }} />
|
||||
<OpenDiagramDialog dialog={{ open: openOpenDiagramDialog }} />
|
||||
<OpenDiagramDialog
|
||||
dialog={{ open: openOpenDiagramDialog }}
|
||||
{...openDiagramDialogParams}
|
||||
/>
|
||||
<ExportSQLDialog
|
||||
dialog={{ open: openExportSQLDialog }}
|
||||
{...exportSQLDialogParams}
|
||||
/>
|
||||
<BaseAlertDialog dialog={{ open: showAlert }} {...alertParams} />
|
||||
<CreateRelationshipDialog
|
||||
dialog={{ open: openCreateRelationshipDialog }}
|
||||
{...createRelationshipDialogParams}
|
||||
/>
|
||||
<ImportDatabaseDialog
|
||||
dialog={{ open: openImportDatabaseDialog }}
|
||||
@@ -170,6 +189,10 @@ export const DialogProvider: React.FC<React.PropsWithChildren> = ({
|
||||
/>
|
||||
<ExportDiagramDialog dialog={{ open: openExportDiagramDialog }} />
|
||||
<ImportDiagramDialog dialog={{ open: openImportDiagramDialog }} />
|
||||
<ImportDBMLDialog
|
||||
dialog={{ open: openImportDBMLDialog }}
|
||||
{...importDBMLDialogParams}
|
||||
/>
|
||||
</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 { exportImageContext } from './export-image-context';
|
||||
import { toJpeg, toPng, toSvg } from 'html-to-image';
|
||||
@@ -6,6 +6,8 @@ import { useReactFlow } from '@xyflow/react';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
|
||||
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> = ({
|
||||
children,
|
||||
@@ -14,6 +16,24 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const { setNodes, getViewport } = useReactFlow();
|
||||
const { effectiveTheme } = useTheme();
|
||||
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(
|
||||
(dataUrl: string, type: ImageType) => {
|
||||
@@ -128,16 +148,22 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
'http://www.w3.org/2000/svg',
|
||||
'rect'
|
||||
);
|
||||
const padding = 2000;
|
||||
backgroundRect.setAttribute('x', String(-viewport.x - padding));
|
||||
backgroundRect.setAttribute('y', String(-viewport.y - padding));
|
||||
const bgPadding = 2000;
|
||||
backgroundRect.setAttribute(
|
||||
'x',
|
||||
String(-viewport.x - bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'y',
|
||||
String(-viewport.y - bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'width',
|
||||
String(reactFlowBounds.width + 2 * padding)
|
||||
String(reactFlowBounds.width + 2 * bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute(
|
||||
'height',
|
||||
String(reactFlowBounds.height + 2 * padding)
|
||||
String(reactFlowBounds.height + 2 * bgPadding)
|
||||
);
|
||||
backgroundRect.setAttribute('fill', 'url(#background-pattern)');
|
||||
tempSvg.appendChild(backgroundRect);
|
||||
@@ -148,27 +174,110 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
try {
|
||||
const dataUrl = await imageCreateFn(viewportElement, {
|
||||
...(type === 'jpeg' || type === 'png'
|
||||
? {
|
||||
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,
|
||||
});
|
||||
// Handle SVG export differently
|
||||
if (type === 'svg') {
|
||||
const dataUrl = await imageCreateFn(viewportElement, {
|
||||
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,
|
||||
});
|
||||
downloadImage(dataUrl, type);
|
||||
return;
|
||||
}
|
||||
|
||||
downloadImage(dataUrl, type);
|
||||
// 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);
|
||||
};
|
||||
});
|
||||
|
||||
// Convert canvas to data URL
|
||||
const finalDataUrl = canvas.toDataURL(
|
||||
type === 'png' ? 'image/png' : 'image/jpeg'
|
||||
);
|
||||
downloadImage(finalDataUrl, type);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
viewportElement.removeChild(tempSvg);
|
||||
hideLoader();
|
||||
@@ -183,6 +292,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setNodes,
|
||||
showLoader,
|
||||
effectiveTheme,
|
||||
logoBase64,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useHistory } from '@/hooks/use-history';
|
||||
import { useDialog } from '@/hooks/use-dialog';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { useLayout } from '@/hooks/use-layout';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
|
||||
export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -17,6 +18,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
const { openOpenDiagramDialog } = useDialog();
|
||||
const { updateDiagramUpdatedAt } = useChartDB();
|
||||
const { toggleSidePanel } = useLayout();
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.REDO].keyCombination,
|
||||
@@ -37,7 +39,7 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.OPEN_DIAGRAM]
|
||||
.keyCombination,
|
||||
openOpenDiagramDialog,
|
||||
() => openOpenDiagramDialog(),
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
@@ -61,6 +63,20 @@ export const KeyboardShortcutsProvider: React.FC<React.PropsWithChildren> = ({
|
||||
},
|
||||
[toggleSidePanel]
|
||||
);
|
||||
useHotkeys(
|
||||
keyboardShortcutsForOS[KeyboardShortcutAction.SHOW_ALL].keyCombination,
|
||||
() => {
|
||||
fitView({
|
||||
duration: 500,
|
||||
padding: 0.1,
|
||||
maxZoom: 0.8,
|
||||
});
|
||||
},
|
||||
{
|
||||
preventDefault: true,
|
||||
},
|
||||
[fitView]
|
||||
);
|
||||
|
||||
return (
|
||||
<keyboardShortcutsContext.Provider value={{}}>
|
||||
|
||||
@@ -6,6 +6,8 @@ export enum KeyboardShortcutAction {
|
||||
OPEN_DIAGRAM = 'open_diagram',
|
||||
SAVE_DIAGRAM = 'save_diagram',
|
||||
TOGGLE_SIDE_PANEL = 'toggle_side_panel',
|
||||
SHOW_ALL = 'show_all',
|
||||
TOGGLE_THEME = 'toggle_theme',
|
||||
}
|
||||
|
||||
export interface KeyboardShortcut {
|
||||
@@ -55,6 +57,20 @@ export const keyboardShortcuts: Record<
|
||||
keyCombinationMac: 'meta+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 {
|
||||
|
||||
@@ -32,6 +32,9 @@ export interface LocalConfigContext {
|
||||
|
||||
showDependenciesOnCanvas: boolean;
|
||||
setShowDependenciesOnCanvas: (showDependenciesOnCanvas: boolean) => void;
|
||||
|
||||
showMiniMapOnCanvas: boolean;
|
||||
setShowMiniMapOnCanvas: (showMiniMapOnCanvas: boolean) => void;
|
||||
}
|
||||
|
||||
export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
@@ -44,7 +47,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
schemasFilter: {},
|
||||
setSchemasFilter: emptyFn,
|
||||
|
||||
showCardinality: false,
|
||||
showCardinality: true,
|
||||
setShowCardinality: emptyFn,
|
||||
|
||||
hideMultiSchemaNotification: false,
|
||||
@@ -58,4 +61,7 @@ export const LocalConfigContext = createContext<LocalConfigContext>({
|
||||
|
||||
showDependenciesOnCanvas: false,
|
||||
setShowDependenciesOnCanvas: emptyFn,
|
||||
|
||||
showMiniMapOnCanvas: false,
|
||||
setShowMiniMapOnCanvas: emptyFn,
|
||||
});
|
||||
|
||||
@@ -11,6 +11,7 @@ const hideMultiSchemaNotificationKey = 'hide_multi_schema_notification';
|
||||
const githubRepoOpenedKey = 'github_repo_opened';
|
||||
const starUsDialogLastOpenKey = 'star_us_dialog_last_open';
|
||||
const showDependenciesOnCanvasKey = 'show_dependencies_on_canvas';
|
||||
const showMiniMapOnCanvasKey = 'show_minimap_on_canvas';
|
||||
|
||||
export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -30,7 +31,7 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
|
||||
const [showCardinality, setShowCardinality] = React.useState<boolean>(
|
||||
(localStorage.getItem(showCardinalityKey) || 'false') === 'true'
|
||||
(localStorage.getItem(showCardinalityKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
const [hideMultiSchemaNotification, setHideMultiSchemaNotification] =
|
||||
@@ -54,6 +55,11 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
'true'
|
||||
);
|
||||
|
||||
const [showMiniMapOnCanvas, setShowMiniMapOnCanvas] =
|
||||
React.useState<boolean>(
|
||||
(localStorage.getItem(showMiniMapOnCanvasKey) || 'true') === 'true'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
starUsDialogLastOpenKey,
|
||||
@@ -95,6 +101,13 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
);
|
||||
}, [showDependenciesOnCanvas]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(
|
||||
showMiniMapOnCanvasKey,
|
||||
showMiniMapOnCanvas.toString()
|
||||
);
|
||||
}, [showMiniMapOnCanvas]);
|
||||
|
||||
return (
|
||||
<LocalConfigContext.Provider
|
||||
value={{
|
||||
@@ -114,6 +127,8 @@ export const LocalConfigProvider: React.FC<React.PropsWithChildren> = ({
|
||||
setStarUsDialogLastOpen,
|
||||
showDependenciesOnCanvas,
|
||||
setShowDependenciesOnCanvas,
|
||||
showMiniMapOnCanvas,
|
||||
setShowMiniMapOnCanvas,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -134,6 +134,20 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
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 () => {
|
||||
const config = await getConfig();
|
||||
|
||||
@@ -282,6 +296,23 @@ export const StorageProvider: React.FC<React.PropsWithChildren> = ({
|
||||
attributes: Partial<Diagram>;
|
||||
}) => {
|
||||
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 (
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import type { EffectiveTheme } from './theme-context';
|
||||
import { ThemeContext } from './theme-context';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
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> = ({
|
||||
children,
|
||||
@@ -29,6 +34,24 @@ export const ThemeProvider: React.FC<React.PropsWithChildren> = ({
|
||||
}
|
||||
}, [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 (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, effectiveTheme }}>
|
||||
{children}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from '@/components/alert-dialog/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 {
|
||||
title: string;
|
||||
@@ -33,7 +33,7 @@ export const BaseAlertDialog: React.FC<BaseAlertDialogProps> = ({
|
||||
content,
|
||||
onClose,
|
||||
}) => {
|
||||
const { closeAlert } = useDialog();
|
||||
const { closeAlert } = useAlert();
|
||||
|
||||
const closeAlertHandler = useCallback(() => {
|
||||
onClose?.();
|
||||
|
||||
@@ -85,6 +85,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
const [showCheckJsonButton, setShowCheckJsonButton] = useState(false);
|
||||
const [isCheckingJson, setIsCheckingJson] = useState(false);
|
||||
|
||||
const [showSSMSInfoDialog, setShowSSMSInfoDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const loadScripts = async () => {
|
||||
const { importMetadataScripts } = await import(
|
||||
@@ -127,6 +129,11 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const inputValue = e.target.value;
|
||||
setScriptResult(inputValue);
|
||||
|
||||
// Automatically open SSMS info when input length is exactly 65535
|
||||
if (inputValue.length === 65535) {
|
||||
setShowSSMSInfoDialog(true);
|
||||
}
|
||||
},
|
||||
[setScriptResult]
|
||||
);
|
||||
@@ -245,7 +252,10 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
{t('new_diagram_dialog.import_database.step_1')}
|
||||
</div>
|
||||
{databaseType === DatabaseType.SQL_SERVER && (
|
||||
<SSMSInfo />
|
||||
<SSMSInfo
|
||||
open={showSSMSInfoDialog}
|
||||
setOpen={setShowSSMSInfoDialog}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{databaseTypeToClientsMap[databaseType].length > 0 ? (
|
||||
@@ -369,6 +379,8 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
showCheckJsonButton,
|
||||
isCheckingJson,
|
||||
handleCheckJson,
|
||||
showSSMSInfoDialog,
|
||||
setShowSSMSInfoDialog,
|
||||
]);
|
||||
|
||||
const renderFooter = useCallback(() => {
|
||||
|
||||
@@ -4,32 +4,55 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from '@/components/hover-card/hover-card';
|
||||
import { Label } from '@/components/label/label';
|
||||
import { Info } from 'lucide-react';
|
||||
import React from 'react';
|
||||
import { Info, X } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useMemo } from 'react';
|
||||
import SSMSInstructions from '@/assets/ssms-instructions.png';
|
||||
import { ZoomableImage } from '@/components/zoomable-image/zoomable-image';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
export interface SSMSInfoProps {}
|
||||
export interface SSMSInfoProps {
|
||||
open?: boolean;
|
||||
setOpen?: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export const SSMSInfo = React.forwardRef<
|
||||
React.ElementRef<typeof HoverCardTrigger>,
|
||||
SSMSInfoProps
|
||||
>((props, ref) => {
|
||||
>(({ open: controlledOpen, setOpen: setControlledOpen }, ref) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
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 (
|
||||
<HoverCard
|
||||
open={open}
|
||||
open={isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
if (controlledOpen) {
|
||||
return;
|
||||
}
|
||||
setOpen(isOpen);
|
||||
}}
|
||||
>
|
||||
<HoverCardTrigger ref={ref} {...props} asChild>
|
||||
<HoverCardTrigger ref={ref} asChild>
|
||||
<div
|
||||
className="flex flex-row items-center gap-1 text-pink-600"
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
setOpen?.(!open);
|
||||
}}
|
||||
>
|
||||
<Info size={14} />
|
||||
@@ -41,13 +64,21 @@ export const SSMSInfo = React.forwardRef<
|
||||
</div>
|
||||
</HoverCardTrigger>
|
||||
<HoverCardContent className="w-80">
|
||||
<div className="flex">
|
||||
<div className="space-y-1">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-start justify-between">
|
||||
<h4 className="text-sm font-semibold">
|
||||
{t(
|
||||
'new_diagram_dialog.import_database.ssms_instructions.title'
|
||||
)}
|
||||
</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">
|
||||
<span className="font-semibold">1. </span>
|
||||
{t(
|
||||
|
||||
@@ -28,7 +28,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||
DatabaseType.GENERIC
|
||||
);
|
||||
const { closeCreateDiagramDialog } = useDialog();
|
||||
const { closeCreateDiagramDialog, openImportDBMLDialog } = useDialog();
|
||||
const { updateConfig } = useConfig();
|
||||
const [scriptResult, setScriptResult] = useState('');
|
||||
const [databaseEdition, setDatabaseEdition] = useState<
|
||||
@@ -104,6 +104,10 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
await updateConfig({ defaultDiagramId: diagram.id });
|
||||
closeCreateDiagramDialog();
|
||||
navigate(`/diagrams/${diagram.id}`);
|
||||
setTimeout(
|
||||
() => openImportDBMLDialog({ withCreateEmptyDiagram: true }),
|
||||
700
|
||||
);
|
||||
}, [
|
||||
databaseType,
|
||||
addDiagram,
|
||||
@@ -112,6 +116,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
navigate,
|
||||
updateConfig,
|
||||
diagramNumber,
|
||||
openImportDBMLDialog,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -12,9 +12,9 @@ export const ExampleOption: React.FC<ExampleOptionProps> = () => {
|
||||
href="/examples"
|
||||
className="col-span-3 text-primary hover:text-primary"
|
||||
>
|
||||
<div className="flex h-8 w-full cursor-pointer flex-row items-center justify-center gap-2 rounded-md border py-3 text-center md:h-12">
|
||||
<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-5 md:size-6" />
|
||||
<LayoutGrid className="size-4" />
|
||||
</div>
|
||||
<div className="flex flex-col-reverse">
|
||||
<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 { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { DatabaseOption } from './database-option';
|
||||
import { ExampleOption } from './example-option';
|
||||
|
||||
import { Button } from '@/components/button/button';
|
||||
import { ChevronDown, ChevronUp } from 'lucide-react';
|
||||
export interface SelectDatabaseContentProps {
|
||||
databaseType: DatabaseType;
|
||||
setDatabaseType: React.Dispatch<React.SetStateAction<DatabaseType>>;
|
||||
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> = ({
|
||||
databaseType,
|
||||
setDatabaseType,
|
||||
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 (
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-4">
|
||||
<ToggleGroup
|
||||
value={databaseType}
|
||||
onValueChange={(value: DatabaseType) => {
|
||||
@@ -30,13 +69,41 @@ export const SelectDatabaseContent: React.FC<SelectDatabaseContentProps> = ({
|
||||
type="single"
|
||||
className="grid grid-flow-row grid-cols-3 gap-6"
|
||||
>
|
||||
<DatabaseOption type={DatabaseType.MYSQL} />
|
||||
<DatabaseOption type={DatabaseType.POSTGRESQL} />
|
||||
<DatabaseOption type={DatabaseType.MARIADB} />
|
||||
<DatabaseOption type={DatabaseType.SQLITE} />
|
||||
<DatabaseOption type={DatabaseType.SQL_SERVER} />
|
||||
<DatabaseOption type={DatabaseType.CLICKHOUSE} />
|
||||
<ExampleOption />
|
||||
{Array.from({ length: TOTAL_SLOTS }).map((_, index) =>
|
||||
currentDatabasesTypes?.[index] ? (
|
||||
<DatabaseOption
|
||||
key={currentDatabasesTypes[index]}
|
||||
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 />
|
||||
</div>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,13 +22,17 @@ import { areFieldTypesCompatible } from '@/lib/data/data-types/data-types';
|
||||
const ErrorMessageRelationshipFieldsNotSameType =
|
||||
'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<
|
||||
CreateRelationshipDialogProps
|
||||
> = ({ dialog }) => {
|
||||
> = ({ dialog, sourceTableId: preSelectedSourceTableId }) => {
|
||||
const { closeCreateRelationshipDialog } = useDialog();
|
||||
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>();
|
||||
const [primaryTableId, setPrimaryTableId] = useState<string | undefined>(
|
||||
preSelectedSourceTableId
|
||||
);
|
||||
const [primaryFieldId, setPrimaryFieldId] = useState<string | undefined>();
|
||||
const [referencedTableId, setReferencedTableId] = useState<
|
||||
string | undefined
|
||||
@@ -43,6 +47,9 @@ export const CreateRelationshipDialog: React.FC<
|
||||
const [canCreateRelationship, setCanCreateRelationship] = useState(false);
|
||||
const { fitView, setEdges } = useReactFlow();
|
||||
const { databaseType } = useChartDB();
|
||||
const [primaryFieldSelectOpen, setPrimaryFieldSelectOpen] = useState(false);
|
||||
const [referencedTableSelectOpen, setReferencedTableSelectOpen] =
|
||||
useState(false);
|
||||
|
||||
const tableOptions = useMemo(() => {
|
||||
return tables.map(
|
||||
@@ -89,8 +96,23 @@ export const CreateRelationshipDialog: React.FC<
|
||||
setReferencedTableId(undefined);
|
||||
setReferencedFieldId(undefined);
|
||||
setErrorMessage('');
|
||||
setPrimaryFieldSelectOpen(false);
|
||||
setReferencedTableSelectOpen(false);
|
||||
}, [dialog.open]);
|
||||
|
||||
useEffect(() => {
|
||||
if (preSelectedSourceTableId) {
|
||||
const table = getTable(preSelectedSourceTableId);
|
||||
if (table) {
|
||||
setPrimaryTableId(preSelectedSourceTableId);
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setPrimaryFieldSelectOpen(true);
|
||||
}, 100);
|
||||
}
|
||||
}, [preSelectedSourceTableId, getTable]);
|
||||
|
||||
useEffect(() => {
|
||||
setCanCreateRelationship(false);
|
||||
setErrorMessage('');
|
||||
@@ -223,8 +245,14 @@ export const CreateRelationshipDialog: React.FC<
|
||||
)}
|
||||
value={primaryTableId}
|
||||
onChange={(value) => {
|
||||
setPrimaryTableId(value as string);
|
||||
setPrimaryFieldId(undefined);
|
||||
const newTableId = value as string;
|
||||
setPrimaryTableId(newTableId);
|
||||
if (
|
||||
newTableId !==
|
||||
preSelectedSourceTableId
|
||||
) {
|
||||
setPrimaryFieldId(undefined);
|
||||
}
|
||||
}}
|
||||
emptyPlaceholder={t(
|
||||
'create_relationship_dialog.no_tables_found'
|
||||
@@ -253,6 +281,8 @@ export const CreateRelationshipDialog: React.FC<
|
||||
'create_relationship_dialog.primary_field_placeholder'
|
||||
)}
|
||||
value={primaryFieldId}
|
||||
open={primaryFieldSelectOpen}
|
||||
onOpenChange={setPrimaryFieldSelectOpen}
|
||||
onChange={(value) =>
|
||||
setPrimaryFieldId(value as string)
|
||||
}
|
||||
@@ -283,6 +313,8 @@ export const CreateRelationshipDialog: React.FC<
|
||||
'create_relationship_dialog.referenced_table_placeholder'
|
||||
)}
|
||||
value={referencedTableId}
|
||||
open={referencedTableSelectOpen}
|
||||
onOpenChange={setReferencedTableSelectOpen}
|
||||
onChange={(value) => {
|
||||
setReferencedTableId(value as string);
|
||||
setReferencedFieldId(undefined);
|
||||
|
||||
@@ -15,11 +15,10 @@ import { SelectBox } from '@/components/select-box/select-box';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { diagramToJSONOutput } from '@/lib/export-import-utils';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { waitFor } from '@/lib/utils';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/alert/alert';
|
||||
import { useExportDiagram } from '@/hooks/use-export-diagram';
|
||||
|
||||
export interface ExportDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -27,44 +26,27 @@ export const ExportDiagramDialog: React.FC<ExportDiagramDialogProps> = ({
|
||||
dialog,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const { diagramName, currentDiagram } = useChartDB();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const { currentDiagram } = useChartDB();
|
||||
const { closeExportDiagramDialog } = useDialog();
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) return;
|
||||
setIsLoading(false);
|
||||
setError(false);
|
||||
}, [dialog.open]);
|
||||
|
||||
const downloadOutput = useCallback(
|
||||
(dataUrl: string) => {
|
||||
const a = document.createElement('a');
|
||||
a.setAttribute('download', `ChartDB(${diagramName}).json`);
|
||||
a.setAttribute('href', dataUrl);
|
||||
a.click();
|
||||
},
|
||||
[diagramName]
|
||||
);
|
||||
const { exportDiagram, isExporting: isLoading } = useExportDiagram();
|
||||
|
||||
const handleExport = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
await waitFor(1000);
|
||||
try {
|
||||
const json = diagramToJSONOutput(currentDiagram);
|
||||
const blob = new Blob([json], { type: 'application/json' });
|
||||
const dataUrl = URL.createObjectURL(blob);
|
||||
downloadOutput(dataUrl);
|
||||
setIsLoading(false);
|
||||
await exportDiagram({ diagram: currentDiagram });
|
||||
closeExportDiagramDialog();
|
||||
} catch (e) {
|
||||
setError(true);
|
||||
setIsLoading(false);
|
||||
|
||||
throw e;
|
||||
}
|
||||
}, [downloadOutput, currentDiagram, closeExportDiagramDialog]);
|
||||
}, [exportDiagram, currentDiagram, closeExportDiagramDialog]);
|
||||
|
||||
const outputTypeOptions: SelectBoxOption[] = useMemo(
|
||||
() =>
|
||||
|
||||
@@ -20,10 +20,12 @@ import {
|
||||
} from '@/lib/data/export-metadata/export-sql-script';
|
||||
import { databaseTypeToLabelMap } from '@/lib/databases';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { shouldShowTablesBySchemaFilter } from '@/lib/domain/db-table';
|
||||
import { Annoyed, Sparkles } from 'lucide-react';
|
||||
import React, { useCallback, useEffect, useRef } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
|
||||
export interface ExportSQLDialogProps extends BaseDialogProps {
|
||||
targetDatabaseType: DatabaseType;
|
||||
@@ -34,7 +36,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
targetDatabaseType,
|
||||
}) => {
|
||||
const { closeExportSQLDialog } = useDialog();
|
||||
const { currentDiagram } = useChartDB();
|
||||
const { currentDiagram, filteredSchemas } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
const [script, setScript] = React.useState<string>();
|
||||
const [error, setError] = React.useState<boolean>(false);
|
||||
@@ -43,17 +45,58 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
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) {
|
||||
return Promise.resolve(exportBaseSQL(currentDiagram));
|
||||
return Promise.resolve(exportBaseSQL(filteredDiagram));
|
||||
} else {
|
||||
return exportSQL(currentDiagram, targetDatabaseType, {
|
||||
return exportSQL(filteredDiagram, targetDatabaseType, {
|
||||
stream: true,
|
||||
onResultStream: (text) =>
|
||||
setScript((prev) => (prev ? prev + text : text)),
|
||||
signal: abortControllerRef.current?.signal,
|
||||
});
|
||||
}
|
||||
}, [targetDatabaseType, currentDiagram]);
|
||||
}, [targetDatabaseType, currentDiagram, filteredSchemas]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dialog.open) {
|
||||
@@ -70,7 +113,7 @@ export const ExportSQLDialog: React.FC<ExportSQLDialogProps> = ({
|
||||
const script = await exportSQLScript();
|
||||
setScript(script);
|
||||
setIsScriptLoading(false);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
setError(true);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import { useRedoUndoStack } from '@/hooks/use-redo-undo-stack';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { useReactFlow } from '@xyflow/react';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useAlert } from '@/context/alert-context/alert-context';
|
||||
|
||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
||||
databaseType: DatabaseType;
|
||||
@@ -21,7 +22,8 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||
dialog,
|
||||
databaseType,
|
||||
}) => {
|
||||
const { closeImportDatabaseDialog, showAlert } = useDialog();
|
||||
const { closeImportDatabaseDialog } = useDialog();
|
||||
const { showAlert } = useAlert();
|
||||
const {
|
||||
tables,
|
||||
relationships,
|
||||
|
||||
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 { useStorage } from '@/hooks/use-storage';
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
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> = ({
|
||||
dialog,
|
||||
canClose = true,
|
||||
}) => {
|
||||
const { closeOpenDiagramDialog } = useDialog();
|
||||
const { t } = useTranslation();
|
||||
@@ -58,24 +62,77 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
fetchDiagrams();
|
||||
}, [listDiagrams, setDiagrams, dialog.open]);
|
||||
|
||||
const openDiagram = (diagramId: string) => {
|
||||
if (diagramId) {
|
||||
updateConfig({ defaultDiagramId: diagramId });
|
||||
navigate(`/diagrams/${diagramId}`);
|
||||
}
|
||||
};
|
||||
const openDiagram = useCallback(
|
||||
(diagramId: string) => {
|
||||
if (diagramId) {
|
||||
updateConfig({ defaultDiagramId: 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 (
|
||||
<Dialog
|
||||
{...dialog}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
if (!open && canClose) {
|
||||
closeOpenDiagramDialog();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
className="flex h-[30rem] max-h-screen w-[90vw] flex-col overflow-y-auto md:w-screen xl:min-w-[55vw]"
|
||||
showClose
|
||||
className="flex h-[30rem] max-h-screen flex-col overflow-y-auto md:min-w-[80vw] xl:min-w-[55vw]"
|
||||
showClose={canClose}
|
||||
>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('open_diagram_dialog.title')}</DialogTitle>
|
||||
@@ -112,10 +169,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{diagrams.map((diagram) => (
|
||||
{diagrams.map((diagram, index) => (
|
||||
<TableRow
|
||||
key={diagram.id}
|
||||
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) => {
|
||||
switch (e.detail) {
|
||||
case 1:
|
||||
@@ -133,11 +197,17 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
);
|
||||
}
|
||||
}}
|
||||
onKeyDown={handleRowKeyDown}
|
||||
>
|
||||
<TableCell className="table-cell">
|
||||
<div className="flex justify-center">
|
||||
<DiagramIcon
|
||||
diagram={diagram}
|
||||
databaseType={
|
||||
diagram.databaseType
|
||||
}
|
||||
databaseEdition={
|
||||
diagram.databaseEdition
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TableCell>
|
||||
@@ -159,11 +229,15 @@ export const OpenDiagramDialog: React.FC<OpenDiagramDialogProps> = ({
|
||||
</DialogInternalContent>
|
||||
|
||||
<DialogFooter className="flex !justify-between gap-2">
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
{t('open_diagram_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{canClose ? (
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="secondary">
|
||||
{t('open_diagram_dialog.cancel')}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<DialogClose asChild>
|
||||
<Button
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.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,
|
||||
};
|
||||
};
|
||||
@@ -22,6 +22,7 @@ import { te, teMetadata } from './locales/te';
|
||||
import { bn, bnMetadata } from './locales/bn';
|
||||
import { gu, guMetadata } from './locales/gu';
|
||||
import { vi, viMetadata } from './locales/vi';
|
||||
import { ar, arMetadata } from './locales/ar';
|
||||
|
||||
export const languages: LanguageMetadata[] = [
|
||||
enMetadata,
|
||||
@@ -44,6 +45,7 @@ export const languages: LanguageMetadata[] = [
|
||||
bnMetadata,
|
||||
guMetadata,
|
||||
viMetadata,
|
||||
arMetadata,
|
||||
];
|
||||
|
||||
const resources = {
|
||||
@@ -67,6 +69,7 @@ const resources = {
|
||||
bn,
|
||||
gu,
|
||||
vi,
|
||||
ar,
|
||||
};
|
||||
|
||||
i18n.use(LanguageDetector)
|
||||
|
||||
430
src/i18n/locales/ar.ts
Normal file
@@ -0,0 +1,430 @@
|
||||
import type { LanguageMetadata, LanguageTranslation } from '../types';
|
||||
|
||||
export const ar: LanguageTranslation = {
|
||||
translation: {
|
||||
menu: {
|
||||
file: {
|
||||
file: 'ملف',
|
||||
new: 'جديد',
|
||||
open: 'فتح',
|
||||
save: 'حفظ',
|
||||
import: 'استيراد قاعدة بيانات',
|
||||
export_sql: 'SQL تصدير',
|
||||
export_as: 'تصدير كـ',
|
||||
delete_diagram: 'حذف الرسم البياني',
|
||||
exit: 'خروج',
|
||||
},
|
||||
edit: {
|
||||
edit: 'تحرير',
|
||||
undo: 'تراجع',
|
||||
redo: 'إعادة',
|
||||
clear: 'مسح',
|
||||
},
|
||||
view: {
|
||||
view: 'عرض',
|
||||
show_sidebar: 'إظهار الشريط الجانبي',
|
||||
hide_sidebar: 'إخفاء الشريط الجانبي',
|
||||
hide_cardinality: 'إخفاء الكاردينالية',
|
||||
show_cardinality: 'إظهار الكاردينالية',
|
||||
zoom_on_scroll: 'تكبير/تصغير عند التمرير',
|
||||
theme: 'المظهر',
|
||||
show_dependencies: 'إظهار الاعتمادات',
|
||||
hide_dependencies: 'إخفاء الاعتمادات',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
backup: {
|
||||
backup: 'النسخ الاحتياطي',
|
||||
export_diagram: 'تصدير المخطط',
|
||||
restore_diagram: 'استعادة المخطط',
|
||||
},
|
||||
help: {
|
||||
help: 'مساعدة',
|
||||
docs_website: 'الوثائق',
|
||||
visit_website: 'ChartDB قم بزيارة',
|
||||
join_discord: 'Discord انضم إلينا على',
|
||||
schedule_a_call: '!تحدث معنا',
|
||||
},
|
||||
},
|
||||
|
||||
delete_diagram_alert: {
|
||||
title: 'حذف المخطط',
|
||||
description:
|
||||
'.لا يمكن التراجع عن هذا الإجراء. سيتم حذف الرسم البياني بشكل دائم',
|
||||
cancel: 'إلغاء',
|
||||
delete: 'حذف',
|
||||
},
|
||||
|
||||
clear_diagram_alert: {
|
||||
title: 'مسح الرسم البياني',
|
||||
description:
|
||||
'.لا يمكن التراجع عن هذا الاجراء. سيتم حذف جميع البيانات في الرسم البياني بشكل دائم',
|
||||
cancel: 'إلغاء',
|
||||
clear: 'مسح',
|
||||
},
|
||||
|
||||
reorder_diagram_alert: {
|
||||
title: 'إعادة ترتيب الرسم البياني',
|
||||
description:
|
||||
'هذا الإجراء سيقوم بإعادة ترتيب الجداول في المخطط بشكل تلقائي. هل تريد المتابعة؟',
|
||||
reorder: 'إعادة ترتيب',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
|
||||
multiple_schemas_alert: {
|
||||
title: 'مخططات متعددة',
|
||||
description:
|
||||
'{{formattedSchemas}} :مخططات في هذا الرسم البياني. يتم حاليا عرض {{schemasCount}} هناك',
|
||||
dont_show_again: 'لا تظهره مجدداً',
|
||||
change_schema: 'تغيير',
|
||||
none: 'لا شيء',
|
||||
},
|
||||
|
||||
copy_to_clipboard_toast: {
|
||||
unsupported: {
|
||||
title: 'فشل النسخ',
|
||||
description: '.الحافظة غير مدعومة',
|
||||
},
|
||||
failed: {
|
||||
title: 'فشل النسخ',
|
||||
description: 'حدث خطأ أثناء النسخ. حاول مجدداً',
|
||||
},
|
||||
},
|
||||
|
||||
theme: {
|
||||
system: 'النظام',
|
||||
light: 'فاتح',
|
||||
dark: 'داكن',
|
||||
},
|
||||
|
||||
zoom: {
|
||||
on: 'تشغيل',
|
||||
off: 'إيقاف',
|
||||
},
|
||||
|
||||
last_saved: 'آخر حفظ',
|
||||
saved: 'تم الحفظ',
|
||||
loading_diagram: '...جارِ تحميل الرسم البياني',
|
||||
deselect_all: 'إلغاء تحديد الكل',
|
||||
select_all: 'تحديد الكل',
|
||||
clear: 'مسح',
|
||||
show_more: 'عرض المزيد',
|
||||
show_less: 'عرض أقل',
|
||||
copy_to_clipboard: 'نسخ إلى الحافظة',
|
||||
copied: '!تم النسخ',
|
||||
|
||||
side_panel: {
|
||||
schema: ':المخطط',
|
||||
filter_by_schema: 'تصفية حسب المخطط',
|
||||
search_schema: '...بحث في المخطط',
|
||||
no_schemas_found: '.لم يتم العثور على مخططات',
|
||||
view_all_options: '...عرض جميع الخيارات',
|
||||
tables_section: {
|
||||
tables: 'الجداول',
|
||||
add_table: 'إضافة جدول',
|
||||
filter: 'تصفية',
|
||||
collapse: 'طي الكل',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'الحقول',
|
||||
nullable: 'يمكن ان يكون فارغاً؟',
|
||||
primary_key: 'المفتاح الأساسي',
|
||||
indexes: 'الفهارس',
|
||||
comments: 'تعليقات',
|
||||
no_comments: 'لا توجد تعليقات',
|
||||
add_field: 'إضافة حقل',
|
||||
add_index: 'إضافة فهرس',
|
||||
index_select_fields: 'حدد الحقول',
|
||||
no_types_found: 'لا يوجد أنواع',
|
||||
field_name: 'الإسم',
|
||||
field_type: 'النوع',
|
||||
field_actions: {
|
||||
title: 'خصائص الحقل',
|
||||
unique: 'فريد',
|
||||
comments: 'تعليقات',
|
||||
no_comments: 'لا يوجد تعليقات',
|
||||
delete_field: 'حذف الحقل',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'خصائص الفهرس',
|
||||
name: 'الإسم',
|
||||
unique: 'فريد',
|
||||
delete_index: 'حذف الفهرس',
|
||||
},
|
||||
table_actions: {
|
||||
title: 'إجراءات الجدول',
|
||||
change_schema: 'تغيير المخطط',
|
||||
add_field: 'إضافة حقل',
|
||||
add_index: 'إضافة فهرس',
|
||||
duplicate_table: 'نسخ الجدول',
|
||||
delete_table: 'حذف الجدول',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'لا توجد جداول',
|
||||
description: 'أنشئ جدولاً للبدء',
|
||||
},
|
||||
},
|
||||
relationships_section: {
|
||||
relationships: 'العلاقات',
|
||||
filter: 'تصفية',
|
||||
add_relationship: 'إضافة علاقة',
|
||||
collapse: 'طي الكل',
|
||||
relationship: {
|
||||
primary: 'الجدول الأساسي',
|
||||
foreign: 'الجدول المرتبط',
|
||||
cardinality: 'الكاردينالية',
|
||||
delete_relationship: 'حذف',
|
||||
relationship_actions: {
|
||||
title: 'إجراءات',
|
||||
delete_relationship: 'حذف',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'لا توجد علاقات',
|
||||
description: 'إنشئ علاقة لربط الجداول',
|
||||
},
|
||||
},
|
||||
dependencies_section: {
|
||||
dependencies: 'الاعتمادات',
|
||||
filter: 'تصفية',
|
||||
collapse: 'طي الكل',
|
||||
dependency: {
|
||||
table: 'الجدول',
|
||||
dependent_table: 'عرض الاعتمادات',
|
||||
delete_dependency: 'حذف',
|
||||
dependency_actions: {
|
||||
title: 'إجراءات',
|
||||
delete_dependency: 'حذف',
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'لا توجد اعتمادات',
|
||||
description: 'إنشاء اعتماد للبدء',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
toolbar: {
|
||||
zoom_in: 'تكبير',
|
||||
zoom_out: 'تصغير',
|
||||
save: 'حفظ',
|
||||
show_all: 'عرض الكل',
|
||||
undo: 'تراجع',
|
||||
redo: 'إعادة',
|
||||
reorder_diagram: 'إعادة ترتيب الرسم البياني',
|
||||
highlight_overlapping_tables: 'تمييز الجداول المتداخلة',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
database_selection: {
|
||||
title: 'ما هو نوع قاعدة البيانات الخاصة بك؟',
|
||||
description:
|
||||
'تتمتع كل قاعدة بيانات بمميزاتها وقدراتها الفريدة.',
|
||||
check_examples_long: 'ألقي نظرة على الأمثلة',
|
||||
check_examples_short: 'أمثلة',
|
||||
},
|
||||
|
||||
import_database: {
|
||||
title: 'إسترد قاعدة بياناتك',
|
||||
database_edition: ':إصدار قاعدة البيانات',
|
||||
step_1: ':قم بتشغيل هذا البرنامج النصي في قاعدة بياناتك',
|
||||
step_2: ':إلصق نتيجة البرنامج النصي هنا',
|
||||
script_results_placeholder: '...نتيجة البرنامج النصي هنا',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS تعليمات',
|
||||
title: 'تعليمات',
|
||||
step_1: 'SQL SERVER < انتقل إلى الأدوات > الخيارات > نتائح الاستعلام',
|
||||
step_2: '(اضبطها على 9999999) XML اذا كنت تستخدم "نتائج إلى الشبكة"، قم بتغيير الحد الاقصى للاحرف المستردة للبيانات غير',
|
||||
},
|
||||
instructions_link: 'تحتاج مساعدة؟ شاهد الفيديو',
|
||||
check_script_result: 'تحقق من نتيجة البرنامج النصي',
|
||||
},
|
||||
|
||||
cancel: 'إلغاء',
|
||||
import_from_file: 'استيراد من ملف',
|
||||
back: 'رجوع',
|
||||
empty_diagram: 'مخطط فارغ',
|
||||
continue: 'متابعة',
|
||||
import: 'استيراد',
|
||||
},
|
||||
|
||||
open_diagram_dialog: {
|
||||
title: 'فتح مخطط',
|
||||
description: 'اختر مخططًا لفتحه من القائمة ادناه',
|
||||
table_columns: {
|
||||
name: 'الإسم',
|
||||
created_at: 'تاريخ الإنشاء',
|
||||
last_modified: 'آخر تعديل',
|
||||
tables_count: 'الجداول',
|
||||
},
|
||||
cancel: 'إلغاء',
|
||||
open: 'فتح',
|
||||
},
|
||||
|
||||
export_sql_dialog: {
|
||||
title: 'SQL تصدير',
|
||||
description:
|
||||
'{{databaseType}} صدّر مخطط الرسم البياني إلى برنامج نصي لـ',
|
||||
close: 'إغلاق',
|
||||
loading: {
|
||||
text: '...{{databaseType}} ل SQL يقوم الذكاء الاصطناعي بإنشاء',
|
||||
description: 'هذا قد يستغرق 30 ثانية',
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
'النصي. يرجى المحاولة مرة اخرى لاحقاً او <0>اتصل بنا</0> SQL خطأ في إنشاء برنامج',
|
||||
description:
|
||||
' الخاصة بك. راجع الدليل <0>هنا</0> OPENAI_TOKEN لا تتردد في استخدام',
|
||||
},
|
||||
},
|
||||
|
||||
create_relationship_dialog: {
|
||||
title: 'إنشاء علاقة',
|
||||
primary_table: 'الجدول الأساسي',
|
||||
primary_field: 'الحقل الأساسي',
|
||||
referenced_table: 'الجدول المرتبط',
|
||||
referenced_field: 'الحقل المرتبط',
|
||||
primary_table_placeholder: 'حدد الجدول',
|
||||
primary_field_placeholder: 'حدد الحقل',
|
||||
referenced_table_placeholder: 'حدد الجدول',
|
||||
referenced_field_placeholder: 'حدد الحقل',
|
||||
no_tables_found: 'لم يتم العثور على جداول',
|
||||
no_fields_found: 'لم يتم العثور على حقول',
|
||||
create: 'إنشاء',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
|
||||
import_database_dialog: {
|
||||
title: 'استيراد إلى المخطط الحالي',
|
||||
override_alert: {
|
||||
title: 'استيراد قاعدة بيانات',
|
||||
content: {
|
||||
alert: 'سيؤدي استيراد هذا المخطط إلى التأثير على الجداول والعلاقات الحالية.',
|
||||
new_tables:
|
||||
'جداول جديدة <bold>{{newTablesNumber}}</bold> سيتم إضافة',
|
||||
new_relationships:
|
||||
'علاقات جديدة <bold>{{newRelationshipsNumber}}</bold> سيتم إنشاء',
|
||||
tables_override:
|
||||
'جداول <bold>{{tablesOverrideNumber}}</bold> سيتم تعديل',
|
||||
proceed: 'هل تريد المتابعة؟',
|
||||
},
|
||||
import: 'استيراد',
|
||||
cancel: 'إلغاء',
|
||||
},
|
||||
},
|
||||
|
||||
export_image_dialog: {
|
||||
title: 'تصدير الصورة',
|
||||
description: ':اختر عامل المقياس للتصدير',
|
||||
scale_1x: '1x عادي',
|
||||
scale_2x: '2x (موصى به)',
|
||||
scale_3x: '3x',
|
||||
scale_4x: '4x',
|
||||
cancel: 'إلغاء',
|
||||
export: 'تصدير',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
title: 'اختر مخططاً',
|
||||
description:
|
||||
'.يتم حالياً عرض مخططات متعددة. اختر واحداً للجدول الجديد',
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'تأكيد',
|
||||
},
|
||||
|
||||
update_table_schema_dialog: {
|
||||
title: 'تغيير المخطط',
|
||||
description: '"{{tableName}}" تحديث مخطط الجدول',
|
||||
cancel: 'إلغاء',
|
||||
confirm: 'تغيير',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: '!ساعدنا على التحسن',
|
||||
description: '؟! إنها مجرد نقرة واحدةGITHUB هل ترغب في تقييمنا على',
|
||||
close: 'ليس الآن',
|
||||
confirm: '!بالتأكيد',
|
||||
},
|
||||
export_diagram_dialog: {
|
||||
title: 'تصدير المخطط',
|
||||
description: ':اختر التنسيق للتصدير',
|
||||
format_json: 'JSON',
|
||||
cancel: 'إلغاء',
|
||||
export: 'تصدير',
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء التصدير',
|
||||
description:
|
||||
'chartdb.io@gmail.com حدث خطأ ما. هل تحتاج إلى مساعدة؟',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
title: 'استيراد الرسم البياني',
|
||||
description: ':للرسم البياني ادناه JSON قم بلصق',
|
||||
cancel: 'إلغاء',
|
||||
import: 'استيراد',
|
||||
error: {
|
||||
title: 'حدث خطأ أثناء الاستيراد',
|
||||
description:
|
||||
'chartdb.io@gmail.com و المحاولة مرة اخرى. هل تحتاج إلى المساعدة؟ JSON غير صالح. يرجى التحقق من JSON الرسم البياني',
|
||||
},
|
||||
},
|
||||
import_dbml_dialog: {
|
||||
// TODO: Translate
|
||||
title: 'Import DBML',
|
||||
example_title: 'Import Example DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'واحد إلى واحد',
|
||||
one_to_many: 'واحد إلى متعدد',
|
||||
many_to_one: 'متعدد إلى واحد',
|
||||
many_to_many: 'متعدد إلى متعدد',
|
||||
},
|
||||
|
||||
canvas_context_menu: {
|
||||
new_table: 'جدول جديد',
|
||||
new_relationship: 'علاقة جديدة',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'تعديل الجدول',
|
||||
duplicate_table: 'نسخ الجدول',
|
||||
delete_table: 'حذف الجدول',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: '({{key}} مغنظة الشبكة (اضغط مع الاستمرار على',
|
||||
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'انقر مرتين للتعديل',
|
||||
},
|
||||
|
||||
language_select: {
|
||||
change_language: 'اللغة',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const arMetadata: LanguageMetadata = {
|
||||
name: 'Arabic',
|
||||
nativeName: 'العربية',
|
||||
code: 'ar',
|
||||
};
|
||||
@@ -8,7 +8,7 @@ export const bn: LanguageTranslation = {
|
||||
new: 'নতুন',
|
||||
open: 'খুলুন',
|
||||
save: 'সংরক্ষণ করুন',
|
||||
import_database: 'ডাটাবেস আমদানি করুন',
|
||||
import: 'ডাটাবেস আমদানি করুন',
|
||||
export_sql: 'SQL রপ্তানি করুন',
|
||||
export_as: 'রূপে রপ্তানি করুন',
|
||||
delete_diagram: 'ডায়াগ্রাম মুছুন',
|
||||
@@ -30,15 +30,19 @@ export const bn: LanguageTranslation = {
|
||||
theme: 'থিম',
|
||||
show_dependencies: 'নির্ভরতাগুলি দেখান',
|
||||
hide_dependencies: 'নির্ভরতাগুলি লুকান',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
|
||||
share: {
|
||||
share: 'শেয়ার করুন',
|
||||
backup: {
|
||||
backup: 'ব্যাকআপ',
|
||||
export_diagram: 'ডায়াগ্রাম রপ্তানি করুন',
|
||||
import_diagram: 'ডায়াগ্রাম আমদানি করুন',
|
||||
restore_diagram: 'ডায়াগ্রাম পুনরুদ্ধার করুন',
|
||||
},
|
||||
help: {
|
||||
help: 'সাহায্য',
|
||||
docs_website: 'ডকুমেন্টেশন',
|
||||
visit_website: 'ChartDB ওয়েবসাইটে যান',
|
||||
join_discord: 'আমাদের Discord-এ যোগ দিন',
|
||||
schedule_a_call: 'আমাদের সাথে কথা বলুন!',
|
||||
@@ -102,7 +106,6 @@ export const bn: LanguageTranslation = {
|
||||
|
||||
last_saved: 'সর্বশেষ সংরক্ষণ',
|
||||
saved: 'সংরক্ষিত',
|
||||
diagrams: 'ডায়াগ্রাম',
|
||||
loading_diagram: 'ডায়াগ্রাম লোড হচ্ছে...',
|
||||
deselect_all: 'সব নির্বাচন সরান',
|
||||
select_all: 'সব নির্বাচন করুন',
|
||||
@@ -123,6 +126,12 @@ export const bn: LanguageTranslation = {
|
||||
add_table: 'টেবিল যোগ করুন',
|
||||
filter: 'ফিল্টার',
|
||||
collapse: 'সব ভাঁজ করুন',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'ফিল্ড',
|
||||
@@ -143,6 +152,8 @@ export const bn: LanguageTranslation = {
|
||||
comments: 'মন্তব্য',
|
||||
no_comments: 'কোনো মন্তব্য নেই',
|
||||
delete_field: 'ফিল্ড মুছুন',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ইনডেক্স কর্ম',
|
||||
@@ -371,6 +382,20 @@ export const bn: LanguageTranslation = {
|
||||
'ডায়াগ্রাম JSON অবৈধ। অনুগ্রহ করে JSON পরীক্ষা করুন এবং আবার চেষ্টা করুন। সাহায্যের প্রয়োজন? chartdb.io@gmail.com-এ যোগাযোগ করুন।',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'এক থেকে এক',
|
||||
one_to_many: 'এক থেকে অনেক',
|
||||
@@ -387,6 +412,7 @@ export const bn: LanguageTranslation = {
|
||||
edit_table: 'টেবিল সম্পাদনা করুন',
|
||||
duplicate_table: 'টেবিল নকল করুন',
|
||||
delete_table: 'টেবিল মুছে ফেলুন',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'গ্রিডে স্ন্যাপ করুন (অবস্থান {{key}})',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const de: LanguageTranslation = {
|
||||
new: 'Neu',
|
||||
open: 'Öffnen',
|
||||
save: 'Speichern',
|
||||
import_database: 'Datenbank importieren',
|
||||
import: 'Datenbank importieren',
|
||||
export_sql: 'SQL exportieren',
|
||||
export_as: 'Exportieren als',
|
||||
delete_diagram: 'Diagramm löschen',
|
||||
@@ -30,15 +30,19 @@ export const de: LanguageTranslation = {
|
||||
theme: 'Stil',
|
||||
show_dependencies: 'Abhängigkeiten anzeigen',
|
||||
hide_dependencies: 'Abhängigkeiten ausblenden',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Hilfe',
|
||||
docs_website: 'Dokumentation',
|
||||
visit_website: 'ChartDB Webseite',
|
||||
join_discord: 'Auf Discord beitreten',
|
||||
schedule_a_call: 'Gespräch vereinbaren',
|
||||
@@ -103,7 +107,6 @@ export const de: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Zuletzt gespeichert',
|
||||
saved: 'Gespeichert',
|
||||
diagrams: 'Diagramme',
|
||||
loading_diagram: 'Diagramm wird geladen...',
|
||||
deselect_all: 'Alles abwählen',
|
||||
select_all: 'Alles auswählen',
|
||||
@@ -124,6 +127,12 @@ export const de: LanguageTranslation = {
|
||||
add_table: 'Tabelle hinzufügen',
|
||||
filter: 'Filter',
|
||||
collapse: 'Alle einklappen',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Felder',
|
||||
@@ -144,6 +153,8 @@ export const de: LanguageTranslation = {
|
||||
comments: 'Kommentare',
|
||||
no_comments: 'Keine Kommentare',
|
||||
delete_field: 'Feld löschen',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Indexattribute',
|
||||
@@ -374,6 +385,20 @@ export const de: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Ein zu Eins (1:1)',
|
||||
one_to_many: 'Ein zu Viele (1:n)',
|
||||
@@ -390,6 +415,7 @@ export const de: LanguageTranslation = {
|
||||
edit_table: 'Tabelle bearbeiten',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'Tabelle löschen',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const en = {
|
||||
new: 'New',
|
||||
open: 'Open',
|
||||
save: 'Save',
|
||||
import_database: 'Import Database',
|
||||
import: 'Import',
|
||||
export_sql: 'Export SQL',
|
||||
export_as: 'Export as',
|
||||
delete_diagram: 'Delete Diagram',
|
||||
@@ -30,14 +30,17 @@ export const en = {
|
||||
theme: 'Theme',
|
||||
show_dependencies: 'Show Dependencies',
|
||||
hide_dependencies: 'Hide Dependencies',
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Help',
|
||||
docs_website: 'Docs',
|
||||
visit_website: 'Visit ChartDB',
|
||||
join_discord: 'Join us on Discord',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
@@ -101,7 +104,6 @@ export const en = {
|
||||
|
||||
last_saved: 'Last saved',
|
||||
saved: 'Saved',
|
||||
diagrams: 'Diagrams',
|
||||
loading_diagram: 'Loading diagram...',
|
||||
deselect_all: 'Deselect All',
|
||||
select_all: 'Select All',
|
||||
@@ -122,6 +124,10 @@ export const en = {
|
||||
add_table: 'Add Table',
|
||||
filter: 'Filter',
|
||||
collapse: 'Collapse All',
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Fields',
|
||||
@@ -139,6 +145,7 @@ export const en = {
|
||||
field_actions: {
|
||||
title: 'Field Attributes',
|
||||
unique: 'Unique',
|
||||
character_length: 'Max Length',
|
||||
comments: 'Comments',
|
||||
no_comments: 'No comments',
|
||||
delete_field: 'Delete Field',
|
||||
@@ -360,7 +367,7 @@ export const en = {
|
||||
|
||||
import_diagram_dialog: {
|
||||
title: 'Import Diagram',
|
||||
description: 'Paste the diagram JSON below:',
|
||||
description: 'Import a diagram from a JSON file.',
|
||||
cancel: 'Cancel',
|
||||
import: 'Import',
|
||||
error: {
|
||||
@@ -369,6 +376,20 @@ export const en = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error importing DBML',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'One to One',
|
||||
one_to_many: 'One to Many',
|
||||
@@ -385,6 +406,7 @@ export const en = {
|
||||
edit_table: 'Edit Table',
|
||||
duplicate_table: 'Duplicate Table',
|
||||
delete_table: 'Delete Table',
|
||||
add_relationship: 'Add Relationship',
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const es: LanguageTranslation = {
|
||||
new: 'Nuevo',
|
||||
open: 'Abrir',
|
||||
save: 'Guardar',
|
||||
import_database: 'Importar Base de Datos',
|
||||
import: 'Importar Base de Datos',
|
||||
export_sql: 'Exportar SQL',
|
||||
export_as: 'Exportar como',
|
||||
delete_diagram: 'Eliminar Diagrama',
|
||||
@@ -30,15 +30,18 @@ export const es: LanguageTranslation = {
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Mostrar dependencias',
|
||||
hide_dependencies: 'Ocultar dependencias',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Respaldo',
|
||||
export_diagram: 'Exportar Diagrama',
|
||||
restore_diagram: 'Restaurar Diagrama',
|
||||
},
|
||||
help: {
|
||||
help: 'Ayuda',
|
||||
docs_website: 'Documentación',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Únete a nosotros en Discord',
|
||||
schedule_a_call: '¡Habla con nosotros!',
|
||||
@@ -93,7 +96,6 @@ export const es: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Último guardado',
|
||||
saved: 'Guardado',
|
||||
diagrams: 'Diagramas',
|
||||
loading_diagram: 'Cargando diagrama...',
|
||||
deselect_all: 'Deseleccionar todo',
|
||||
select_all: 'Seleccionar todo',
|
||||
@@ -114,6 +116,12 @@ export const es: LanguageTranslation = {
|
||||
add_table: 'Agregar Tabla',
|
||||
filter: 'Filtrar',
|
||||
collapse: 'Colapsar Todo',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Campos',
|
||||
@@ -134,6 +142,8 @@ export const es: LanguageTranslation = {
|
||||
comments: 'Comentarios',
|
||||
no_comments: 'Sin comentarios',
|
||||
delete_field: 'Eliminar Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos del Índice',
|
||||
@@ -373,6 +383,20 @@ export const es: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Uno a Uno',
|
||||
one_to_many: 'Uno a Muchos',
|
||||
@@ -389,6 +413,7 @@ export const es: LanguageTranslation = {
|
||||
edit_table: 'Editar Tabla',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'Eliminar Tabla',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const fr: LanguageTranslation = {
|
||||
new: 'Nouveau',
|
||||
open: 'Ouvrir',
|
||||
save: 'Enregistrer',
|
||||
import_database: 'Importer Base de Données',
|
||||
import: 'Importer Base de Données',
|
||||
export_sql: 'Exporter SQL',
|
||||
export_as: 'Exporter en tant que',
|
||||
delete_diagram: 'Supprimer le Diagramme',
|
||||
@@ -30,14 +30,17 @@ export const fr: LanguageTranslation = {
|
||||
theme: 'Thème',
|
||||
show_dependencies: 'Afficher les Dépendances',
|
||||
hide_dependencies: 'Masquer les Dépendances',
|
||||
show_minimap: 'Afficher la Mini Carte',
|
||||
hide_minimap: 'Masquer la Mini Carte',
|
||||
},
|
||||
share: {
|
||||
share: 'Partage',
|
||||
backup: {
|
||||
backup: 'Sauvegarde',
|
||||
export_diagram: 'Exporter le diagramme',
|
||||
import_diagram: 'Importer un diagramme',
|
||||
restore_diagram: 'Restaurer le diagramme',
|
||||
},
|
||||
help: {
|
||||
help: 'Aide',
|
||||
docs_website: 'Documentation',
|
||||
visit_website: 'Visitez ChartDB',
|
||||
join_discord: 'Rejoignez-nous sur Discord',
|
||||
schedule_a_call: 'Parlez avec nous !',
|
||||
@@ -92,16 +95,14 @@ export const fr: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Dernière sauvegarde',
|
||||
saved: 'Enregistré',
|
||||
diagrams: 'Diagrammes',
|
||||
loading_diagram: 'Chargement du diagramme...',
|
||||
deselect_all: 'Tout désélectionner',
|
||||
select_all: 'Tout sélectionner',
|
||||
clear: 'Effacer',
|
||||
show_more: 'Afficher Plus',
|
||||
show_less: 'Afficher Moins',
|
||||
// TODO: Translate
|
||||
copy_to_clipboard: 'Copy to Clipboard',
|
||||
copied: 'Copied!',
|
||||
copy_to_clipboard: 'Copier dans le presse-papiers',
|
||||
copied: 'Copié !',
|
||||
|
||||
side_panel: {
|
||||
schema: 'Schéma:',
|
||||
@@ -114,6 +115,11 @@ export const fr: LanguageTranslation = {
|
||||
add_table: 'Ajouter une Table',
|
||||
filter: 'Filtrer',
|
||||
collapse: 'Réduire Tout',
|
||||
clear: 'Effacer le Filtre',
|
||||
no_results:
|
||||
'Aucune table trouvée correspondant à votre filtre.',
|
||||
show_list: 'Afficher la Liste des Tableaux',
|
||||
show_dbml: "Afficher l'éditeur DBML",
|
||||
|
||||
table: {
|
||||
fields: 'Champs',
|
||||
@@ -134,6 +140,8 @@ export const fr: LanguageTranslation = {
|
||||
comments: 'Commentaires',
|
||||
no_comments: 'Pas de commentaires',
|
||||
delete_field: 'Supprimer le Champ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: "Attributs de l'Index",
|
||||
@@ -145,7 +153,7 @@ export const fr: LanguageTranslation = {
|
||||
title: 'Actions de la Table',
|
||||
add_field: 'Ajouter un Champ',
|
||||
add_index: 'Ajouter un Index',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Tableau dupliqué',
|
||||
delete_table: 'Supprimer la Table',
|
||||
change_schema: 'Changer le Schéma',
|
||||
},
|
||||
@@ -228,14 +236,12 @@ export const fr: LanguageTranslation = {
|
||||
step_2: 'Si vous utilisez "Résultats en Grille", changez le nombre maximum de caractères récupérés pour les données non-XML (définir à 9999999).',
|
||||
},
|
||||
instructions_link: "Besoin d'aide ? Regardez comment",
|
||||
// TODO: Translate
|
||||
check_script_result: 'Check Script Result',
|
||||
check_script_result: 'Vérifier le résultat du Script',
|
||||
},
|
||||
|
||||
cancel: 'Annuler',
|
||||
back: 'Retour',
|
||||
// TODO: Translate
|
||||
import_from_file: 'Import from File',
|
||||
import_from_file: "Importer à partir d'un fichier",
|
||||
empty_diagram: 'Diagramme vide',
|
||||
continue: 'Continuer',
|
||||
import: 'Importer',
|
||||
@@ -350,29 +356,42 @@ export const fr: LanguageTranslation = {
|
||||
cancel: 'Annuler',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
export_diagram_dialog: {
|
||||
title: 'Export Diagram',
|
||||
description: 'Choose the format for export:',
|
||||
title: 'Exporter le Diagramme',
|
||||
description: "Sélectionner le format d'exportation :",
|
||||
format_json: 'JSON',
|
||||
cancel: 'Cancel',
|
||||
export: 'Export',
|
||||
cancel: 'Annuler',
|
||||
export: 'Exporter',
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
title: "Erreur lors de l'exportation du diagramme",
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
"Une erreur s'est produite. Besoin d'aide ? chartdb.io@gmail.com",
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_diagram_dialog: {
|
||||
title: 'Import Diagram',
|
||||
description: 'Paste the diagram JSON below:',
|
||||
cancel: 'Cancel',
|
||||
import: 'Import',
|
||||
title: 'Importer un diagramme',
|
||||
description: 'Coller le diagramme au format JSON ci-dessous :',
|
||||
cancel: 'Annuler',
|
||||
import: 'Exporter',
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
title: "Erreur lors de l'exportation du diagramme",
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
"Le diagramme JSON n'est pas valide. Veuillez vérifier le JSON et réessayer. Besoin d'aide ? chartdb.io@gmail.com",
|
||||
},
|
||||
},
|
||||
import_dbml_dialog: {
|
||||
example_title: "Exemple d'importation DBML",
|
||||
title: 'Import DBML',
|
||||
description:
|
||||
'Importer un schéma de base de données à partir du format DBML.',
|
||||
import: 'Importer',
|
||||
cancel: 'Annuler',
|
||||
skip_and_empty: 'Passer et vider',
|
||||
show_example: 'Afficher un exemple',
|
||||
error: {
|
||||
title: 'Erreur',
|
||||
description:
|
||||
"Erreur d'analyse du DBML. Veuillez vérifier la syntaxe.",
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
@@ -389,12 +408,13 @@ export const fr: LanguageTranslation = {
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Éditer la Table',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Tableau Dupliqué',
|
||||
delete_table: 'Supprimer la Table',
|
||||
add_relationship: 'Ajouter une Relation',
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||
snap_to_grid_tooltip:
|
||||
'Aligner sur la grille (maintenir la touche {{key}})',
|
||||
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'Double-cliquez pour modifier',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const gu: LanguageTranslation = {
|
||||
new: 'નવું',
|
||||
open: 'ખોલો',
|
||||
save: 'સાચવો',
|
||||
import_database: 'ડેટાબેસ આયાત કરો',
|
||||
import: 'ડેટાબેસ આયાત કરો',
|
||||
export_sql: 'SQL નિકાસ કરો',
|
||||
export_as: 'રૂપે નિકાસ કરો',
|
||||
delete_diagram: 'ડાયાગ્રામ કાઢી નાખો',
|
||||
@@ -30,15 +30,19 @@ export const gu: LanguageTranslation = {
|
||||
theme: 'થિમ',
|
||||
show_dependencies: 'નિર્ભરતાઓ બતાવો',
|
||||
hide_dependencies: 'નિર્ભરતાઓ છુપાવો',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
|
||||
share: {
|
||||
share: 'શેર કરો',
|
||||
backup: {
|
||||
backup: 'બેકઅપ',
|
||||
export_diagram: 'ડાયાગ્રામ નિકાસ કરો',
|
||||
import_diagram: 'ડાયાગ્રામ આયાત કરો',
|
||||
restore_diagram: 'ડાયાગ્રામ પુનઃસ્થાપિત કરો',
|
||||
},
|
||||
help: {
|
||||
help: 'મદદ',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB વેબસાઇટ પર જાઓ',
|
||||
join_discord: 'અમારા Discordમાં જોડાઓ',
|
||||
schedule_a_call: 'અમારી સાથે વાત કરો!',
|
||||
@@ -102,7 +106,6 @@ export const gu: LanguageTranslation = {
|
||||
|
||||
last_saved: 'છેલ્લે સાચવ્યું',
|
||||
saved: 'સાચવ્યું',
|
||||
diagrams: 'ડાયાગ્રામ',
|
||||
loading_diagram: 'ડાયાગ્રામ લોડ થઈ રહ્યું છે...',
|
||||
deselect_all: 'બધાને ડીસેલેક્ટ કરો',
|
||||
select_all: 'બધા પસંદ કરો',
|
||||
@@ -123,6 +126,12 @@ export const gu: LanguageTranslation = {
|
||||
add_table: 'ટેબલ ઉમેરો',
|
||||
filter: 'ફિલ્ટર',
|
||||
collapse: 'બધાને સકુચિત કરો',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'ફીલ્ડ્સ',
|
||||
@@ -144,6 +153,8 @@ export const gu: LanguageTranslation = {
|
||||
comments: 'ટિપ્પણીઓ',
|
||||
no_comments: 'કોઈ ટિપ્પણીઓ નથી',
|
||||
delete_field: 'ફીલ્ડ કાઢી નાખો',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ઇન્ડેક્સ લક્ષણો',
|
||||
@@ -371,6 +382,20 @@ export const gu: LanguageTranslation = {
|
||||
'ડાયાગ્રામ JSON અમાન્ય છે. કૃપા કરીને JSON તપાસો અને ફરી પ્રયાસ કરો. મદદ જોઈએ? chartdb.io@gmail.com પર સંપર્ક કરો.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'એકથી એક',
|
||||
one_to_many: 'એકથી ઘણા',
|
||||
@@ -387,6 +412,7 @@ export const gu: LanguageTranslation = {
|
||||
edit_table: 'ટેબલ સંપાદિત કરો',
|
||||
duplicate_table: 'ટેબલ નકલ કરો',
|
||||
delete_table: 'ટેબલ કાઢી નાખો',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'ગ્રિડ પર સ્નેપ કરો (જમાવટ {{key}})',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const hi: LanguageTranslation = {
|
||||
new: 'नया',
|
||||
open: 'खोलें',
|
||||
save: 'सहेजें',
|
||||
import_database: 'डेटाबेस आयात करें',
|
||||
import: 'डेटाबेस आयात करें',
|
||||
export_sql: 'SQL निर्यात करें',
|
||||
export_as: 'के रूप में निर्यात करें',
|
||||
delete_diagram: 'आरेख हटाएँ',
|
||||
@@ -30,15 +30,18 @@ export const hi: LanguageTranslation = {
|
||||
theme: 'थीम',
|
||||
show_dependencies: 'निर्भरता दिखाएँ',
|
||||
hide_dependencies: 'निर्भरता छिपाएँ',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'बैकअप',
|
||||
export_diagram: 'आरेख निर्यात करें',
|
||||
restore_diagram: 'आरेख पुनर्स्थापित करें',
|
||||
},
|
||||
help: {
|
||||
help: 'मदद',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'ChartDB वेबसाइट पर जाएँ',
|
||||
join_discord: 'हमसे Discord पर जुड़ें',
|
||||
schedule_a_call: 'हमसे बात करें!',
|
||||
@@ -102,7 +105,6 @@ export const hi: LanguageTranslation = {
|
||||
|
||||
last_saved: 'अंतिम सहेजा गया',
|
||||
saved: 'सहेजा गया',
|
||||
diagrams: 'आरेख',
|
||||
loading_diagram: 'आरेख लोड हो रहा है...',
|
||||
deselect_all: 'सभी को अचयनित करें',
|
||||
select_all: 'सभी को चुनें',
|
||||
@@ -124,6 +126,12 @@ export const hi: LanguageTranslation = {
|
||||
add_table: 'तालिका जोड़ें',
|
||||
filter: 'फ़िल्टर',
|
||||
collapse: 'सभी को संक्षिप्त करें',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'फ़ील्ड्स',
|
||||
@@ -144,6 +152,8 @@ export const hi: LanguageTranslation = {
|
||||
comments: 'टिप्पणियाँ',
|
||||
no_comments: 'कोई टिप्पणी नहीं',
|
||||
delete_field: 'फ़ील्ड हटाएँ',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचकांक विशेषताएँ',
|
||||
@@ -375,6 +385,20 @@ export const hi: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'एक से एक',
|
||||
one_to_many: 'एक से कई',
|
||||
@@ -391,6 +415,7 @@ export const hi: LanguageTranslation = {
|
||||
edit_table: 'तालिका संपादित करें',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'तालिका हटाएँ',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const id_ID: LanguageTranslation = {
|
||||
new: 'Buat Baru',
|
||||
open: 'Buka',
|
||||
save: 'Simpan',
|
||||
import_database: 'Impor Database',
|
||||
import: 'Impor Database',
|
||||
export_sql: 'Ekspor SQL',
|
||||
export_as: 'Ekspor Sebagai',
|
||||
delete_diagram: 'Hapus Diagram',
|
||||
@@ -30,15 +30,18 @@ export const id_ID: LanguageTranslation = {
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Tampilkan Dependensi',
|
||||
hide_dependencies: 'Sembunyikan Dependensi',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Cadangan',
|
||||
export_diagram: 'Ekspor Diagram',
|
||||
restore_diagram: 'Pulihkan Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Bantuan',
|
||||
docs_website: 'દસ્તાવેજીકરણ',
|
||||
visit_website: 'Kunjungi ChartDB',
|
||||
join_discord: 'Bergabunglah di Discord kami',
|
||||
schedule_a_call: 'Berbicara dengan kami!',
|
||||
@@ -102,7 +105,6 @@ export const id_ID: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Terakhir disimpan',
|
||||
saved: 'Tersimpan',
|
||||
diagrams: 'Diagram',
|
||||
loading_diagram: 'Memuat diagram...',
|
||||
deselect_all: 'Batalkan Semua',
|
||||
select_all: 'Pilih Semua',
|
||||
@@ -123,6 +125,12 @@ export const id_ID: LanguageTranslation = {
|
||||
add_table: 'Tambah Tabel',
|
||||
filter: 'Saring',
|
||||
collapse: 'Lipat Semua',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Kolom',
|
||||
@@ -143,6 +151,8 @@ export const id_ID: LanguageTranslation = {
|
||||
comments: 'Komentar',
|
||||
no_comments: 'Tidak ada komentar',
|
||||
delete_field: 'Hapus Kolom',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atribut Indeks',
|
||||
@@ -346,30 +356,42 @@ export const id_ID: LanguageTranslation = {
|
||||
confirm: 'Tentu saja!',
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
export_diagram_dialog: {
|
||||
title: 'Export Diagram',
|
||||
description: 'Choose the format for export:',
|
||||
title: 'Ekspor Diagram',
|
||||
description: 'Pilih format untuk ekspor:',
|
||||
format_json: 'JSON',
|
||||
cancel: 'Cancel',
|
||||
export: 'Export',
|
||||
cancel: 'Batal',
|
||||
export: 'Ekspor',
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
title: 'Error ekspor diagram',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Sesuatu yang salah. Butuh bantuan? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
import_diagram_dialog: {
|
||||
title: 'Import Diagram',
|
||||
description: 'Paste the diagram JSON below:',
|
||||
cancel: 'Cancel',
|
||||
import: 'Import',
|
||||
title: 'Impor Diagram',
|
||||
description: 'Tempel diagram JSON di bawah:',
|
||||
cancel: 'Batal',
|
||||
import: 'Impor',
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
title: 'Error impor diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
'Diagram JSON tidak valid. Silakan cek JSON dan coba lagi. Butuh bantuan? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -388,16 +410,14 @@ export const id_ID: LanguageTranslation = {
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Ubah Tabel',
|
||||
delete_table: 'Hapus Tabel',
|
||||
// TODO: Translate
|
||||
duplicate_table: 'Duplicate Table',
|
||||
duplicate_table: 'Duplikat Tabel',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||
snap_to_grid_tooltip: 'Snap ke Kisi (Tahan {{key}})',
|
||||
|
||||
// TODO: Translate
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'Double-click to edit',
|
||||
double_click_to_edit: 'Klik ganda untuk mengedit',
|
||||
},
|
||||
|
||||
language_select: {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ja: LanguageTranslation = {
|
||||
new: '新規',
|
||||
open: '開く',
|
||||
save: '保存',
|
||||
import_database: 'データベースをインポート',
|
||||
import: 'データベースをインポート',
|
||||
export_sql: 'SQLをエクスポート',
|
||||
export_as: '形式を指定してエクスポート',
|
||||
delete_diagram: 'ダイアグラムを削除',
|
||||
@@ -31,15 +31,19 @@ export const ja: LanguageTranslation = {
|
||||
// TODO: Translate
|
||||
show_dependencies: 'Show Dependencies',
|
||||
hide_dependencies: 'Hide Dependencies',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'ヘルプ',
|
||||
docs_website: 'ドキュメント',
|
||||
visit_website: 'ChartDBにアクセス',
|
||||
join_discord: 'Discordに参加',
|
||||
schedule_a_call: '話しかけてください!',
|
||||
@@ -104,7 +108,6 @@ export const ja: LanguageTranslation = {
|
||||
|
||||
last_saved: '最後に保存された',
|
||||
saved: '保存されました',
|
||||
diagrams: 'ダイアグラム',
|
||||
loading_diagram: 'ダイアグラムを読み込み中...',
|
||||
deselect_all: 'すべての選択を解除',
|
||||
select_all: 'すべてを選択',
|
||||
@@ -126,6 +129,12 @@ export const ja: LanguageTranslation = {
|
||||
add_table: 'テーブルを追加',
|
||||
filter: 'フィルタ',
|
||||
collapse: 'すべて折りたたむ',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'フィールド',
|
||||
@@ -146,6 +155,8 @@ export const ja: LanguageTranslation = {
|
||||
comments: 'コメント',
|
||||
no_comments: 'コメントがありません',
|
||||
delete_field: 'フィールドを削除',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'インデックス属性',
|
||||
@@ -378,6 +389,20 @@ export const ja: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: '1対1',
|
||||
one_to_many: '1対多',
|
||||
@@ -394,6 +419,7 @@ export const ja: LanguageTranslation = {
|
||||
edit_table: 'テーブルを編集',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'テーブルを削除',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
new: '새 다이어그램',
|
||||
open: '열기',
|
||||
save: '저장',
|
||||
import_database: '데이터베이스 가져오기',
|
||||
import: '데이터베이스 가져오기',
|
||||
export_sql: 'SQL로 저장',
|
||||
export_as: '다른 형식으로 저장',
|
||||
delete_diagram: '다이어그램 삭제',
|
||||
@@ -30,14 +30,18 @@ export const ko_KR: LanguageTranslation = {
|
||||
theme: '테마',
|
||||
show_dependencies: '종속성 보이기',
|
||||
hide_dependencies: '종속성 숨기기',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '공유',
|
||||
backup: {
|
||||
backup: '백업',
|
||||
export_diagram: '다이어그램 내보내기',
|
||||
import_diagram: '다이어그램 가져오기',
|
||||
restore_diagram: '다이어그램 복구',
|
||||
},
|
||||
help: {
|
||||
help: '도움말',
|
||||
docs_website: '선적 서류 비치',
|
||||
visit_website: 'ChartDB 사이트 방문',
|
||||
join_discord: 'Discord 가입',
|
||||
schedule_a_call: 'Talk with us!',
|
||||
@@ -101,7 +105,6 @@ export const ko_KR: LanguageTranslation = {
|
||||
|
||||
last_saved: '최근 저장일시: ',
|
||||
saved: '저장됨',
|
||||
diagrams: '다이어그램',
|
||||
loading_diagram: '다이어그램 로딩중...',
|
||||
deselect_all: '모두 선택 해제',
|
||||
select_all: '모두 선택',
|
||||
@@ -122,6 +125,12 @@ export const ko_KR: LanguageTranslation = {
|
||||
add_table: '테이블 추가',
|
||||
filter: '필터',
|
||||
collapse: '모두 접기',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: '필드',
|
||||
@@ -142,6 +151,8 @@ export const ko_KR: LanguageTranslation = {
|
||||
comments: '주석',
|
||||
no_comments: '주석 없음',
|
||||
delete_field: '필드 삭제',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '인덱스 속성',
|
||||
@@ -367,6 +378,20 @@ export const ko_KR: LanguageTranslation = {
|
||||
'다이어그램 JSON이 유효하지 않습니다. JSON이 올바른 형식인지 확인해주세요. 도움이 필요하신 경우 chartdb.io@gmail.com으로 연락해주세요.',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: '일대일 (1:1)',
|
||||
one_to_many: '일대다 (1:N)',
|
||||
@@ -383,6 +408,7 @@ export const ko_KR: LanguageTranslation = {
|
||||
edit_table: '테이블 수정',
|
||||
duplicate_table: '테이블 복제',
|
||||
delete_table: '테이블 삭제',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: '그리드에 맞추기 ({{key}}를 누른채 유지)',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const mr: LanguageTranslation = {
|
||||
new: 'नवीन',
|
||||
open: 'उघडा',
|
||||
save: 'जतन करा',
|
||||
import_database: 'डेटाबेस इम्पोर्ट करा',
|
||||
import: 'डेटाबेस इम्पोर्ट करा',
|
||||
export_sql: 'SQL एक्स्पोर्ट करा',
|
||||
export_as: 'म्हणून एक्स्पोर्ट करा',
|
||||
delete_diagram: 'आरेख हटवा',
|
||||
@@ -30,15 +30,19 @@ export const mr: LanguageTranslation = {
|
||||
theme: 'थीम',
|
||||
show_dependencies: 'डिपेंडेन्सि दाखवा',
|
||||
hide_dependencies: 'डिपेंडेन्सि लपवा',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
backup: {
|
||||
// TODO: Add translations
|
||||
share: 'Share',
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'मदत',
|
||||
docs_website: 'दस्तऐवजीकरण',
|
||||
visit_website: 'ChartDB ला भेट द्या',
|
||||
join_discord: 'आमच्या डिस्कॉर्डमध्ये सामील व्हा',
|
||||
schedule_a_call: 'आमच्याशी बोला!',
|
||||
@@ -102,7 +106,6 @@ export const mr: LanguageTranslation = {
|
||||
|
||||
last_saved: 'शेवटचे जतन केले',
|
||||
saved: 'जतन केले',
|
||||
diagrams: 'आरेख',
|
||||
loading_diagram: 'आरेख लोड करत आहे...',
|
||||
deselect_all: 'सर्व निवड रद्द करा',
|
||||
select_all: 'सर्व निवडा',
|
||||
@@ -125,6 +128,12 @@ export const mr: LanguageTranslation = {
|
||||
add_table: 'टेबल जोडा',
|
||||
filter: 'फिल्टर',
|
||||
collapse: 'सर्व संकुचित करा',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'फील्ड्स',
|
||||
@@ -145,6 +154,8 @@ export const mr: LanguageTranslation = {
|
||||
comments: 'टिप्पण्या',
|
||||
no_comments: 'कोणत्याही टिप्पणी नाहीत',
|
||||
delete_field: 'फील्ड हटवा',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'इंडेक्स गुणधर्म',
|
||||
@@ -379,6 +390,20 @@ export const mr: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
|
||||
relationship_type: {
|
||||
one_to_one: 'एक ते एक',
|
||||
@@ -395,8 +420,8 @@ export const mr: LanguageTranslation = {
|
||||
table_node_context_menu: {
|
||||
edit_table: 'टेबल संपादित करा',
|
||||
delete_table: 'टेबल हटवा',
|
||||
// TODO: Add translations
|
||||
duplicate_table: 'Duplicate Table',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ne: LanguageTranslation = {
|
||||
new: 'नयाँ',
|
||||
open: 'खोल्नुहोस्',
|
||||
save: 'सुरक्षित गर्नुहोस्',
|
||||
import_database: 'डाटाबेस आयात गर्नुहोस्',
|
||||
import: 'डाटाबेस आयात गर्नुहोस्',
|
||||
export_sql: 'SQL निर्यात गर्नुहोस्',
|
||||
export_as: 'निर्यात गर्नुहोस्',
|
||||
delete_diagram: 'डायाग्राम हटाउनुहोस्',
|
||||
@@ -30,14 +30,19 @@ export const ne: LanguageTranslation = {
|
||||
theme: 'थिम',
|
||||
show_dependencies: 'डिपेन्डेन्सीहरू देखाउनुहोस्',
|
||||
hide_dependencies: 'डिपेन्डेन्सीहरू लुकाउनुहोस्',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'शेयर गर्नुहोस्',
|
||||
export_diagram: 'डायाग्राम निर्यात गर्नुहोस्',
|
||||
import_diagram: 'डायाग्राम आयात गर्नुहोस्',
|
||||
// TODO: Translate
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'मद्दत',
|
||||
docs_website: 'कागजात',
|
||||
visit_website: 'वेबसाइटमा जानुहोस्',
|
||||
join_discord: 'डिस्कोर्डमा सामिल हुनुहोस्',
|
||||
schedule_a_call: 'कल अनुसूची गर्नुहोस्',
|
||||
@@ -101,7 +106,6 @@ export const ne: LanguageTranslation = {
|
||||
|
||||
last_saved: 'अन्तिम सुरक्षित',
|
||||
saved: 'सुरक्षित',
|
||||
diagrams: 'डायाग्रामहरू',
|
||||
loading_diagram: 'डायाग्राम लोड हुँदैछ...',
|
||||
deselect_all: 'सबै चयन हटाउनुहोस्',
|
||||
select_all: 'सबै चयन गर्नुहोस्',
|
||||
@@ -122,6 +126,12 @@ export const ne: LanguageTranslation = {
|
||||
add_table: 'तालिका थप्नुहोस्',
|
||||
filter: 'फिल्टर',
|
||||
collapse: 'सबै लुकाउनुहोस्',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'क्षेत्रहरू',
|
||||
@@ -142,6 +152,8 @@ export const ne: LanguageTranslation = {
|
||||
comments: 'टिप्पणीहरू',
|
||||
no_comments: 'कुनै टिप्पणीहरू छैनन्',
|
||||
delete_field: 'क्षेत्र हटाउनुहोस्',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'सूचक विशेषताहरू',
|
||||
@@ -372,6 +384,20 @@ export const ne: LanguageTranslation = {
|
||||
'डायाग्राम JSON अमान्य छ। कृपया JSON जाँच गर्नुहोस् र पुन: प्रयास गर्नुहोस्। मद्दत चाहिन्छ? chartdb.io@gmail.com मा सम्पर्क गर्नुहोस्',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
|
||||
relationship_type: {
|
||||
one_to_one: 'एक देखि एक',
|
||||
@@ -389,6 +415,7 @@ export const ne: LanguageTranslation = {
|
||||
edit_table: 'तालिका सम्पादन गर्नुहोस्',
|
||||
duplicate_table: 'तालिका नक्कली गर्नुहोस्',
|
||||
delete_table: 'तालिका हटाउनुहोस्',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'ग्रिडमा स्न्याप गर्नुहोस् ({{key}} थिच्नुहोस)',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
new: 'Novo',
|
||||
open: 'Abrir',
|
||||
save: 'Salvar',
|
||||
import_database: 'Importar Banco de Dados',
|
||||
import: 'Importar Banco de Dados',
|
||||
export_sql: 'Exportar SQL',
|
||||
export_as: 'Exportar como',
|
||||
delete_diagram: 'Excluir Diagrama',
|
||||
@@ -30,15 +30,19 @@ export const pt_BR: LanguageTranslation = {
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Mostrar Dependências',
|
||||
hide_dependencies: 'Ocultar Dependências',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Exportar Diagrama',
|
||||
restore_diagram: 'Restaurar Diagrama',
|
||||
},
|
||||
help: {
|
||||
help: 'Ajuda',
|
||||
docs_website: 'Documentação',
|
||||
visit_website: 'Visitar ChartDB',
|
||||
join_discord: 'Junte-se a nós no Discord',
|
||||
schedule_a_call: 'Fale Conosco!',
|
||||
@@ -102,7 +106,6 @@ export const pt_BR: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Última vez salvo',
|
||||
saved: 'Salvo',
|
||||
diagrams: 'Diagramas',
|
||||
loading_diagram: 'Carregando diagrama...',
|
||||
deselect_all: 'Desmarcar Todos',
|
||||
select_all: 'Selecionar Todos',
|
||||
@@ -123,6 +126,12 @@ export const pt_BR: LanguageTranslation = {
|
||||
add_table: 'Adicionar Tabela',
|
||||
filter: 'Filtrar',
|
||||
collapse: 'Colapsar Todas',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Campos',
|
||||
@@ -143,6 +152,8 @@ export const pt_BR: LanguageTranslation = {
|
||||
comments: 'Comentários',
|
||||
no_comments: 'Sem comentários',
|
||||
delete_field: 'Excluir Campo',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Atributos do Índice',
|
||||
@@ -372,6 +383,20 @@ export const pt_BR: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Um para Um',
|
||||
one_to_many: 'Um para Muitos',
|
||||
@@ -388,6 +413,7 @@ export const pt_BR: LanguageTranslation = {
|
||||
edit_table: 'Editar Tabela',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'Excluir Tabela',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
|
||||
@@ -8,7 +8,7 @@ export const ru: LanguageTranslation = {
|
||||
new: 'Создать',
|
||||
open: 'Открыть',
|
||||
save: 'Сохранить',
|
||||
import_database: 'Импортировать базу данных',
|
||||
import: 'Импортировать базу данных',
|
||||
export_sql: 'Экспорт SQL',
|
||||
export_as: 'Экспортировать как',
|
||||
delete_diagram: 'Удалить диаграмму',
|
||||
@@ -30,14 +30,19 @@ export const ru: LanguageTranslation = {
|
||||
theme: 'Тема',
|
||||
show_dependencies: 'Показать зависимости',
|
||||
hide_dependencies: 'Скрыть зависимости',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Поделиться',
|
||||
export_diagram: 'Экспорт кода диаграммы',
|
||||
import_diagram: 'Импорт кода диаграммы',
|
||||
// TODO: Translate
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Помощь',
|
||||
docs_website: 'Документация',
|
||||
visit_website: 'Перейти на сайт ChartDB',
|
||||
join_discord: 'Присоединиться к сообществу в Discord',
|
||||
schedule_a_call: 'Поговорите с нами!',
|
||||
@@ -102,7 +107,6 @@ export const ru: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Последнее сохранение',
|
||||
saved: 'Сохранено',
|
||||
diagrams: 'Диаграммы',
|
||||
loading_diagram: 'Загрузка диаграммы...',
|
||||
deselect_all: 'Отменить выбор всех',
|
||||
select_all: 'Выбрать все',
|
||||
@@ -121,6 +125,12 @@ export const ru: LanguageTranslation = {
|
||||
add_table: 'Добавить таблицу',
|
||||
filter: 'Фильтр',
|
||||
collapse: 'Свернуть все',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Поля',
|
||||
@@ -141,6 +151,8 @@ export const ru: LanguageTranslation = {
|
||||
comments: 'Комментарии',
|
||||
no_comments: 'Нет комментария',
|
||||
delete_field: 'Удалить поле',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибуты индекса',
|
||||
@@ -368,6 +380,20 @@ export const ru: LanguageTranslation = {
|
||||
'Код JSON диаграммы некорректен. Проверьте, пожалуйста, код и попробуйте снова. Проблема не решается? Напишите нам: chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Один к одному',
|
||||
one_to_many: 'Один ко многим',
|
||||
@@ -384,6 +410,7 @@ export const ru: LanguageTranslation = {
|
||||
edit_table: 'Изменить таблицу',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'Удалить таблицу',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
copy_to_clipboard: 'Скопировать в буфер обмена',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const te: LanguageTranslation = {
|
||||
new: 'కొత్తది',
|
||||
open: 'తెరవు',
|
||||
save: 'సేవ్',
|
||||
import_database: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||
import: 'డేటాబేస్ను దిగుమతి చేసుకోండి',
|
||||
export_sql: 'SQL ఎగుమతి',
|
||||
export_as: 'వగా ఎగుమతి చేయండి',
|
||||
delete_diagram: 'చిత్రాన్ని తొలగించండి',
|
||||
@@ -30,15 +30,19 @@ export const te: LanguageTranslation = {
|
||||
theme: 'థీమ్',
|
||||
show_dependencies: 'ఆధారాలు చూపించండి',
|
||||
hide_dependencies: 'ఆధారాలను దాచండి',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'సహాయం',
|
||||
docs_website: 'డాక్యుమెంటేషన్',
|
||||
visit_website: 'ChartDB సందర్శించండి',
|
||||
join_discord: 'డిస్కార్డ్లో మా నుంచి చేరండి',
|
||||
schedule_a_call: 'మాతో మాట్లాడండి!',
|
||||
@@ -102,7 +106,6 @@ export const te: LanguageTranslation = {
|
||||
|
||||
last_saved: 'చివరిగా సేవ్ చేయబడిన',
|
||||
saved: 'సేవ్ చేయబడింది',
|
||||
diagrams: 'చిత్రాలు',
|
||||
loading_diagram: 'చిత్రం లోడ్ అవుతోంది...',
|
||||
deselect_all: 'అన్ని ఎంచుకోకుండా ఉంచు',
|
||||
select_all: 'అన్ని ఎంచుకోండి',
|
||||
@@ -123,6 +126,12 @@ export const te: LanguageTranslation = {
|
||||
add_table: 'పట్టికను జోడించు',
|
||||
filter: 'ఫిల్టర్',
|
||||
collapse: 'అన్ని కూల్ చేయి',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'ఫీల్డులు',
|
||||
@@ -143,6 +152,8 @@ export const te: LanguageTranslation = {
|
||||
comments: 'వ్యాఖ్యలు',
|
||||
no_comments: 'వ్యాఖ్యలు లేవు',
|
||||
delete_field: 'ఫీల్డ్ తొలగించు',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'ఇండెక్స్ గుణాలు',
|
||||
@@ -375,6 +386,20 @@ export const te: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
|
||||
relationship_type: {
|
||||
one_to_one: 'ఒకటి_కీ_ఒకటి',
|
||||
@@ -390,9 +415,9 @@ export const te: LanguageTranslation = {
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'పట్టికను సవరించు',
|
||||
// TODO: Translate
|
||||
duplicate_table: 'Duplicate Table',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: 'పట్టికను తొలగించు',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
|
||||
@@ -8,7 +8,7 @@ export const tr: LanguageTranslation = {
|
||||
new: 'Yeni',
|
||||
open: 'Aç',
|
||||
save: 'Kaydet',
|
||||
import_database: 'Veritabanı İçe Aktar',
|
||||
import: 'Veritabanı İçe Aktar',
|
||||
export_sql: 'SQL Olarak Dışa Aktar',
|
||||
export_as: 'Olarak Dışa Aktar',
|
||||
delete_diagram: 'Diyagramı Sil',
|
||||
@@ -30,15 +30,19 @@ export const tr: LanguageTranslation = {
|
||||
theme: 'Tema',
|
||||
show_dependencies: 'Bağımlılıkları Göster',
|
||||
hide_dependencies: 'Bağımlılıkları Gizle',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
backup: {
|
||||
backup: 'Backup',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
restore_diagram: 'Restore Diagram',
|
||||
},
|
||||
help: {
|
||||
help: 'Yardım',
|
||||
docs_website: 'Belgeleme',
|
||||
visit_website: "ChartDB'yi Ziyaret Et",
|
||||
join_discord: "Discord'a Katıl",
|
||||
schedule_a_call: 'Bize Ulaş!',
|
||||
@@ -102,8 +106,6 @@ export const tr: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Son kaydedilen',
|
||||
saved: 'Kaydedildi',
|
||||
diagrams: 'Diyagramlar',
|
||||
|
||||
loading_diagram: 'Diyagram yükleniyor...',
|
||||
deselect_all: 'Hepsini Seçme',
|
||||
select_all: 'Hepsini Seç',
|
||||
@@ -123,6 +125,13 @@ export const tr: LanguageTranslation = {
|
||||
add_table: 'Tablo Ekle',
|
||||
filter: 'Filtrele',
|
||||
collapse: 'Hepsini Daralt',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Alanlar',
|
||||
nullable: 'Boş Bırakılabilir?',
|
||||
@@ -142,6 +151,8 @@ export const tr: LanguageTranslation = {
|
||||
comments: 'Yorumlar',
|
||||
no_comments: 'Yorum yok',
|
||||
delete_field: 'Alanı Sil',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'İndeks Özellikleri',
|
||||
@@ -362,6 +373,20 @@ export const tr: LanguageTranslation = {
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Bir Bir',
|
||||
one_to_many: 'Bir Çok',
|
||||
@@ -375,8 +400,8 @@ export const tr: LanguageTranslation = {
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Tabloyu Düzenle',
|
||||
delete_table: 'Tabloyu Sil',
|
||||
// TODO: Translate
|
||||
duplicate_table: 'Duplicate Table',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Translate
|
||||
|
||||
@@ -4,44 +4,46 @@ export const uk: LanguageTranslation = {
|
||||
translation: {
|
||||
menu: {
|
||||
file: {
|
||||
file: 'файл',
|
||||
new: 'новий',
|
||||
open: 'відкрити',
|
||||
save: 'зберегти',
|
||||
import_database: 'Імпорт бази даних',
|
||||
file: 'Файл',
|
||||
new: 'Новий',
|
||||
open: 'Відкрити',
|
||||
save: 'Зберегти',
|
||||
import: 'Імпорт бази даних',
|
||||
export_sql: 'Експорт SQL',
|
||||
export_as: 'Експортувати як',
|
||||
delete_diagram: 'Видалити діаграму',
|
||||
exit: 'вийти',
|
||||
exit: 'Вийти',
|
||||
},
|
||||
edit: {
|
||||
edit: 'редагувати',
|
||||
edit: 'Редагувати',
|
||||
undo: 'Скасувати',
|
||||
redo: 'Повторити',
|
||||
clear: 'очистити',
|
||||
clear: 'Очистити',
|
||||
},
|
||||
view: {
|
||||
view: 'переглянути',
|
||||
view: 'Перегляд',
|
||||
show_sidebar: 'Показати бічну панель',
|
||||
hide_sidebar: 'Приховати бічну панель',
|
||||
hide_cardinality: 'Приховати потужність',
|
||||
show_cardinality: 'Показати кардинальність',
|
||||
zoom_on_scroll: 'Збільшити прокручування',
|
||||
zoom_on_scroll: 'Масштабувати прокручуванням',
|
||||
theme: 'Тема',
|
||||
show_dependencies: 'Показати залежності',
|
||||
hide_dependencies: 'Приховати залежності',
|
||||
show_minimap: 'Показати мінімапу',
|
||||
hide_minimap: 'Приховати мінімапу',
|
||||
},
|
||||
// TODO: Translate
|
||||
share: {
|
||||
share: 'Share',
|
||||
export_diagram: 'Export Diagram',
|
||||
import_diagram: 'Import Diagram',
|
||||
backup: {
|
||||
backup: 'Резервне копіювання',
|
||||
export_diagram: 'Експорт діаграми',
|
||||
restore_diagram: 'Відновити діаграму',
|
||||
},
|
||||
help: {
|
||||
help: 'Допомога',
|
||||
visit_website: 'Відвідайте ChartDB',
|
||||
help: 'Довідка',
|
||||
docs_website: 'Документація',
|
||||
visit_website: 'Сайт ChartDB',
|
||||
join_discord: 'Приєднуйтесь до нас в Діскорд',
|
||||
schedule_a_call: 'Поговоріть з нами!',
|
||||
schedule_a_call: 'Забронювати зустріч!',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -54,18 +56,18 @@ export const uk: LanguageTranslation = {
|
||||
},
|
||||
|
||||
clear_diagram_alert: {
|
||||
title: 'Чітка діаграма',
|
||||
title: 'Очистити діаграму',
|
||||
description:
|
||||
'Цю дію не можна скасувати. Це назавжди видалить усі дані на діаграмі.',
|
||||
cancel: 'Скасувати',
|
||||
clear: 'очистити',
|
||||
clear: 'Очистити',
|
||||
},
|
||||
|
||||
reorder_diagram_alert: {
|
||||
title: 'Діаграма зміни порядку',
|
||||
title: 'Перевпорядкувати діаграму',
|
||||
description:
|
||||
'Ця дія перевпорядкує всі таблиці на діаграмі. Хочете продовжити?',
|
||||
reorder: 'Змінити порядок',
|
||||
reorder: 'Перевпорядкувати',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
|
||||
@@ -90,24 +92,23 @@ export const uk: LanguageTranslation = {
|
||||
},
|
||||
|
||||
theme: {
|
||||
system: 'система',
|
||||
light: 'світлий',
|
||||
dark: 'Темний',
|
||||
system: 'Системна',
|
||||
light: 'Світла',
|
||||
dark: 'Темна',
|
||||
},
|
||||
|
||||
zoom: {
|
||||
on: 'увімкнути',
|
||||
off: 'вимкнути',
|
||||
on: 'Увімкнути',
|
||||
off: 'Вимкнути',
|
||||
},
|
||||
|
||||
last_saved: 'Востаннє збережено',
|
||||
saved: 'Збережено',
|
||||
diagrams: 'Діаграми',
|
||||
loading_diagram: 'Діаграма завантаження...',
|
||||
deselect_all: 'Зняти вибір із усіх',
|
||||
loading_diagram: 'Завантаження діаграми…',
|
||||
deselect_all: 'Зняти виділення з усіх',
|
||||
select_all: 'Вибрати усі',
|
||||
clear: 'Очистити',
|
||||
show_more: 'показати більше',
|
||||
show_more: 'Показати більше',
|
||||
show_less: 'Показати менше',
|
||||
copy_to_clipboard: 'Копіювати в буфер обміну',
|
||||
copied: 'Скопійовано!',
|
||||
@@ -115,47 +116,55 @@ export const uk: LanguageTranslation = {
|
||||
side_panel: {
|
||||
schema: 'Схема:',
|
||||
filter_by_schema: 'Фільтрувати за схемою',
|
||||
search_schema: 'Схема пошуку...',
|
||||
search_schema: 'Пошук схеми…',
|
||||
no_schemas_found: 'Схеми не знайдено.',
|
||||
view_all_options: 'Переглянути всі параметри...',
|
||||
view_all_options: 'Переглянути всі параметри…',
|
||||
tables_section: {
|
||||
tables: 'Таблиці',
|
||||
add_table: 'Додати таблицю',
|
||||
filter: 'фільтр',
|
||||
filter: 'Фільтр',
|
||||
collapse: 'Згорнути все',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'поля',
|
||||
nullable: 'Зведений нанівець?',
|
||||
fields: 'Поля',
|
||||
nullable: 'Може бути Null?',
|
||||
primary_key: 'Первинний ключ',
|
||||
indexes: 'Індекси',
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Без коментарів',
|
||||
no_comments: 'Немає коментарів',
|
||||
add_field: 'Додати поле',
|
||||
add_index: 'Додати індекс',
|
||||
index_select_fields: 'Виберіть поля',
|
||||
no_types_found: 'Типи не знайдено',
|
||||
field_name: "Ім'я",
|
||||
field_name: 'Назва поля',
|
||||
field_type: 'Тип',
|
||||
field_actions: {
|
||||
title: 'Атрибути полів',
|
||||
unique: 'Унікальний',
|
||||
unique: 'Унікальне',
|
||||
comments: 'Коментарі',
|
||||
no_comments: 'Без коментарів',
|
||||
no_comments: 'Немає коментарів',
|
||||
delete_field: 'Видалити поле',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Атрибути індексу',
|
||||
name: "Ім'я",
|
||||
name: 'Назва індекса',
|
||||
unique: 'Унікальний',
|
||||
delete_index: 'Видалити індекс',
|
||||
},
|
||||
table_actions: {
|
||||
title: 'Дії таблиці',
|
||||
title: 'Дії з таблицею',
|
||||
change_schema: 'Змінити схему',
|
||||
add_field: 'Додати поле',
|
||||
add_index: 'Додати індекс',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Дублювати таблицю',
|
||||
delete_table: 'Видалити таблицю',
|
||||
},
|
||||
},
|
||||
@@ -165,14 +174,14 @@ export const uk: LanguageTranslation = {
|
||||
},
|
||||
},
|
||||
relationships_section: {
|
||||
relationships: 'стосунки',
|
||||
filter: 'фільтр',
|
||||
add_relationship: "Додати зв'язок",
|
||||
relationships: 'Звʼязки',
|
||||
filter: 'Фільтр',
|
||||
add_relationship: 'Додати звʼязок',
|
||||
collapse: 'Згорнути все',
|
||||
relationship: {
|
||||
primary: 'Первинна таблиця',
|
||||
foreign: 'Посилання на таблицю',
|
||||
cardinality: 'Кардинальність',
|
||||
cardinality: 'Звʼязок',
|
||||
delete_relationship: 'Видалити',
|
||||
relationship_actions: {
|
||||
title: 'Дії',
|
||||
@@ -180,17 +189,17 @@ export const uk: LanguageTranslation = {
|
||||
},
|
||||
},
|
||||
empty_state: {
|
||||
title: 'Жодних стосунків',
|
||||
description: 'Створіть зв’язок для з’єднання таблиць',
|
||||
title: 'Звʼязків немає',
|
||||
description: 'Створіть звʼязок для зʼєднання таблиць',
|
||||
},
|
||||
},
|
||||
dependencies_section: {
|
||||
dependencies: 'Залежності',
|
||||
filter: 'фільтр',
|
||||
filter: 'Фільтр',
|
||||
collapse: 'Згорнути все',
|
||||
dependency: {
|
||||
table: 'Таблиця',
|
||||
dependent_table: 'Залежний вид',
|
||||
dependent_table: 'Залежне подання',
|
||||
delete_dependency: 'Видалити',
|
||||
dependency_actions: {
|
||||
title: 'Дії',
|
||||
@@ -207,34 +216,34 @@ export const uk: LanguageTranslation = {
|
||||
toolbar: {
|
||||
zoom_in: 'Збільшити',
|
||||
zoom_out: 'Зменшити',
|
||||
save: 'зберегти',
|
||||
save: 'Зберегти',
|
||||
show_all: 'Показати все',
|
||||
undo: 'Скасувати',
|
||||
redo: 'Повторити',
|
||||
reorder_diagram: 'Діаграма зміни порядку',
|
||||
highlight_overlapping_tables: 'Виділіть таблиці, що перекриваються',
|
||||
reorder_diagram: 'Перевпорядкувати діаграму',
|
||||
highlight_overlapping_tables: 'Показати таблиці, що перекриваються',
|
||||
},
|
||||
|
||||
new_diagram_dialog: {
|
||||
database_selection: {
|
||||
title: 'Що таке ваша база даних?',
|
||||
title: 'Яка у вас база даних?',
|
||||
description:
|
||||
'Кожна база даних має свої унікальні особливості та можливості.',
|
||||
check_examples_long: 'Перевірте приклади',
|
||||
check_examples_long: 'Подивіться приклади',
|
||||
check_examples_short: 'Приклади',
|
||||
},
|
||||
|
||||
import_database: {
|
||||
title: 'Імпортуйте вашу базу даних',
|
||||
database_edition: 'Редакція бази даних:',
|
||||
database_edition: 'Варіант бази даних:',
|
||||
step_1: 'Запустіть цей сценарій у своїй базі даних:',
|
||||
step_2: 'Вставте сюди результат сценарію:',
|
||||
script_results_placeholder: 'Результати сценарію тут...',
|
||||
script_results_placeholder: 'Результати сценарію має бути тут…',
|
||||
ssms_instructions: {
|
||||
button_text: 'SSMS Інструкції',
|
||||
title: 'Інструкції',
|
||||
step_1: 'Перейдіть до Інструменти > Опції > Результати запиту > SQL Сервер.',
|
||||
step_2: 'Якщо ви використовуєте «Результати в сітку», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
||||
step_2: 'Якщо ви використовуєте «Results to Grid», змініть максимальну кількість символів, отриманих для даних, що не є XML (встановіть на 9999999).',
|
||||
},
|
||||
instructions_link: 'Потрібна допомога? Подивіться як',
|
||||
check_script_result: 'Перевірте результат сценарію',
|
||||
@@ -242,20 +251,19 @@ export const uk: LanguageTranslation = {
|
||||
|
||||
cancel: 'Скасувати',
|
||||
back: 'Назад',
|
||||
// TODO: Translate
|
||||
import_from_file: 'Import from File',
|
||||
import_from_file: 'Імпортувати з файлу',
|
||||
empty_diagram: 'Порожня діаграма',
|
||||
continue: 'Продовжити',
|
||||
import: 'Імпорт',
|
||||
},
|
||||
|
||||
open_diagram_dialog: {
|
||||
title: 'Відкрита діаграма',
|
||||
title: 'Відкрити діаграму',
|
||||
description:
|
||||
'Виберіть діаграму, яку потрібно відкрити, зі списку нижче.',
|
||||
table_columns: {
|
||||
name: "Ім'я",
|
||||
created_at: 'Створено в',
|
||||
name: 'Назва',
|
||||
created_at: 'Створено0',
|
||||
last_modified: 'Востаннє змінено',
|
||||
tables_count: 'Таблиці',
|
||||
},
|
||||
@@ -269,23 +277,23 @@ export const uk: LanguageTranslation = {
|
||||
'Експортуйте свою схему діаграми в {{databaseType}} сценарій',
|
||||
close: 'Закрити',
|
||||
loading: {
|
||||
text: 'ШІ створює SQL для {{databaseType}}...',
|
||||
text: 'ШІ створює SQL для {{databaseType}}…',
|
||||
description: 'Це має зайняти до 30 секунд.',
|
||||
},
|
||||
error: {
|
||||
message:
|
||||
"Помилка створення сценарію SQL. Спробуйте пізніше або <0>зв'яжіться з нами</0>.",
|
||||
'Помилка створення сценарію SQL. Спробуйте пізніше або <0>звʼяжіться з нами</0>.',
|
||||
description:
|
||||
'Не соромтеся використовувати свій OPENAI_TOKEN, дивіться посібник <0>тут</0>.',
|
||||
},
|
||||
},
|
||||
|
||||
create_relationship_dialog: {
|
||||
title: 'Створити відносини',
|
||||
title: 'Створити звʼязок',
|
||||
primary_table: 'Первинна таблиця',
|
||||
primary_field: 'Первинне поле',
|
||||
referenced_table: 'Посилання на таблицю',
|
||||
referenced_field: 'Поле посилання',
|
||||
referenced_table: 'Звʼязана таблиця',
|
||||
referenced_field: 'Повʼязане поле',
|
||||
primary_table_placeholder: 'Виберіть таблицю',
|
||||
primary_field_placeholder: 'Виберіть поле',
|
||||
referenced_table_placeholder: 'Виберіть таблицю',
|
||||
@@ -305,12 +313,12 @@ export const uk: LanguageTranslation = {
|
||||
new_tables:
|
||||
'<bold>{{newTablesNumber}}</bold> будуть додані нові таблиці.',
|
||||
new_relationships:
|
||||
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові відносини.',
|
||||
'<bold>{{newRelationshipsNumber}}</bold> будуть створені нові звʼязки.',
|
||||
tables_override:
|
||||
'<bold>{{tablesOverrideNumber}}</bold> таблиці будуть перезаписані.',
|
||||
proceed: 'Ви хочете продовжити?',
|
||||
},
|
||||
import: 'Імпорт',
|
||||
import: 'Імпортувати',
|
||||
cancel: 'Скасувати',
|
||||
},
|
||||
},
|
||||
@@ -318,83 +326,95 @@ export const uk: LanguageTranslation = {
|
||||
export_image_dialog: {
|
||||
title: 'Експорт зображення',
|
||||
description: 'Виберіть коефіцієнт масштабування для експорту:',
|
||||
scale_1x: '1x Регулярний',
|
||||
scale_1x: '1x Звичайний',
|
||||
scale_2x: '2x (Рекомендовано)',
|
||||
scale_3x: '3x',
|
||||
scale_4x: '4x',
|
||||
cancel: 'Скасувати',
|
||||
export: 'Експорт',
|
||||
export: 'Експортувати',
|
||||
},
|
||||
|
||||
new_table_schema_dialog: {
|
||||
title: 'Виберіть Схему',
|
||||
description:
|
||||
'Наразі відображається кілька схем. Виберіть один для нової таблиці.',
|
||||
'Наразі показується кілька схем. Виберіть одну для нової таблиці.',
|
||||
cancel: 'Скасувати',
|
||||
confirm: 'Підтвердити',
|
||||
},
|
||||
|
||||
update_table_schema_dialog: {
|
||||
title: 'Змінити схему',
|
||||
description: 'Оновити таблицю "{{tableName}}" схему',
|
||||
description: 'Оновити схему таблиці "{{tableName}}"',
|
||||
cancel: 'Скасувати',
|
||||
confirm: 'Змінити',
|
||||
},
|
||||
|
||||
star_us_dialog: {
|
||||
title: 'Допоможіть нам покращитися!',
|
||||
description: 'Хочете позначити нас на Ґітхаб? Це лише один клік!',
|
||||
description: 'Поставне на зірку на GitHub? Це лише один клік!',
|
||||
close: 'Не зараз',
|
||||
confirm: 'звичайно!',
|
||||
confirm: 'Звісно!',
|
||||
},
|
||||
// TODO: Translate
|
||||
export_diagram_dialog: {
|
||||
title: 'Export Diagram',
|
||||
description: 'Choose the format for export:',
|
||||
title: 'Експорт Діаграми',
|
||||
description: 'Оберіть формат експорту:',
|
||||
format_json: 'JSON',
|
||||
cancel: 'Cancel',
|
||||
export: 'Export',
|
||||
cancel: 'Скасувати',
|
||||
export: 'Експортувати',
|
||||
error: {
|
||||
title: 'Error exporting diagram',
|
||||
title: 'Помилка експорут діаграми',
|
||||
description:
|
||||
'Something went wrong. Need help? chartdb.io@gmail.com',
|
||||
'Щось пішло не так. Потрібна допомога? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
import_diagram_dialog: {
|
||||
title: 'Імпорт Діаграми',
|
||||
description: 'Вставте JSON діаграми нижче:',
|
||||
cancel: 'Скасувати',
|
||||
import: 'Імпортувати',
|
||||
error: {
|
||||
title: 'Помилка імпорту діаграми',
|
||||
description:
|
||||
'JSON діаграми є неправильним. Будь ласка, перевірте JSON і спробуйте ще раз. Потрібна допомога? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_diagram_dialog: {
|
||||
title: 'Import Diagram',
|
||||
description: 'Paste the diagram JSON below:',
|
||||
cancel: 'Cancel',
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error importing diagram',
|
||||
description:
|
||||
'The diagram JSON is invalid. Please check the JSON and try again. Need help? chartdb.io@gmail.com',
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Один до одного',
|
||||
one_to_many: 'Один до багатьох',
|
||||
many_to_one: 'Багато до одного',
|
||||
many_to_many: 'Багато до багатьох',
|
||||
one_to_one: 'Один до Одного',
|
||||
one_to_many: 'Один до Багатьох',
|
||||
many_to_one: 'Багато до Одного',
|
||||
many_to_many: 'Багато до Багатьох',
|
||||
},
|
||||
|
||||
canvas_context_menu: {
|
||||
new_table: 'Нова таблиця',
|
||||
new_relationship: 'Нові стосунки',
|
||||
new_relationship: 'Новий звʼязок',
|
||||
},
|
||||
|
||||
table_node_context_menu: {
|
||||
edit_table: 'Редагувати таблицю',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
duplicate_table: 'Дублювати таблицю',
|
||||
delete_table: 'Видалити таблицю',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
// TODO: Add translations
|
||||
snap_to_grid_tooltip: 'Snap to Grid (Hold {{key}})',
|
||||
snap_to_grid_tooltip: 'Вирівнювати за сіткою (Отримуйте {{key}})',
|
||||
|
||||
tool_tips: {
|
||||
double_click_to_edit: 'Двойной клик для редактирования',
|
||||
double_click_to_edit: 'Подвійне клацання для редагування',
|
||||
},
|
||||
|
||||
language_select: {
|
||||
|
||||
@@ -8,7 +8,7 @@ export const vi: LanguageTranslation = {
|
||||
new: 'Tạo mới',
|
||||
open: 'Mở',
|
||||
save: 'Lưu',
|
||||
import_database: 'Nhập cơ sở dữ liệu',
|
||||
import: 'Nhập cơ sở dữ liệu',
|
||||
export_sql: 'Xuất SQL',
|
||||
export_as: 'Xuất thành',
|
||||
delete_diagram: 'Xóa sơ đồ',
|
||||
@@ -30,14 +30,18 @@ export const vi: LanguageTranslation = {
|
||||
theme: 'Chủ đề',
|
||||
show_dependencies: 'Hiển thị các phụ thuộc',
|
||||
hide_dependencies: 'Ẩn các phụ thuộc',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: 'Chia sẻ',
|
||||
backup: {
|
||||
backup: 'Hỗ trợ',
|
||||
export_diagram: 'Xuất sơ đồ',
|
||||
import_diagram: 'Nhập sơ đồ',
|
||||
restore_diagram: 'Khôi phục sơ đồ',
|
||||
},
|
||||
help: {
|
||||
help: 'Trợ giúp',
|
||||
docs_website: 'Tài liệu',
|
||||
visit_website: 'Truy cập ChartDB',
|
||||
join_discord: 'Tham gia Discord',
|
||||
schedule_a_call: 'Trò chuyện cùng chúng tôi!',
|
||||
@@ -101,7 +105,6 @@ export const vi: LanguageTranslation = {
|
||||
|
||||
last_saved: 'Đã lưu lần cuối',
|
||||
saved: 'Đã lưu',
|
||||
diagrams: 'Sơ đồ',
|
||||
loading_diagram: 'Đang tải sơ đồ...',
|
||||
deselect_all: 'Bỏ chọn tất cả',
|
||||
select_all: 'Chọn tất cả',
|
||||
@@ -122,6 +125,12 @@ export const vi: LanguageTranslation = {
|
||||
add_table: 'Thêm bảng',
|
||||
filter: 'Lọc',
|
||||
collapse: 'Thu gọn tất cả',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: 'Trường',
|
||||
@@ -142,6 +151,8 @@ export const vi: LanguageTranslation = {
|
||||
comments: 'Bình luận',
|
||||
no_comments: 'Không có bình luận',
|
||||
delete_field: 'Xóa trường',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: 'Thuộc tính chỉ mục',
|
||||
@@ -368,6 +379,20 @@ export const vi: LanguageTranslation = {
|
||||
'Sơ đồ ở dạng JSON không hợp lệ. Vui lòng kiểm tra JSON và thử lại. Bạn cần trợ giúp? chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: 'Quan hệ một-một',
|
||||
one_to_many: 'Quan hệ một-nhiều',
|
||||
@@ -384,6 +409,7 @@ export const vi: LanguageTranslation = {
|
||||
edit_table: 'Sửa bảng',
|
||||
duplicate_table: 'Nhân đôi bảng',
|
||||
delete_table: 'Xóa bảng',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: 'Căn lưới (Giữ phím {{key}})',
|
||||
|
||||
@@ -8,7 +8,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
new: '新建',
|
||||
open: '打开',
|
||||
save: '保存',
|
||||
import_database: '导入数据库',
|
||||
import: '导入数据库',
|
||||
export_sql: '导出 SQL 语句',
|
||||
export_as: '导出为',
|
||||
delete_diagram: '删除关系图',
|
||||
@@ -30,14 +30,18 @@ export const zh_CN: LanguageTranslation = {
|
||||
theme: '主题',
|
||||
show_dependencies: '展示依赖',
|
||||
hide_dependencies: '隐藏依赖',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '分享',
|
||||
backup: {
|
||||
backup: '备份',
|
||||
export_diagram: '导出关系图',
|
||||
import_diagram: '导入关系图',
|
||||
restore_diagram: '还原图表',
|
||||
},
|
||||
help: {
|
||||
help: '帮助',
|
||||
docs_website: '文档',
|
||||
visit_website: '访问 ChartDB',
|
||||
join_discord: '在 Discord 上加入我们',
|
||||
schedule_a_call: '和我们交流!',
|
||||
@@ -98,7 +102,6 @@ export const zh_CN: LanguageTranslation = {
|
||||
|
||||
last_saved: '上次保存时间:',
|
||||
saved: '已保存',
|
||||
diagrams: '关系图',
|
||||
loading_diagram: '加载关系图...',
|
||||
deselect_all: '取消全选',
|
||||
select_all: '全选',
|
||||
@@ -119,6 +122,12 @@ export const zh_CN: LanguageTranslation = {
|
||||
add_table: '添加表',
|
||||
filter: '筛选',
|
||||
collapse: '全部折叠',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: '字段',
|
||||
@@ -139,6 +148,8 @@ export const zh_CN: LanguageTranslation = {
|
||||
comments: '注释',
|
||||
no_comments: '空',
|
||||
delete_field: '删除字段',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引属性',
|
||||
@@ -364,6 +375,20 @@ export const zh_CN: LanguageTranslation = {
|
||||
'关系图 JSON 无效,请检查 JSON 后重试。需要帮助? 联系 chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: '一对一',
|
||||
one_to_many: '一对多',
|
||||
@@ -380,6 +405,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
edit_table: '编辑表',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: '删除表',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: '对齐到网格(按住 {{key}})',
|
||||
@@ -395,7 +421,7 @@ export const zh_CN: LanguageTranslation = {
|
||||
};
|
||||
|
||||
export const zh_CNMetadata: LanguageMetadata = {
|
||||
name: 'Chinese',
|
||||
name: 'Chinese (Simplified)',
|
||||
nativeName: '简体中文',
|
||||
code: 'zh_CN',
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
new: '新增',
|
||||
open: '開啟',
|
||||
save: '儲存',
|
||||
import_database: '匯入資料庫',
|
||||
import: '匯入資料庫',
|
||||
export_sql: '匯出 SQL',
|
||||
export_as: '匯出為特定格式',
|
||||
delete_diagram: '刪除圖表',
|
||||
@@ -30,14 +30,18 @@ export const zh_TW: LanguageTranslation = {
|
||||
theme: '主題',
|
||||
show_dependencies: '顯示相依性',
|
||||
hide_dependencies: '隱藏相依性',
|
||||
// TODO: Translate
|
||||
show_minimap: 'Show Mini Map',
|
||||
hide_minimap: 'Hide Mini Map',
|
||||
},
|
||||
share: {
|
||||
share: '分享',
|
||||
backup: {
|
||||
backup: '備份',
|
||||
export_diagram: '匯出圖表',
|
||||
import_diagram: '匯入圖表',
|
||||
restore_diagram: '恢復圖表',
|
||||
},
|
||||
help: {
|
||||
help: '幫助',
|
||||
docs_website: '文件',
|
||||
visit_website: '訪問 ChartDB 網站',
|
||||
join_discord: '加入 Discord',
|
||||
schedule_a_call: '與我們聯絡!',
|
||||
@@ -98,7 +102,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
|
||||
last_saved: '上次儲存於',
|
||||
saved: '已儲存',
|
||||
diagrams: '圖表',
|
||||
loading_diagram: '正在載入圖表...',
|
||||
deselect_all: '取消所有選取',
|
||||
select_all: '全選',
|
||||
@@ -119,6 +122,12 @@ export const zh_TW: LanguageTranslation = {
|
||||
add_table: '新增表格',
|
||||
filter: '篩選',
|
||||
collapse: '全部摺疊',
|
||||
// TODO: Translate
|
||||
clear: 'Clear Filter',
|
||||
no_results: 'No tables found matching your filter.',
|
||||
// TODO: Translate
|
||||
show_list: 'Show Table List',
|
||||
show_dbml: 'Show DBML Editor',
|
||||
|
||||
table: {
|
||||
fields: '欄位',
|
||||
@@ -139,6 +148,8 @@ export const zh_TW: LanguageTranslation = {
|
||||
comments: '註解',
|
||||
no_comments: '無註解',
|
||||
delete_field: '刪除欄位',
|
||||
// TODO: Translate
|
||||
character_length: 'Max Length',
|
||||
},
|
||||
index_actions: {
|
||||
title: '索引屬性',
|
||||
@@ -363,6 +374,20 @@ export const zh_TW: LanguageTranslation = {
|
||||
'圖表的 JSON 無效。請檢查 JSON 並再試一次。如需幫助,請聯繫 chartdb.io@gmail.com',
|
||||
},
|
||||
},
|
||||
// TODO: Translate
|
||||
import_dbml_dialog: {
|
||||
example_title: 'Import Example DBML',
|
||||
title: 'Import DBML',
|
||||
description: 'Import a database schema from DBML format.',
|
||||
import: 'Import',
|
||||
cancel: 'Cancel',
|
||||
skip_and_empty: 'Skip & Empty',
|
||||
show_example: 'Show Example',
|
||||
error: {
|
||||
title: 'Error',
|
||||
description: 'Failed to parse DBML. Please check the syntax.',
|
||||
},
|
||||
},
|
||||
relationship_type: {
|
||||
one_to_one: '一對一',
|
||||
one_to_many: '一對多',
|
||||
@@ -379,6 +404,7 @@ export const zh_TW: LanguageTranslation = {
|
||||
edit_table: '編輯表格',
|
||||
duplicate_table: 'Duplicate Table', // TODO: Translate
|
||||
delete_table: '刪除表格',
|
||||
add_relationship: 'Add Relationship', // TODO: Translate
|
||||
},
|
||||
|
||||
snap_to_grid_tooltip: '對齊網格(按住 {{key}})',
|
||||
@@ -395,6 +421,6 @@ export const zh_TW: LanguageTranslation = {
|
||||
|
||||
export const zh_TWMetadata: LanguageMetadata = {
|
||||
nativeName: '繁體中文',
|
||||
name: 'Traditional Chinese',
|
||||
name: 'Chinese (Traditional)',
|
||||
code: 'zh_TW',
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const clickhouseDataTypes: readonly DataType[] = [
|
||||
export const clickhouseDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'uint8', id: 'uint8' },
|
||||
{ name: 'uint16', id: 'uint16' },
|
||||
@@ -48,25 +48,41 @@ export const clickhouseDataTypes: readonly DataType[] = [
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'char large object', id: 'char_large_object' },
|
||||
{ name: 'char varying', id: 'char_varying' },
|
||||
{ name: 'char varying', id: 'char_varying', hasCharMaxLength: true },
|
||||
{ name: 'character large object', id: 'character_large_object' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'nchar large object', id: 'nchar_large_object' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying' },
|
||||
{ name: 'nchar varying', id: 'nchar_varying', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'national character large object',
|
||||
id: 'national_character_large_object',
|
||||
},
|
||||
{ name: 'national character varying', id: 'national_character_varying' },
|
||||
{ name: 'national char varying', id: 'national_char_varying' },
|
||||
{ name: 'national character', id: 'national_character' },
|
||||
{ name: 'national char', id: 'national_char' },
|
||||
{
|
||||
name: 'national character varying',
|
||||
id: 'national_character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national char varying',
|
||||
id: 'national_char_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{
|
||||
name: 'national character',
|
||||
id: 'national_character',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'national char', id: 'national_char', hasCharMaxLength: true },
|
||||
{ name: 'binary large object', id: 'binary_large_object' },
|
||||
{ name: 'binary varying', id: 'binary_varying' },
|
||||
{ name: 'fixedstring', id: 'fixedstring' },
|
||||
{ name: 'binary varying', id: 'binary_varying', hasCharMaxLength: true },
|
||||
{ name: 'fixedstring', id: 'fixedstring', hasCharMaxLength: true },
|
||||
{ name: 'string', id: 'string' },
|
||||
|
||||
// Date Types
|
||||
|
||||
@@ -13,12 +13,16 @@ export interface DataType {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface DataTypeData extends DataType {
|
||||
hasCharMaxLength?: boolean;
|
||||
}
|
||||
|
||||
export const dataTypeSchema: z.ZodType<DataType> = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
});
|
||||
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
||||
export const dataTypeMap: Record<DatabaseType, readonly DataTypeData[]> = {
|
||||
[DatabaseType.GENERIC]: genericDataTypes,
|
||||
[DatabaseType.POSTGRESQL]: postgresDataTypes,
|
||||
[DatabaseType.MYSQL]: mysqlDataTypes,
|
||||
@@ -26,6 +30,7 @@ export const dataTypeMap: Record<DatabaseType, readonly DataType[]> = {
|
||||
[DatabaseType.MARIADB]: mariadbDataTypes,
|
||||
[DatabaseType.SQLITE]: sqliteDataTypes,
|
||||
[DatabaseType.CLICKHOUSE]: clickhouseDataTypes,
|
||||
[DatabaseType.COCKROACHDB]: postgresDataTypes,
|
||||
} as const;
|
||||
|
||||
const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
||||
@@ -42,6 +47,7 @@ const compatibleTypes: Record<DatabaseType, Record<string, string[]>> = {
|
||||
[DatabaseType.MARIADB]: {},
|
||||
[DatabaseType.SQLITE]: {},
|
||||
[DatabaseType.CLICKHOUSE]: {},
|
||||
[DatabaseType.COCKROACHDB]: {},
|
||||
[DatabaseType.GENERIC]: {},
|
||||
};
|
||||
|
||||
@@ -60,3 +66,23 @@ export function areFieldTypesCompatible(
|
||||
dbCompatibleTypes[type2.id]?.includes(type1.id)
|
||||
);
|
||||
}
|
||||
|
||||
export const dataTypes = Object.values(dataTypeMap).flat();
|
||||
|
||||
export const dataTypeDataToDataType = (
|
||||
dataTypeData: DataTypeData
|
||||
): DataType => ({
|
||||
id: dataTypeData.id,
|
||||
name: dataTypeData.name,
|
||||
});
|
||||
|
||||
export const findDataTypeDataById = (
|
||||
id: string,
|
||||
databaseType?: DatabaseType
|
||||
): DataTypeData | undefined => {
|
||||
const dataTypesOptions = databaseType
|
||||
? dataTypeMap[databaseType]
|
||||
: dataTypes;
|
||||
|
||||
return dataTypesOptions.find((dataType) => dataType.id === id);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const genericDataTypes: readonly DataType[] = [
|
||||
export const genericDataTypes: readonly DataTypeData[] = [
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
@@ -22,6 +22,6 @@ export const genericDataTypes: readonly DataType[] = [
|
||||
{ name: 'time', id: 'time' },
|
||||
{ name: 'timestamp', id: 'timestamp' },
|
||||
{ name: 'uuid', id: 'uuid' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
] as const;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mariadbDataTypes: readonly DataType[] = [
|
||||
export const mariadbDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
@@ -23,10 +23,10 @@ export const mariadbDataTypes: readonly DataType[] = [
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const mysqlDataTypes: readonly DataType[] = [
|
||||
export const mysqlDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'tinyint', id: 'tinyint' },
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
@@ -23,10 +23,10 @@ export const mysqlDataTypes: readonly DataType[] = [
|
||||
{ name: 'year', id: 'year' },
|
||||
|
||||
// String Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'tinyblob', id: 'tinyblob' },
|
||||
{ name: 'blob', id: 'blob' },
|
||||
{ name: 'mediumblob', id: 'mediumblob' },
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const postgresDataTypes: readonly DataType[] = [
|
||||
export const postgresDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'smallint', id: 'smallint' },
|
||||
{ name: 'integer', id: 'integer' },
|
||||
@@ -15,9 +15,13 @@ export const postgresDataTypes: readonly DataType[] = [
|
||||
{ name: 'money', id: 'money' },
|
||||
|
||||
// Character Types
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'character varying', id: 'character_varying' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{
|
||||
name: 'character varying',
|
||||
id: 'character_varying',
|
||||
hasCharMaxLength: true,
|
||||
},
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Binary Data Types
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqlServerDataTypes: readonly DataType[] = [
|
||||
export const sqlServerDataTypes: readonly DataTypeData[] = [
|
||||
// Exact Numerics
|
||||
{ name: 'bigint', id: 'bigint' },
|
||||
{ name: 'bit', id: 'bit' },
|
||||
@@ -25,18 +25,18 @@ export const sqlServerDataTypes: readonly DataType[] = [
|
||||
{ name: 'time', id: 'time' },
|
||||
|
||||
// Character Strings
|
||||
{ name: 'char', id: 'char' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'char', id: 'char', hasCharMaxLength: true },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'text', id: 'text' },
|
||||
|
||||
// Unicode Character Strings
|
||||
{ name: 'nchar', id: 'nchar' },
|
||||
{ name: 'nvarchar', id: 'nvarchar' },
|
||||
{ name: 'nchar', id: 'nchar', hasCharMaxLength: true },
|
||||
{ name: 'nvarchar', id: 'nvarchar', hasCharMaxLength: true },
|
||||
{ name: 'ntext', id: 'ntext' },
|
||||
|
||||
// Binary Strings
|
||||
{ name: 'binary', id: 'binary' },
|
||||
{ name: 'varbinary', id: 'varbinary' },
|
||||
{ name: 'binary', id: 'binary', hasCharMaxLength: true },
|
||||
{ name: 'varbinary', id: 'varbinary', hasCharMaxLength: true },
|
||||
{ name: 'image', id: 'image' },
|
||||
|
||||
// Other Data Types
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DataType } from './data-types';
|
||||
import type { DataTypeData } from './data-types';
|
||||
|
||||
export const sqliteDataTypes: readonly DataType[] = [
|
||||
export const sqliteDataTypes: readonly DataTypeData[] = [
|
||||
// Numeric Types
|
||||
{ name: 'integer', id: 'integer' },
|
||||
{ name: 'real', id: 'real' },
|
||||
@@ -12,6 +12,9 @@ export const sqliteDataTypes: readonly DataType[] = [
|
||||
// Blob Type
|
||||
{ name: 'blob', id: 'blob' },
|
||||
|
||||
// Blob Type
|
||||
{ name: 'json', id: 'json' },
|
||||
|
||||
// Date/Time Types (SQLite uses TEXT, REAL, or INTEGER types for dates and times)
|
||||
{ name: 'date', id: 'date' },
|
||||
{ name: 'datetime', id: 'datetime' },
|
||||
@@ -19,6 +22,6 @@ export const sqliteDataTypes: readonly DataType[] = [
|
||||
{ name: 'int', id: 'int' },
|
||||
{ name: 'float', id: 'float' },
|
||||
{ name: 'boolean', id: 'boolean' },
|
||||
{ name: 'varchar', id: 'varchar' },
|
||||
{ name: 'varchar', id: 'varchar', hasCharMaxLength: true },
|
||||
{ name: 'decimal', id: 'decimal' },
|
||||
] as const;
|
||||
|
||||
82
src/lib/data/export-metadata/export-per-type/common.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
|
||||
export function isFunction(value: string): boolean {
|
||||
// Common SQL functions
|
||||
const functionPatterns = [
|
||||
/^CURRENT_TIMESTAMP$/i,
|
||||
/^NOW\(\)$/i,
|
||||
/^GETDATE\(\)$/i,
|
||||
/^CURRENT_DATE$/i,
|
||||
/^CURRENT_TIME$/i,
|
||||
/^UUID\(\)$/i,
|
||||
/^NEWID\(\)$/i,
|
||||
/^NEXT VALUE FOR/i,
|
||||
/^IDENTITY\s*\(\d+,\s*\d+\)$/i,
|
||||
];
|
||||
return functionPatterns.some((pattern) => pattern.test(value.trim()));
|
||||
}
|
||||
|
||||
export function isKeyword(value: string): boolean {
|
||||
// Common SQL keywords that can be used as default values
|
||||
const keywords = [
|
||||
'NULL',
|
||||
'TRUE',
|
||||
'FALSE',
|
||||
'CURRENT_TIMESTAMP',
|
||||
'CURRENT_DATE',
|
||||
'CURRENT_TIME',
|
||||
'CURRENT_USER',
|
||||
'SESSION_USER',
|
||||
'SYSTEM_USER',
|
||||
];
|
||||
return keywords.includes(value.trim().toUpperCase());
|
||||
}
|
||||
|
||||
export function strHasQuotes(value: string): boolean {
|
||||
return /^['"].*['"]$/.test(value.trim());
|
||||
}
|
||||
|
||||
export function exportFieldComment(comment: string): string {
|
||||
if (!comment) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return comment
|
||||
.split('\n')
|
||||
.map((commentLine) => ` -- ${commentLine}\n`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
export function getInlineFK(table: DBTable, diagram: Diagram): string {
|
||||
if (!diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const fks = diagram.relationships
|
||||
.filter((r) => r.sourceTableId === table.id)
|
||||
.map((r) => {
|
||||
const targetTable = diagram.tables?.find(
|
||||
(t) => t.id === r.targetTableId
|
||||
);
|
||||
const sourceField = table.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable?.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!targetTable || !sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const targetTableName = targetTable.schema
|
||||
? `"${targetTable.schema}"."${targetTable.name}"`
|
||||
: `"${targetTable.name}"`;
|
||||
|
||||
return ` FOREIGN KEY ("${sourceField.name}") REFERENCES ${targetTableName}("${targetField.name}")`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return fks.join(',\n');
|
||||
}
|
||||
247
src/lib/data/export-metadata/export-per-type/mssql.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import {
|
||||
exportFieldComment,
|
||||
isFunction,
|
||||
isKeyword,
|
||||
strHasQuotes,
|
||||
} from './common';
|
||||
import type { Diagram } from '@/lib/domain/diagram';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DBField } from '@/lib/domain/db-field';
|
||||
import type { DBRelationship } from '@/lib/domain/db-relationship';
|
||||
|
||||
function parseMSSQLDefault(field: DBField): string {
|
||||
if (!field.default) {
|
||||
return '';
|
||||
}
|
||||
|
||||
let defaultValue = field.default.trim();
|
||||
|
||||
// Remove type casting for SQL Server
|
||||
defaultValue = defaultValue.split('::')[0];
|
||||
|
||||
// Handle nextval sequences for SQL Server
|
||||
if (defaultValue.includes('nextval')) {
|
||||
return 'IDENTITY(1,1)';
|
||||
}
|
||||
|
||||
// Special handling for SQL Server DEFAULT values
|
||||
if (defaultValue.match(/^\(\(.*\)\)$/)) {
|
||||
// Handle ((0)), ((0.00)) style defaults
|
||||
return defaultValue.replace(/^\(\(|\)\)$/g, '');
|
||||
} else if (defaultValue.match(/^\(N'.*'\)$/)) {
|
||||
// Handle (N'value') style defaults
|
||||
const innerValue = defaultValue.replace(/^\(N'|'\)$/g, '');
|
||||
return `N'${innerValue}'`;
|
||||
} else if (defaultValue.match(/^\(NULL\)$/i)) {
|
||||
// Handle (NULL) defaults
|
||||
return 'NULL';
|
||||
} else if (defaultValue.match(/^\(getdate\(\)\)$/i)) {
|
||||
// Handle (getdate()) defaults
|
||||
return 'getdate()';
|
||||
} else if (defaultValue.match(/^\('?\*'?\)$/i) || defaultValue === '*') {
|
||||
// Handle ('*') or (*) or * defaults - common for "all" values
|
||||
return "N'*'";
|
||||
} else if (defaultValue.match(/^\((['"])(.*)\1\)$/)) {
|
||||
// Handle ('value') or ("value") style defaults
|
||||
const matches = defaultValue.match(/^\((['"])(.*)\1\)$/);
|
||||
return matches ? `N'${matches[2]}'` : defaultValue;
|
||||
}
|
||||
|
||||
// Handle special characters that could be interpreted as operators
|
||||
const sqlServerSpecialChars = /[*+\-/%&|^!=<>~]/;
|
||||
if (sqlServerSpecialChars.test(defaultValue)) {
|
||||
// If the value contains special characters and isn't already properly quoted
|
||||
if (
|
||||
!strHasQuotes(defaultValue) &&
|
||||
!isFunction(defaultValue) &&
|
||||
!isKeyword(defaultValue)
|
||||
) {
|
||||
return `N'${defaultValue.replace(/'/g, "''")}'`;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
strHasQuotes(defaultValue) ||
|
||||
isFunction(defaultValue) ||
|
||||
isKeyword(defaultValue) ||
|
||||
/^-?\d+(\.\d+)?$/.test(defaultValue)
|
||||
) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return `'${defaultValue}'`;
|
||||
}
|
||||
|
||||
export function exportMSSQL(diagram: Diagram): string {
|
||||
if (!diagram.tables || !diagram.relationships) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tables = diagram.tables;
|
||||
const relationships = diagram.relationships;
|
||||
|
||||
// Create CREATE SCHEMA statements for all schemas
|
||||
let sqlScript = '';
|
||||
const schemas = new Set<string>();
|
||||
|
||||
tables.forEach((table) => {
|
||||
if (table.schema) {
|
||||
schemas.add(table.schema);
|
||||
}
|
||||
});
|
||||
|
||||
// Add schema creation statements
|
||||
schemas.forEach((schema) => {
|
||||
sqlScript += `IF NOT EXISTS (SELECT * FROM sys.schemas WHERE name = '${schema}')\nBEGIN\n EXEC('CREATE SCHEMA [${schema}]');\nEND;\n\n`;
|
||||
});
|
||||
|
||||
// Generate table creation SQL
|
||||
sqlScript += tables
|
||||
.map((table: DBTable) => {
|
||||
// Skip views
|
||||
if (table.isView) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const tableName = table.schema
|
||||
? `[${table.schema}].[${table.name}]`
|
||||
: `[${table.name}]`;
|
||||
|
||||
return `${
|
||||
table.comments ? `/**\n${table.comments}\n*/\n` : ''
|
||||
}CREATE TABLE ${tableName} (\n${table.fields
|
||||
.map((field: DBField) => {
|
||||
const fieldName = `[${field.name}]`;
|
||||
const typeName = field.type.name;
|
||||
|
||||
// Handle SQL Server specific type formatting
|
||||
let typeWithSize = typeName;
|
||||
if (field.characterMaximumLength) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'varchar' ||
|
||||
typeName.toLowerCase() === 'nvarchar' ||
|
||||
typeName.toLowerCase() === 'char' ||
|
||||
typeName.toLowerCase() === 'nchar'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.characterMaximumLength})`;
|
||||
}
|
||||
} else if (field.precision && field.scale) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision}, ${field.scale})`;
|
||||
}
|
||||
} else if (field.precision) {
|
||||
if (
|
||||
typeName.toLowerCase() === 'decimal' ||
|
||||
typeName.toLowerCase() === 'numeric'
|
||||
) {
|
||||
typeWithSize = `${typeName}(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
const notNull = field.nullable ? '' : ' NOT NULL';
|
||||
|
||||
// Check if identity column
|
||||
const identity = field.default
|
||||
?.toLowerCase()
|
||||
.includes('identity')
|
||||
? ' IDENTITY(1,1)'
|
||||
: '';
|
||||
|
||||
const unique =
|
||||
!field.primaryKey && field.unique ? ' UNIQUE' : '';
|
||||
|
||||
// Handle default value using SQL Server specific parser
|
||||
const defaultValue =
|
||||
field.default &&
|
||||
!field.default.toLowerCase().includes('identity')
|
||||
? ` DEFAULT ${parseMSSQLDefault(field)}`
|
||||
: '';
|
||||
|
||||
// Do not add PRIMARY KEY as a column constraint - will add as table constraint
|
||||
return `${exportFieldComment(field.comments ?? '')} ${fieldName} ${typeWithSize}${notNull}${identity}${unique}${defaultValue}`;
|
||||
})
|
||||
.join(',\n')}${
|
||||
table.fields.filter((f) => f.primaryKey).length > 0
|
||||
? `,\n PRIMARY KEY (${table.fields
|
||||
.filter((f) => f.primaryKey)
|
||||
.map((f) => `[${f.name}]`)
|
||||
.join(', ')})`
|
||||
: ''
|
||||
}\n);\n\n${table.indexes
|
||||
.map((index) => {
|
||||
const indexName = table.schema
|
||||
? `[${table.schema}_${index.name}]`
|
||||
: `[${index.name}]`;
|
||||
const indexFields = index.fieldIds
|
||||
.map((fieldId) => {
|
||||
const field = table.fields.find(
|
||||
(f) => f.id === fieldId
|
||||
);
|
||||
return field ? `[${field.name}]` : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
// SQL Server has a limit of 32 columns in an index
|
||||
if (indexFields.length > 32) {
|
||||
const warningComment = `/* WARNING: This index originally had ${indexFields.length} columns. It has been truncated to 32 columns due to SQL Server's index column limit. */\n`;
|
||||
console.warn(
|
||||
`Warning: Index ${indexName} on table ${tableName} has ${indexFields.length} columns. SQL Server limits indexes to 32 columns. The index will be truncated.`
|
||||
);
|
||||
indexFields.length = 32;
|
||||
return indexFields.length > 0
|
||||
? `${warningComment}CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||
: '';
|
||||
}
|
||||
|
||||
return indexFields.length > 0
|
||||
? `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX ${indexName}\nON ${tableName} (${indexFields.join(', ')});\n\n`
|
||||
: '';
|
||||
})
|
||||
.join('')}`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings (views)
|
||||
.join('\n');
|
||||
|
||||
// Generate foreign keys
|
||||
sqlScript += `\n${relationships
|
||||
.map((r: DBRelationship) => {
|
||||
const sourceTable = tables.find((t) => t.id === r.sourceTableId);
|
||||
const targetTable = tables.find((t) => t.id === r.targetTableId);
|
||||
|
||||
if (
|
||||
!sourceTable ||
|
||||
!targetTable ||
|
||||
sourceTable.isView ||
|
||||
targetTable.isView
|
||||
) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceField = sourceTable.fields.find(
|
||||
(f) => f.id === r.sourceFieldId
|
||||
);
|
||||
const targetField = targetTable.fields.find(
|
||||
(f) => f.id === r.targetFieldId
|
||||
);
|
||||
|
||||
if (!sourceField || !targetField) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const sourceTableName = sourceTable.schema
|
||||
? `[${sourceTable.schema}].[${sourceTable.name}]`
|
||||
: `[${sourceTable.name}]`;
|
||||
const targetTableName = targetTable.schema
|
||||
? `[${targetTable.schema}].[${targetTable.name}]`
|
||||
: `[${targetTable.name}]`;
|
||||
|
||||
return `ALTER TABLE ${sourceTableName}\nADD CONSTRAINT [${r.name}] FOREIGN KEY([${sourceField.name}]) REFERENCES ${targetTableName}([${targetField.name}]);\n`;
|
||||
})
|
||||
.filter(Boolean) // Remove empty strings
|
||||
.join('\n')}`;
|
||||
|
||||
return sqlScript;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { Diagram } from '../../domain/diagram';
|
||||
import { OPENAI_API_KEY } from '@/lib/env';
|
||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { OPENAI_API_KEY, OPENAI_API_ENDPOINT, LLM_MODEL_NAME } from '@/lib/env';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DataType } from '../data-types/data-types';
|
||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||
import { exportMSSQL } from './export-per-type/mssql';
|
||||
|
||||
export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
const { tables, relationships } = diagram;
|
||||
@@ -12,6 +13,10 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (diagram.databaseType === DatabaseType.SQL_SERVER) {
|
||||
return exportMSSQL(diagram);
|
||||
}
|
||||
|
||||
// Filter out the tables that are views
|
||||
const nonViewTables = tables.filter((table) => !table.isView);
|
||||
|
||||
@@ -110,8 +115,22 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
|
||||
// Remove the type cast part after :: if it exists
|
||||
if (fieldDefault.includes('::')) {
|
||||
const endedWithParentheses = fieldDefault.endsWith(')');
|
||||
fieldDefault = fieldDefault.split('::')[0];
|
||||
|
||||
if (
|
||||
(fieldDefault.startsWith('(') &&
|
||||
!fieldDefault.endsWith(')')) ||
|
||||
endedWithParentheses
|
||||
) {
|
||||
fieldDefault += ')';
|
||||
}
|
||||
}
|
||||
|
||||
if (fieldDefault === `('now')`) {
|
||||
fieldDefault = `now()`;
|
||||
}
|
||||
|
||||
sqlScript += ` DEFAULT ${fieldDefault}`;
|
||||
}
|
||||
|
||||
@@ -196,6 +215,26 @@ export const exportBaseSQL = (diagram: Diagram): string => {
|
||||
return sqlScript;
|
||||
};
|
||||
|
||||
const validateConfiguration = () => {
|
||||
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
|
||||
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
|
||||
const modelName = window?.env?.LLM_MODEL_NAME ?? LLM_MODEL_NAME;
|
||||
|
||||
// If using custom endpoint and model, don't require OpenAI API key
|
||||
if (baseUrl && modelName) {
|
||||
return { useCustomEndpoint: true };
|
||||
}
|
||||
|
||||
// If using OpenAI's service, require API key
|
||||
if (apiKey) {
|
||||
return { useCustomEndpoint: false };
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
'Configuration Error: Either provide an OpenAI API key or both a custom endpoint and model name'
|
||||
);
|
||||
};
|
||||
|
||||
export const exportSQL = async (
|
||||
diagram: Diagram,
|
||||
databaseType: DatabaseType,
|
||||
@@ -206,6 +245,10 @@ export const exportSQL = async (
|
||||
}
|
||||
): Promise<string> => {
|
||||
const sqlScript = exportBaseSQL(diagram);
|
||||
if (databaseType === DatabaseType.SQL_SERVER) {
|
||||
return sqlScript;
|
||||
}
|
||||
|
||||
const cacheKey = await generateCacheKey(databaseType, sqlScript);
|
||||
|
||||
const cachedResult = getFromCache(cacheKey);
|
||||
@@ -213,43 +256,76 @@ export const exportSQL = async (
|
||||
return cachedResult;
|
||||
}
|
||||
|
||||
// Validate configuration before proceeding
|
||||
const { useCustomEndpoint } = validateConfiguration();
|
||||
|
||||
const [{ streamText, generateText }, { createOpenAI }] = await Promise.all([
|
||||
import('ai'),
|
||||
import('@ai-sdk/openai'),
|
||||
]);
|
||||
|
||||
const openai = createOpenAI({
|
||||
apiKey: window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY,
|
||||
});
|
||||
const apiKey = window?.env?.OPENAI_API_KEY ?? OPENAI_API_KEY;
|
||||
const baseUrl = window?.env?.OPENAI_API_ENDPOINT ?? OPENAI_API_ENDPOINT;
|
||||
const modelName =
|
||||
window?.env?.LLM_MODEL_NAME ??
|
||||
LLM_MODEL_NAME ??
|
||||
'gpt-4o-mini-2024-07-18';
|
||||
|
||||
let config: { apiKey: string; baseUrl?: string };
|
||||
|
||||
if (useCustomEndpoint) {
|
||||
config = {
|
||||
apiKey: apiKey,
|
||||
baseUrl: baseUrl,
|
||||
};
|
||||
} else {
|
||||
config = {
|
||||
apiKey: apiKey,
|
||||
};
|
||||
}
|
||||
|
||||
const openai = createOpenAI(config);
|
||||
|
||||
const prompt = generateSQLPrompt(databaseType, sqlScript);
|
||||
|
||||
if (options?.stream) {
|
||||
const { textStream, text: textPromise } = await streamText({
|
||||
model: openai('gpt-4o-mini-2024-07-18'),
|
||||
try {
|
||||
if (options?.stream) {
|
||||
const { textStream, text: textPromise } = await streamText({
|
||||
model: openai(modelName),
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
for await (const textPart of textStream) {
|
||||
if (options.signal?.aborted) {
|
||||
return '';
|
||||
}
|
||||
options.onResultStream(textPart);
|
||||
}
|
||||
|
||||
const text = await textPromise;
|
||||
|
||||
setInCache(cacheKey, text);
|
||||
return text;
|
||||
}
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai(modelName),
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
for await (const textPart of textStream) {
|
||||
if (options.signal?.aborted) {
|
||||
return '';
|
||||
}
|
||||
options.onResultStream(textPart);
|
||||
}
|
||||
|
||||
const text = await textPromise;
|
||||
|
||||
setInCache(cacheKey, text);
|
||||
return text;
|
||||
} catch (error: unknown) {
|
||||
console.error('Error generating SQL:', error);
|
||||
if (error instanceof Error && error.message.includes('API key')) {
|
||||
throw new Error(
|
||||
'Error: Please check your API configuration. If using a custom endpoint, make sure the endpoint URL is correct.'
|
||||
);
|
||||
}
|
||||
throw new Error(
|
||||
'Error generating SQL script. Please check your configuration and try again.'
|
||||
);
|
||||
}
|
||||
|
||||
const { text } = await generateText({
|
||||
model: openai('gpt-4o-mini-2024-07-18'),
|
||||
prompt: prompt,
|
||||
});
|
||||
|
||||
setInCache(cacheKey, text);
|
||||
return text;
|
||||
};
|
||||
|
||||
function getMySQLDataTypeSize(type: DataType) {
|
||||
@@ -383,6 +459,12 @@ const generateSQLPrompt = (databaseType: DatabaseType, sqlScript: string) => {
|
||||
- **Conditional Logic**: Ensure the script uses SQLite-compatible syntax and does not include unsupported features.
|
||||
`,
|
||||
clickhouse: '',
|
||||
cockroachdb: `
|
||||
- **Sequence Creation**: Use \`CREATE SEQUENCE IF NOT EXISTS\` for sequence creation.
|
||||
- **Table and Index Creation**: Use \`CREATE TABLE IF NOT EXISTS\` and \`CREATE INDEX IF NOT EXISTS\` to avoid errors if the object already exists.
|
||||
- **Serial and Identity Columns**: For auto-increment columns, use \`SERIAL\` or \`GENERATED BY DEFAULT AS IDENTITY\`.
|
||||
- **Conditional Statements**: Utilize PostgreSQL’s support for \`IF NOT EXISTS\` in relevant \`CREATE\` statements.
|
||||
`,
|
||||
};
|
||||
|
||||
const dialectInstruction = dialectInstructionMap[databaseType] ?? '';
|
||||
|
||||
196
src/lib/data/import-metadata/scripts/cockroachdb-script.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
const cockroachdbFilters = `
|
||||
AND connamespace::regnamespace::text NOT IN ('pg_extension', 'crdb_internal')
|
||||
`;
|
||||
|
||||
const cockroachdbColFilter = `
|
||||
AND cols.table_schema NOT IN ('pg_extension', 'crdb_internal')
|
||||
`;
|
||||
|
||||
const cockroachdbTableFilter = `
|
||||
AND tbls.table_schema NOT IN ('pg_extension', 'crdb_internal')
|
||||
`;
|
||||
|
||||
const cockroachdbIndexesFilter = `
|
||||
WHERE schema_name NOT IN ('pg_extension', 'crdb_internal')
|
||||
`;
|
||||
|
||||
const cockroachdbViewsFilter = `
|
||||
AND views.schemaname NOT IN ('pg_extension', 'crdb_internal')
|
||||
`;
|
||||
|
||||
export const cockroachdbQuery = `${`/* CockroachDB - PostgreSQL edition */`}
|
||||
WITH fk_info AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name::TEXT, '"', ''), '"',
|
||||
',"table":"', replace(table_name::TEXT, '"', ''), '"',
|
||||
',"column":"', replace(fk_column::TEXT, '"', ''), '"',
|
||||
',"foreign_key_name":"', foreign_key_name::TEXT, '"',
|
||||
',"reference_schema":"', COALESCE(reference_schema::TEXT, 'public'), '"',
|
||||
',"reference_table":"', reference_table::TEXT, '"',
|
||||
',"reference_column":"', reference_column::TEXT, '"',
|
||||
',"fk_def":"', replace(fk_def::TEXT, '"', ''),
|
||||
'"}')), ',') as fk_metadata
|
||||
FROM (
|
||||
SELECT c.conname AS foreign_key_name,
|
||||
n.nspname AS schema_name,
|
||||
CASE
|
||||
WHEN position('.' in conrelid::regclass::text) > 0
|
||||
THEN split_part(conrelid::regclass::text, '.', 2)
|
||||
ELSE conrelid::regclass::text
|
||||
END AS table_name,
|
||||
a.attname AS fk_column,
|
||||
nr.nspname AS reference_schema,
|
||||
CASE
|
||||
WHEN position('.' in confrelid::regclass::text) > 0
|
||||
THEN split_part(confrelid::regclass::text, '.', 2)
|
||||
ELSE confrelid::regclass::text
|
||||
END AS reference_table,
|
||||
af.attname AS reference_column,
|
||||
pg_get_constraintdef(c.oid) as fk_def
|
||||
FROM
|
||||
pg_constraint AS c
|
||||
JOIN
|
||||
pg_attribute AS a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
|
||||
JOIN
|
||||
pg_class AS cl ON cl.oid = c.conrelid
|
||||
JOIN
|
||||
pg_namespace AS n ON n.oid = cl.relnamespace
|
||||
JOIN
|
||||
pg_attribute AS af ON af.attnum = ANY(c.confkey) AND af.attrelid = c.confrelid
|
||||
JOIN
|
||||
pg_class AS clf ON clf.oid = c.confrelid
|
||||
JOIN
|
||||
pg_namespace AS nr ON nr.oid = clf.relnamespace
|
||||
WHERE
|
||||
c.contype = 'f'
|
||||
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${cockroachdbFilters}
|
||||
) AS x
|
||||
), pk_info AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', replace(schema_name::TEXT, '"', ''), '"',
|
||||
',"table":"', replace(pk_table::TEXT, '"', ''), '"',
|
||||
',"column":"', replace(pk_column::TEXT, '"', ''), '"',
|
||||
',"pk_def":"', replace(pk_def::TEXT, '"', ''),
|
||||
'"}')), ',') AS pk_metadata
|
||||
FROM (
|
||||
SELECT connamespace::regnamespace::text AS schema_name,
|
||||
CASE
|
||||
WHEN strpos(conrelid::regclass::text, '.') > 0
|
||||
THEN split_part(conrelid::regclass::text, '.', 2)
|
||||
ELSE conrelid::regclass::text
|
||||
END AS pk_table,
|
||||
unnest(string_to_array(substring(pg_get_constraintdef(oid) FROM '\\((.*?)\\)'), ',')) AS pk_column,
|
||||
pg_get_constraintdef(oid) as pk_def
|
||||
FROM
|
||||
pg_constraint
|
||||
WHERE
|
||||
contype = 'p'
|
||||
AND connamespace::regnamespace::text NOT IN ('information_schema', 'pg_catalog')${cockroachdbFilters}
|
||||
) AS y
|
||||
),
|
||||
indexes_cols AS (
|
||||
SELECT tnsp.nspname AS schema_name,
|
||||
trel.relname AS table_name,
|
||||
null AS index_size,
|
||||
irel.relname AS index_name,
|
||||
am.amname AS index_type,
|
||||
a.attname AS col_name,
|
||||
(CASE WHEN i.indisunique = TRUE THEN 'true' ELSE 'false' END) AS is_unique,
|
||||
irel.reltuples AS cardinality,
|
||||
1 + Array_position(i.indkey, a.attnum) AS column_position,
|
||||
CASE o.OPTION & 1 WHEN 1 THEN 'DESC' ELSE 'ASC' END AS direction,
|
||||
CASE WHEN indpred IS NOT NULL THEN 'true' ELSE 'false' END AS is_partial_index
|
||||
FROM pg_index AS i
|
||||
JOIN pg_class AS trel ON trel.oid = i.indrelid
|
||||
JOIN pg_namespace AS tnsp ON trel.relnamespace = tnsp.oid
|
||||
JOIN pg_class AS irel ON irel.oid = i.indexrelid
|
||||
JOIN pg_am AS am ON irel.relam = am.oid
|
||||
CROSS JOIN LATERAL unnest (i.indkey)
|
||||
WITH ORDINALITY AS c (colnum, ordinality) LEFT JOIN LATERAL unnest (i.indoption)
|
||||
WITH ORDINALITY AS o (option, ordinality)
|
||||
ON c.ordinality = o.ordinality JOIN pg_attribute AS a ON trel.oid = a.attrelid AND a.attnum = c.colnum
|
||||
WHERE tnsp.nspname NOT LIKE 'pg_%'
|
||||
GROUP BY tnsp.nspname, trel.relname, irel.relname, am.amname, i.indisunique, i.indexrelid, irel.reltuples, a.attname, Array_position(i.indkey, a.attnum), o.OPTION, i.indpred
|
||||
),
|
||||
cols AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', cols.table_schema::TEXT,
|
||||
'","table":"', cols.table_name::TEXT,
|
||||
'","name":"', cols.column_name::TEXT,
|
||||
'","ordinal_position":"', cols.ordinal_position::TEXT,
|
||||
'","type":"', LOWER(replace(cols.data_type::TEXT, '"', '')),
|
||||
'","character_maximum_length":"', COALESCE(cols.character_maximum_length::TEXT, 'null'),
|
||||
'","precision":',
|
||||
CASE
|
||||
WHEN cols.data_type = 'numeric' OR cols.data_type = 'decimal'
|
||||
THEN CONCAT('{"precision":', COALESCE(cols.numeric_precision::TEXT, 'null'),
|
||||
',"scale":', COALESCE(cols.numeric_scale::TEXT, 'null'), '}')
|
||||
ELSE 'null'
|
||||
END,
|
||||
',"nullable":', CASE WHEN (cols.IS_NULLABLE = 'YES') THEN 'true' ELSE 'false' END::TEXT,
|
||||
',"default":"', COALESCE(replace(replace(cols.column_default::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
||||
'","collation":"', COALESCE(cols.COLLATION_NAME::TEXT, ''),
|
||||
'","comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
||||
'"}')), ',') AS cols_metadata
|
||||
FROM information_schema.columns cols
|
||||
LEFT JOIN pg_catalog.pg_class c
|
||||
ON c.relname = cols.table_name
|
||||
JOIN pg_catalog.pg_namespace n
|
||||
ON n.oid = c.relnamespace AND n.nspname = cols.table_schema
|
||||
LEFT JOIN pg_catalog.pg_description dsc ON dsc.objoid = c.oid
|
||||
AND dsc.objsubid = cols.ordinal_position
|
||||
WHERE cols.table_schema NOT IN ('information_schema', 'pg_catalog')${cockroachdbColFilter}
|
||||
), indexes_metadata AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', schema_name::TEXT,
|
||||
'","table":"', table_name::TEXT,
|
||||
'","name":"', index_name::TEXT,
|
||||
'","column":"', replace(col_name::TEXT, '"', E'"'),
|
||||
'","index_type":"', index_type::TEXT,
|
||||
'","cardinality":', COALESCE(cardinality::TEXT, '0'),
|
||||
',"size":', COALESCE(index_size::TEXT, 'null'),
|
||||
',"unique":', is_unique::TEXT,
|
||||
',"is_partial_index":', is_partial_index::TEXT,
|
||||
',"column_position":', column_position::TEXT,
|
||||
',"direction":"', LOWER(direction::TEXT),
|
||||
'"}')), ',') AS indexes_metadata
|
||||
FROM indexes_cols x${cockroachdbIndexesFilter}
|
||||
), tbls AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{',
|
||||
'"schema":"', tbls.TABLE_SCHEMA::TEXT, '",',
|
||||
'"table":"', tbls.TABLE_NAME::TEXT, '",',
|
||||
'"rows":', COALESCE((SELECT s.n_live_tup::TEXT
|
||||
FROM pg_stat_user_tables s
|
||||
WHERE tbls.TABLE_SCHEMA = s.schemaname AND tbls.TABLE_NAME = s.relname),
|
||||
'0'), ', "type":"', tbls.TABLE_TYPE::TEXT, '",', '"engine":"",', '"collation":"",',
|
||||
'"comment":"', COALESCE(replace(replace(dsc.description::TEXT, '"', '\\"'), '\\x', '\\\\x'), ''),
|
||||
'"}'
|
||||
)),
|
||||
',') AS tbls_metadata
|
||||
FROM information_schema.tables tbls
|
||||
LEFT JOIN pg_catalog.pg_class c ON c.relname = tbls.TABLE_NAME
|
||||
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
||||
AND n.nspname = tbls.TABLE_SCHEMA
|
||||
LEFT JOIN pg_catalog.pg_description dsc ON dsc.objoid = c.oid
|
||||
AND dsc.objsubid = 0
|
||||
WHERE tbls.TABLE_SCHEMA NOT IN ('information_schema', 'pg_catalog')${cockroachdbTableFilter}
|
||||
), config AS (
|
||||
SELECT array_to_string(
|
||||
array_agg(CONCAT('{"name":"', conf.name, '","value":"', replace(conf.setting, '"', E'"'), '"}')),
|
||||
',') AS config_metadata
|
||||
FROM pg_settings conf
|
||||
), views AS (
|
||||
SELECT array_to_string(array_agg(CONCAT('{"schema":"', views.schemaname::TEXT,
|
||||
'","view_name":"', viewname::TEXT,
|
||||
'","view_definition":"', encode(convert_to(REPLACE(definition::TEXT, '"', '\\"'), 'UTF8'), 'base64'),
|
||||
'"}')),
|
||||
',') AS views_metadata
|
||||
FROM pg_views views
|
||||
WHERE views.schemaname NOT IN ('information_schema', 'pg_catalog')${cockroachdbViewsFilter}
|
||||
)
|
||||
SELECT CONCAT('{ "fk_info": [', COALESCE(fk_metadata, ''),
|
||||
'], "pk_info": [', COALESCE(pk_metadata, ''),
|
||||
'], "columns": [', COALESCE(cols_metadata, ''),
|
||||
'], "indexes": [', COALESCE(indexes_metadata, ''),
|
||||
'], "tables":[', COALESCE(tbls_metadata, ''),
|
||||
'], "views":[', COALESCE(views_metadata, ''),
|
||||
'], "database_name": "', CURRENT_DATABASE(), '', '", "version": "', '',
|
||||
'"}') AS metadata_json_to_import
|
||||
FROM fk_info, pk_info, cols, indexes_metadata, tbls, config, views;
|
||||
`;
|
||||
@@ -275,7 +275,7 @@ FROM fk_info${databaseEdition ? '_' + databaseEdition : ''}, pk_info, cols, inde
|
||||
if (options.databaseClient === DatabaseClient.POSTGRESQL_PSQL) {
|
||||
return `${psqlPreCommand}psql -h HOST_NAME -p PORT -U USER_NAME -d DATABASE_NAME -c "
|
||||
${query.replace(/"/g, '\\"').replace(/\\\\/g, '\\\\\\').replace(/\\x/g, '\\\\x')}
|
||||
" -t -A | pbcopy; LG='\\033[0;32m'; NC='\\033[0m'; echo "You got the resultset ($(pbpaste | wc -c | xargs) characters) in Copy/Paste. \${LG}Go back & paste in ChartDB :)\${NC}";`;
|
||||
" -t -A > output.json;`;
|
||||
}
|
||||
|
||||
return query;
|
||||
|
||||
@@ -7,6 +7,7 @@ import { mariaDBQuery } from './maria-script';
|
||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import type { DatabaseClient } from '@/lib/domain/database-clients';
|
||||
import { clickhouseQuery } from './clickhouse-script';
|
||||
import { cockroachdbQuery } from './cockroachdb-script';
|
||||
|
||||
export type ImportMetadataScripts = Record<
|
||||
DatabaseType,
|
||||
@@ -24,4 +25,5 @@ export const importMetadataScripts: ImportMetadataScripts = {
|
||||
[DatabaseType.SQL_SERVER]: getSqlServerQuery,
|
||||
[DatabaseType.MARIADB]: () => mariaDBQuery,
|
||||
[DatabaseType.CLICKHOUSE]: () => clickhouseQuery,
|
||||
[DatabaseType.COCKROACHDB]: () => cockroachdbQuery,
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ export const sqliteQuery = `WITH fk_info AS (
|
||||
ELSE LOWER(p.type)
|
||||
END,
|
||||
'ordinal_position', p.cid,
|
||||
'nullable', (CASE WHEN p."notnull" = 0 THEN 'true' ELSE 'false' END),
|
||||
'nullable', (CASE WHEN p."notnull" = 0 THEN true ELSE false END),
|
||||
'collation', '',
|
||||
'character_maximum_length',
|
||||
CASE
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
|
||||
const sqlServerQuery = `WITH fk_info AS (
|
||||
const sqlServerQuery = `${`/* SQL Server 2017 and above edition (14.0, 15.0, 16.0, 17.0)*/`}
|
||||
WITH fk_info AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + STRING_AGG(
|
||||
N'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
|
||||
), ','
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
|
||||
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
|
||||
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
|
||||
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
|
||||
'"}') COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) + N']'
|
||||
) AS all_fks_json
|
||||
FROM sys.foreign_keys AS fk
|
||||
@@ -31,299 +34,271 @@ const sqlServerQuery = `WITH fk_info AS (
|
||||
), pk_info AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
|
||||
), ','
|
||||
) + N']'
|
||||
N'[' +
|
||||
STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) + N']'
|
||||
) AS all_pks_json
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
kcu.TABLE_SCHEMA,
|
||||
kcu.TABLE_NAME,
|
||||
kcu.COLUMN_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
JOIN
|
||||
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
||||
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
||||
WHERE
|
||||
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
) pk
|
||||
FROM (
|
||||
SELECT
|
||||
kcu.TABLE_SCHEMA,
|
||||
kcu.TABLE_NAME,
|
||||
kcu.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
||||
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
||||
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
) pk
|
||||
),
|
||||
cols AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + STRING_AGG(
|
||||
JSON_QUERY(N'[' +
|
||||
STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
|
||||
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
|
||||
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
|
||||
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||
'", "type": "' + LOWER(cols.DATA_TYPE) +
|
||||
'", "character_maximum_length": "' +
|
||||
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
|
||||
'", "precision": ' +
|
||||
CASE
|
||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
|
||||
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
|
||||
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
|
||||
ELSE
|
||||
'null'
|
||||
END +
|
||||
', "nullable": "' +
|
||||
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||
'", "default": "' +
|
||||
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), '') +
|
||||
'", "collation": "' +
|
||||
COALESCE(cols.COLLATION_NAME, '') +
|
||||
'"}')
|
||||
), ','
|
||||
) + ']'
|
||||
) AS all_columns_json
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS cols
|
||||
WHERE
|
||||
cols.TABLE_CATALOG = DB_NAME()
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
|
||||
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
|
||||
'", "character_maximum_length": ' +
|
||||
CASE
|
||||
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
|
||||
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
|
||||
END +
|
||||
', "precision": ' +
|
||||
CASE
|
||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
|
||||
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
|
||||
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
|
||||
ELSE 'null'
|
||||
END +
|
||||
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||
', "default": ' +
|
||||
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
||||
', "collation": ' + CASE
|
||||
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
||||
END +
|
||||
N'}') COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) +
|
||||
N']') AS all_columns_json
|
||||
FROM INFORMATION_SCHEMA.COLUMNS cols
|
||||
WHERE cols.TABLE_CATALOG = DB_NAME()
|
||||
),
|
||||
indexes AS (
|
||||
SELECT
|
||||
'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
||||
)
|
||||
), ','
|
||||
) + N']' AS all_indexes_json
|
||||
FROM
|
||||
sys.indexes i
|
||||
JOIN
|
||||
sys.tables t ON i.object_id = t.object_id
|
||||
JOIN
|
||||
sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN
|
||||
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN
|
||||
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
AND i.name IS NOT NULL
|
||||
N'[' +
|
||||
STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
|
||||
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
|
||||
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
|
||||
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
|
||||
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
||||
) COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) +
|
||||
N']' AS all_indexes_json
|
||||
FROM sys.indexes i
|
||||
JOIN sys.tables t ON i.object_id = t.object_id
|
||||
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE s.name LIKE '%' AND i.name IS NOT NULL AND ic.is_included_column = 0
|
||||
),
|
||||
tbls AS (
|
||||
SELECT
|
||||
'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(aggregated.table_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||
'", "table_type": "' + aggregated.table_type COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
|
||||
)
|
||||
), ','
|
||||
) + N']' AS all_tables_json
|
||||
FROM
|
||||
(
|
||||
-- Select from tables
|
||||
SELECT
|
||||
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
||||
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
||||
SUM(p.rows) AS row_count,
|
||||
t.type_desc AS table_type,
|
||||
t.create_date AS creation_date
|
||||
FROM
|
||||
sys.tables t
|
||||
JOIN
|
||||
sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN
|
||||
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
GROUP BY
|
||||
s.name, t.name, t.type_desc, t.create_date
|
||||
N'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
|
||||
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
|
||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
|
||||
) COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) +
|
||||
N']' AS all_tables_json
|
||||
FROM (
|
||||
SELECT
|
||||
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
||||
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
||||
SUM(p.rows) AS row_count,
|
||||
t.type_desc AS table_type,
|
||||
t.create_date AS creation_date
|
||||
FROM sys.tables t
|
||||
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||
WHERE s.name LIKE '%'
|
||||
GROUP BY s.name, t.name, t.type_desc, t.create_date
|
||||
|
||||
UNION ALL
|
||||
UNION ALL
|
||||
|
||||
-- Select from views
|
||||
SELECT
|
||||
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
|
||||
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
|
||||
0 AS row_count, -- Views don't have row counts
|
||||
'VIEW' AS table_type,
|
||||
v.create_date AS creation_date
|
||||
FROM
|
||||
sys.views v
|
||||
JOIN
|
||||
sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
) AS aggregated
|
||||
SELECT
|
||||
COALESCE(REPLACE(s.name, '"', ''), '') AS table_name,
|
||||
COALESCE(REPLACE(v.name, '"', ''), '') AS object_name,
|
||||
0 AS row_count,
|
||||
'VIEW' AS table_type,
|
||||
v.create_date AS creation_date
|
||||
FROM sys.views v
|
||||
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE s.name LIKE '%'
|
||||
) AS aggregated
|
||||
),
|
||||
views AS (
|
||||
SELECT
|
||||
'[' + STRING_AGG(
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + STRING_ESCAPE(COALESCE(s.name, ''), 'json') +
|
||||
'", "view_name": "' + STRING_ESCAPE(COALESCE(v.name, ''), 'json') +
|
||||
'", "view_definition": "' +
|
||||
STRING_ESCAPE(
|
||||
CAST(
|
||||
'' AS XML
|
||||
).value(
|
||||
'xs:base64Binary(sql:column("DefinitionBinary"))',
|
||||
'VARCHAR(MAX)'
|
||||
), 'json') +
|
||||
'"}'
|
||||
)
|
||||
), ','
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
|
||||
'", "view_definition": "' +
|
||||
STRING_ESCAPE(
|
||||
CAST(
|
||||
'' AS XML
|
||||
).value(
|
||||
'xs:base64Binary(sql:column("DefinitionBinary"))',
|
||||
'VARCHAR(MAX)'
|
||||
), 'json') +
|
||||
N'"}') COLLATE DATABASE_DEFAULT
|
||||
), N','
|
||||
) + N']' AS all_views_json
|
||||
FROM
|
||||
sys.views v
|
||||
JOIN
|
||||
sys.schemas s ON v.schema_id = s.schema_id
|
||||
JOIN
|
||||
sys.sql_modules m ON v.object_id = m.object_id
|
||||
FROM sys.views v
|
||||
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||
JOIN sys.sql_modules m ON v.object_id = m.object_id
|
||||
CROSS APPLY
|
||||
(SELECT CONVERT(VARBINARY(MAX), m.definition) AS DefinitionBinary) AS bin
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
WHERE s.name LIKE '%'
|
||||
)
|
||||
SELECT JSON_QUERY(
|
||||
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||
N'{
|
||||
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
||||
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
||||
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
||||
', "tables": ' + ISNULL((SELECT cast(all_tables_json as nvarchar(max)) FROM tbls), N'[]') +
|
||||
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
||||
', "database_name": "' + DB_NAME() + '"' +
|
||||
', "version": ""}'
|
||||
', "database_name": "' + STRING_ESCAPE(DB_NAME(), 'json') +
|
||||
'", "version": ""
|
||||
}'
|
||||
) AS metadata_json_to_import;
|
||||
`;
|
||||
|
||||
const sqlServer2016AndBelowQuery = `WITH fk_info AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + ISNULL(
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(tp_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(tp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "foreign_key_name": "' + COALESCE(REPLACE(fk.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_schema": "' + COALESCE(REPLACE(tr_schema.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_table": "' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "reference_column": "' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "fk_def": "FOREIGN KEY (' + COALESCE(REPLACE(cp.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
') REFERENCES ' + COALESCE(REPLACE(tr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'(' + COALESCE(REPLACE(cr.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
') ON DELETE ' + fk.delete_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
' ON UPDATE ' + fk.update_referential_action_desc COLLATE SQL_Latin1_General_CP1_CI_AS + '"}')
|
||||
)
|
||||
FROM
|
||||
sys.foreign_keys AS fk
|
||||
JOIN
|
||||
sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
|
||||
JOIN
|
||||
sys.tables AS tp ON fkc.parent_object_id = tp.object_id
|
||||
JOIN
|
||||
sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
|
||||
JOIN
|
||||
sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
|
||||
JOIN
|
||||
sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
|
||||
JOIN
|
||||
sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
|
||||
JOIN
|
||||
sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ N']'
|
||||
) AS all_fks_json
|
||||
const sqlServer2016AndBelowQuery = `${`/* SQL Server 2016 and below edition (13.0, 12.0, 11.0..) */`}
|
||||
WITH fk_info AS (
|
||||
SELECT JSON_QUERY('[' +
|
||||
ISNULL(
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tp_schema.name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(tp.name, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||
'", "foreign_key_name": "' + STRING_ESCAPE(COALESCE(REPLACE(fk.name, '"', ''), ''), 'json') +
|
||||
'", "reference_schema": "' + STRING_ESCAPE(COALESCE(REPLACE(tr_schema.name, '"', ''), ''), 'json') +
|
||||
'", "reference_table": "' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||
'", "reference_column": "' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||
'", "fk_def": "FOREIGN KEY (' + STRING_ESCAPE(COALESCE(REPLACE(cp.name, '"', ''), ''), 'json') +
|
||||
') REFERENCES ' + STRING_ESCAPE(COALESCE(REPLACE(tr.name, '"', ''), ''), 'json') +
|
||||
'(' + STRING_ESCAPE(COALESCE(REPLACE(cr.name, '"', ''), ''), 'json') +
|
||||
') ON DELETE ' + STRING_ESCAPE(fk.delete_referential_action_desc, 'json') +
|
||||
' ON UPDATE ' + STRING_ESCAPE(fk.update_referential_action_desc, 'json') +
|
||||
'"}') COLLATE DATABASE_DEFAULT
|
||||
)
|
||||
FROM sys.foreign_keys AS fk
|
||||
JOIN sys.foreign_key_columns AS fkc ON fk.object_id = fkc.constraint_object_id
|
||||
JOIN sys.tables AS tp ON fkc.parent_object_id = tp.object_id
|
||||
JOIN sys.schemas AS tp_schema ON tp.schema_id = tp_schema.schema_id
|
||||
JOIN sys.columns AS cp ON fkc.parent_object_id = cp.object_id AND fkc.parent_column_id = cp.column_id
|
||||
JOIN sys.tables AS tr ON fkc.referenced_object_id = tr.object_id
|
||||
JOIN sys.schemas AS tr_schema ON tr.schema_id = tr_schema.schema_id
|
||||
JOIN sys.columns AS cr ON fkc.referenced_object_id = cr.object_id AND fkc.referenced_column_id = cr.column_id
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ N']') AS all_fks_json
|
||||
),
|
||||
pk_info AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + ISNULL(
|
||||
STUFF((
|
||||
SELECT JSON_QUERY('[' +
|
||||
ISNULL(STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(N'{"schema": "' + COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "pk_def": "PRIMARY KEY (' + pk.COLUMN_NAME COLLATE SQL_Latin1_General_CP1_CI_AS + ')"}')
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.TABLE_NAME, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(pk.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||
'", "pk_def": "PRIMARY KEY (' + STRING_ESCAPE(pk.COLUMN_NAME, 'json') + N')"}') COLLATE DATABASE_DEFAULT
|
||||
)
|
||||
FROM
|
||||
(
|
||||
SELECT
|
||||
kcu.TABLE_SCHEMA,
|
||||
kcu.TABLE_NAME,
|
||||
kcu.COLUMN_NAME
|
||||
FROM
|
||||
INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
JOIN
|
||||
INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
SELECT kcu.TABLE_SCHEMA,
|
||||
kcu.TABLE_NAME,
|
||||
kcu.COLUMN_NAME
|
||||
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
||||
JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS tc
|
||||
ON kcu.CONSTRAINT_NAME = tc.CONSTRAINT_NAME
|
||||
AND kcu.CONSTRAINT_SCHEMA = tc.CONSTRAINT_SCHEMA
|
||||
WHERE
|
||||
tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
WHERE tc.CONSTRAINT_TYPE = 'PRIMARY KEY'
|
||||
) pk
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ N']'
|
||||
) AS all_pks_json
|
||||
+ N']') AS all_pks_json
|
||||
),
|
||||
cols AS (
|
||||
SELECT
|
||||
JSON_QUERY(
|
||||
'[' + ISNULL(
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY('{"schema": "' + COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), '') +
|
||||
'", "table": "' + COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), '') +
|
||||
'", "name": "' + COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), '') +
|
||||
'", "ordinal_position": "' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||
'", "type": "' + LOWER(cols.DATA_TYPE) +
|
||||
'", "character_maximum_length": "' +
|
||||
COALESCE(CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX)), 'null') +
|
||||
'", "precision": ' +
|
||||
CASE
|
||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal') THEN
|
||||
CONCAT('{"precision":', COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null'),
|
||||
',"scale":', COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null'), '}')
|
||||
ELSE
|
||||
'null'
|
||||
END +
|
||||
', "nullable": "' +
|
||||
CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||
'", "default": "' +
|
||||
COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '"'), '') +
|
||||
'", "collation": "' +
|
||||
COALESCE(cols.COLLATION_NAME, '') +
|
||||
'"}')
|
||||
)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS cols
|
||||
WHERE
|
||||
cols.TABLE_CATALOG = DB_NAME()
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ ']'
|
||||
) AS all_columns_json
|
||||
SELECT JSON_QUERY('[' +
|
||||
ISNULL(
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY('{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_SCHEMA, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.TABLE_NAME, '"', ''), ''), 'json') +
|
||||
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(cols.COLUMN_NAME, '"', ''), ''), 'json') +
|
||||
'", "ordinal_position": ' + CAST(cols.ORDINAL_POSITION AS NVARCHAR(MAX)) +
|
||||
', "type": "' + STRING_ESCAPE(LOWER(cols.DATA_TYPE), 'json') +
|
||||
'", "character_maximum_length": ' +
|
||||
CASE
|
||||
WHEN cols.CHARACTER_MAXIMUM_LENGTH IS NULL THEN 'null'
|
||||
ELSE CAST(cols.CHARACTER_MAXIMUM_LENGTH AS NVARCHAR(MAX))
|
||||
END +
|
||||
', "precision": ' +
|
||||
CASE
|
||||
WHEN cols.DATA_TYPE IN ('numeric', 'decimal')
|
||||
THEN '{"precision":' + COALESCE(CAST(cols.NUMERIC_PRECISION AS NVARCHAR(MAX)), 'null') +
|
||||
',"scale":' + COALESCE(CAST(cols.NUMERIC_SCALE AS NVARCHAR(MAX)), 'null') + '}'
|
||||
ELSE 'null'
|
||||
END +
|
||||
', "nullable": ' + CASE WHEN cols.IS_NULLABLE = 'YES' THEN 'true' ELSE 'false' END +
|
||||
', "default": ' +
|
||||
'"' + STRING_ESCAPE(COALESCE(REPLACE(CAST(cols.COLUMN_DEFAULT AS NVARCHAR(MAX)), '"', '\\"'), ''), 'json') + '"' +
|
||||
', "collation": ' +
|
||||
CASE
|
||||
WHEN cols.COLLATION_NAME IS NULL THEN 'null'
|
||||
ELSE '"' + STRING_ESCAPE(cols.COLLATION_NAME, 'json') + '"'
|
||||
END +
|
||||
N'}')
|
||||
)
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS cols
|
||||
WHERE
|
||||
cols.TABLE_CATALOG = DB_NAME()
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ ']') AS all_columns_json
|
||||
),
|
||||
indexes AS (
|
||||
SELECT
|
||||
@@ -331,30 +306,25 @@ indexes AS (
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(t.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "name": "' + COALESCE(REPLACE(i.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "column": "' + COALESCE(REPLACE(c.name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "index_type": "' + LOWER(i.type_desc) COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(t.name, '"', ''), ''), 'json') +
|
||||
'", "name": "' + STRING_ESCAPE(COALESCE(REPLACE(i.name, '"', ''), ''), 'json') +
|
||||
'", "column": "' + STRING_ESCAPE(COALESCE(REPLACE(c.name, '"', ''), ''), 'json') +
|
||||
'", "index_type": "' + STRING_ESCAPE(LOWER(i.type_desc), 'json') +
|
||||
'", "unique": ' + CASE WHEN i.is_unique = 1 THEN 'true' ELSE 'false' END +
|
||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
', "direction": "' + CASE WHEN ic.is_descending_key = 1 THEN 'desc' ELSE 'asc' END +
|
||||
'", "column_position": ' + CAST(ic.key_ordinal AS nvarchar(max)) + N'}'
|
||||
)
|
||||
) COLLATE DATABASE_DEFAULT
|
||||
)
|
||||
FROM
|
||||
sys.indexes i
|
||||
JOIN
|
||||
sys.tables t ON i.object_id = t.object_id
|
||||
JOIN
|
||||
sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN
|
||||
sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN
|
||||
sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
AND i.name IS NOT NULL
|
||||
FROM sys.indexes i
|
||||
JOIN sys.tables t ON i.object_id = t.object_id
|
||||
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
||||
JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id
|
||||
WHERE s.name LIKE '%'
|
||||
AND i.name IS NOT NULL
|
||||
AND ic.is_included_column = 0
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
+ N']' AS all_indexes_json
|
||||
@@ -365,12 +335,12 @@ tbls AS (
|
||||
STUFF((
|
||||
SELECT ',' +
|
||||
CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + COALESCE(REPLACE(aggregated.schema_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "table": "' + COALESCE(REPLACE(aggregated.object_name, '"', ''), '') COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "row_count": "' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||
'", "object_type": "' + aggregated.object_type COLLATE SQL_Latin1_General_CP1_CI_AS +
|
||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + '"}'
|
||||
JSON_QUERY(N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.schema_name, '"', ''), ''), 'json') +
|
||||
'", "table": "' + STRING_ESCAPE(COALESCE(REPLACE(aggregated.table_name, '"', ''), ''), 'json') +
|
||||
'", "row_count": ' + CAST(aggregated.row_count AS NVARCHAR(MAX)) +
|
||||
', "table_type": "' + STRING_ESCAPE(aggregated.table_type, 'json') +
|
||||
'", "creation_date": "' + CONVERT(NVARCHAR(MAX), aggregated.creation_date, 120) + N'"}'
|
||||
)
|
||||
)
|
||||
FROM
|
||||
@@ -378,20 +348,15 @@ tbls AS (
|
||||
-- Select from tables
|
||||
SELECT
|
||||
COALESCE(REPLACE(s.name, '"', ''), '') AS schema_name,
|
||||
COALESCE(REPLACE(t.name, '"', ''), '') AS object_name,
|
||||
COALESCE(REPLACE(t.name, '"', ''), '') AS table_name,
|
||||
SUM(p.rows) AS row_count,
|
||||
t.type_desc AS object_type,
|
||||
t.type_desc AS table_type,
|
||||
t.create_date AS creation_date
|
||||
FROM
|
||||
sys.tables t
|
||||
JOIN
|
||||
sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN
|
||||
sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
GROUP BY
|
||||
s.name, t.name, t.type_desc, t.create_date
|
||||
FROM sys.tables t
|
||||
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
||||
JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0, 1)
|
||||
WHERE s.name LIKE '%'
|
||||
GROUP BY s.name, t.name, t.type_desc, t.create_date
|
||||
|
||||
UNION ALL
|
||||
|
||||
@@ -402,12 +367,9 @@ tbls AS (
|
||||
0 AS row_count, -- Views don't have row counts
|
||||
'VIEW' AS object_type,
|
||||
v.create_date AS creation_date
|
||||
FROM
|
||||
sys.views v
|
||||
JOIN
|
||||
sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
FROM sys.views v
|
||||
JOIN sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE s.name LIKE '%'
|
||||
) AS aggregated
|
||||
FOR XML PATH('')
|
||||
), 1, 1, ''), '')
|
||||
@@ -417,38 +379,40 @@ views AS (
|
||||
SELECT
|
||||
'[' +
|
||||
(
|
||||
SELECT
|
||||
STUFF((
|
||||
SELECT ',' + CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{"schema": "' + COALESCE(REPLACE(s.name, '"', ''), '') +
|
||||
'", "view_name": "' + COALESCE(REPLACE(v.name, '"', ''), '') +
|
||||
'", "view_definition": "' +
|
||||
CAST(
|
||||
(
|
||||
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
|
||||
) AS NVARCHAR(MAX)
|
||||
) + '"}'
|
||||
SELECT STUFF((
|
||||
SELECT ',' + CONVERT(nvarchar(max),
|
||||
JSON_QUERY(
|
||||
N'{
|
||||
"schema": "' + STRING_ESCAPE(COALESCE(REPLACE(s.name, '"', ''), ''), 'json') +
|
||||
'", "view_name": "' + STRING_ESCAPE(COALESCE(REPLACE(v.name, '"', ''), ''), 'json') +
|
||||
'", "view_definition": "' +
|
||||
CAST(
|
||||
(
|
||||
SELECT CAST(OBJECT_DEFINITION(v.object_id) AS VARBINARY(MAX)) FOR XML PATH('')
|
||||
) AS NVARCHAR(MAX)
|
||||
) + N'"}'
|
||||
)
|
||||
)
|
||||
)
|
||||
FROM
|
||||
sys.views v
|
||||
JOIN
|
||||
sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
|
||||
FROM
|
||||
sys.views v
|
||||
JOIN
|
||||
sys.schemas s ON v.schema_id = s.schema_id
|
||||
WHERE
|
||||
s.name LIKE '%'
|
||||
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
|
||||
) + ']' AS all_views_json
|
||||
)
|
||||
SELECT JSON_QUERY(
|
||||
N'{"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||
N'{
|
||||
"fk_info": ' + ISNULL((SELECT cast(all_fks_json as nvarchar(max)) FROM fk_info), N'[]') +
|
||||
', "pk_info": ' + ISNULL((SELECT cast(all_pks_json as nvarchar(max)) FROM pk_info), N'[]') +
|
||||
', "columns": ' + ISNULL((SELECT cast(all_columns_json as nvarchar(max)) FROM cols), N'[]') +
|
||||
', "indexes": ' + ISNULL((SELECT cast(all_indexes_json as nvarchar(max)) FROM indexes), N'[]') +
|
||||
', "tables": ' + ISNULL((SELECT cast(all_objects_json as nvarchar(max)) FROM tbls), N'[]') +
|
||||
', "views": ' + ISNULL((SELECT cast(all_views_json as nvarchar(max)) FROM views), N'[]') +
|
||||
', "database_name": "' + DB_NAME() + '"' +
|
||||
', "version": ""}'
|
||||
', "version": ""
|
||||
}'
|
||||
) AS metadata_json_to_import;`;
|
||||
|
||||
export const getSqlServerQuery = (
|
||||
|
||||
@@ -10,14 +10,20 @@ export const fixMetadataJson = async (
|
||||
return (
|
||||
metadataJson
|
||||
.trim()
|
||||
// First unescape the JSON string
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\\\/g, '\\')
|
||||
.replace(/^[^{]*/, '') // Remove everything before the first '{'
|
||||
.replace(/}[^}]*$/, '}') // Remove everything after the last '}'
|
||||
.replace(/:""([^"]+)""/g, ':"$1"') // Convert :""value"" to :"value"
|
||||
.replace(/""(\w+)""/g, '"$1"') // Convert ""key"" to "key"
|
||||
.replace(/^\s+|\s+$/g, '')
|
||||
.replace(/^"|"$/g, '')
|
||||
.replace(/^'|'$/g, '')
|
||||
.replace(/""""/g, '""') // Remove Quadruple quotes from keys
|
||||
.replace(/"""([^",}]+)"""/g, '"$1"') // Remove tripple quotes from keys
|
||||
.replace(/""([^",}]+)""/g, '"$1"') // Remove double quotes from keys
|
||||
|
||||
/* eslint-disable-next-line no-useless-escape */
|
||||
.replace(/\"/g, '___ESCAPED_QUOTE___') // Temporarily replace empty strings
|
||||
.replace(/(?<=:\s*)""(?=\s*[,}])/g, '___EMPTY___') // Temporarily replace empty strings
|
||||
|
||||