Compare commits

...

154 Commits

Author SHA1 Message Date
Stela Augustinova
137fc6b928 Enhance clipboard formatters to skip empty rows, improving data handling in clipboard operations 2026-04-02 10:29:02 +02:00
Stela Augustinova
9d1d7b7e34 Improve clipboard formatters to omit undefined values, enhancing data integrity in exports 2026-04-01 15:49:35 +02:00
Jan Prochazka
588cd39d7c Merge pull request #1404 from dbgate/feature/fetch-all-button
Add fetch all button
2026-04-01 09:44:04 +02:00
Stela Augustinova
79ebfa9b7a Add fetchAll command to dataGrid menu 2026-03-31 13:37:06 +02:00
Stela Augustinova
0c6b2746d1 Fix file stream reference in jsldata and remove redundant buffer assignment in LoadingDataGridCore 2026-03-31 08:59:33 +02:00
Stela Augustinova
978972c55c Enhance file path validation in streamRows to include symlink resolution and case normalization, improving security and error handling 2026-03-31 08:31:43 +02:00
Stela Augustinova
37854fc577 Refactor fetchAll to trim lines before parsing, improving error handling for malformed data 2026-03-31 06:54:37 +02:00
Stela Augustinova
5537e193a6 Improve fetchAll error handling and cleanup process during streaming and paginated reads 2026-03-31 06:21:06 +02:00
Stela Augustinova
0d42b2b133 Refactor fetchAll cancel function to improve cleanup process and prevent errors 2026-03-30 15:48:35 +02:00
Stela Augustinova
44bd7972d4 Enhance fetchAll functionality with improved error handling and state management 2026-03-30 14:34:57 +02:00
Stela Augustinova
5143eb39f7 Implement fetchAll functionality with streaming support and error handling 2026-03-30 13:30:12 +02:00
Stela Augustinova
cf51883b3e Add checkbox to skip confirmation when fetching all rows 2026-03-26 15:24:25 +01:00
Stela Augustinova
484ca0c78a Reset loaded time reference in reload function 2026-03-26 15:11:11 +01:00
Stela Augustinova
8f5cad0e2c Prevent loading next data when fetching all rows is in progress 2026-03-26 15:03:54 +01:00
Stela Augustinova
988512a571 Update warning message in FetchAllConfirmModal to simplify language 2026-03-26 14:50:09 +01:00
Stela Augustinova
f8bd380051 Optimize fetchAllRows by using a local buffer to reduce array copies and improve performance 2026-03-26 14:19:11 +01:00
Stela Augustinova
281131dbba Enhance fetchAll functionality by adding loading state check 2026-03-26 14:07:12 +01:00
Stela Augustinova
ea3a61077a v7.1.6 2026-03-26 12:47:09 +01:00
Stela Augustinova
d1a898b40d SYNC: Add translations for cloudUnavailable message in multiple languages 2026-03-26 11:11:07 +00:00
Stela Augustinova
a521a81ef0 v7.1.6-premium-beta.1 2026-03-26 11:25:13 +01:00
Stela Augustinova
2505c61975 Add fetch all button 2026-03-26 11:24:05 +01:00
Stela Augustinova
ab5a54dbb6 SYNC: Merge pull request #89 from dbgate/feature/cloud-error 2026-03-26 10:12:05 +00:00
Stela Augustinova
44ad8fa60a Update CHANGELOG for version 7.1.5 2026-03-25 16:59:13 +01:00
Stela Augustinova
5b27a241d7 v7.1.5 2026-03-25 16:21:59 +01:00
Stela Augustinova
084019ca65 v7.1.5-premium-beta.3 2026-03-25 15:21:43 +01:00
Stela Augustinova
ba147af8fe SYNC: v7.1.5-premium-beta.2 2026-03-25 14:08:24 +00:00
Stela Augustinova
1b3f4db07d SYNC: Merge pull request #88 from dbgate/feature/cloud-error 2026-03-25 13:39:00 +00:00
Jan Prochazka
c36705d458 Merge pull request #1395 from dbgate/feature/display-uuid
Feature/display UUID
2026-03-25 10:04:58 +01:00
Stela Augustinova
0e126cb8cf Enhance BinData subType handling to support hexadecimal strings and improve validation 2026-03-25 08:32:03 +01:00
Stela Augustinova
c48183a539 Enhance base64 to UUID conversion with error handling and regex improvements 2026-03-25 08:23:15 +01:00
Stela Augustinova
50f380dbbe Enhance uuidToBase64 function with validation and improve UUID parsing in parseCellValue 2026-03-24 17:15:32 +01:00
Stela Augustinova
66023a9a68 Validate base64 UUID conversion and enhance handling in stringifyCellValue 2026-03-24 17:06:52 +01:00
Stela Augustinova
c3fbc3354c Validate BinData subType to ensure it is an integer between 0 and 255 2026-03-24 16:32:16 +01:00
Jan Prochazka
a7d2ed11f3 SYNC: Merge pull request #86 from dbgate/feature/icon-vulnerability 2026-03-23 12:50:27 +00:00
Stela Augustinova
6a3dc92572 Add uuid to base64 conversion and enhance cell value parsing for UUIDs 2026-03-20 12:46:50 +01:00
Stela Augustinova
e3a4667422 feat: add base64 to UUID conversion and integrate into cell value parsing 2026-03-19 14:50:08 +01:00
Stela Augustinova
c4dd99bba9 Changelog 7.1.4 2026-03-19 13:07:44 +01:00
Stela Augustinova
588b6f9882 v7.1.4 2026-03-19 12:13:37 +01:00
Stela Augustinova
375f69ca1e v7.1.4-alpha.2 2026-03-19 11:13:29 +01:00
Stela Augustinova
a32e5cc139 v7.1.4-alpha.1 2026-03-19 10:56:16 +01:00
CI workflows
8e00137751 chore: auto-update github workflows 2026-03-19 09:33:56 +00:00
Stela Augustinova
003db50833 SYNC: Add missing publish step for rest 2026-03-19 09:33:36 +00:00
Stela Augustinova
bc519c2c20 Changelog 7.1.3 2026-03-18 16:06:01 +01:00
Stela Augustinova
3b41fa8cfa v7.1.3 2026-03-18 15:31:26 +01:00
Stela Augustinova
39ed0f6d2d v7.1.3-premium-beta.7 2026-03-18 14:27:27 +01:00
CI workflows
710f796832 chore: auto-update github workflows 2026-03-18 13:15:43 +00:00
CI workflows
9ec5fb7263 Update pro ref 2026-03-18 13:15:24 +00:00
Stela Augustinova
407db457d5 SYNC: Added new translations and error codes 2026-03-18 13:15:12 +00:00
Jan Prochazka
0c5d2cfcd1 Merge pull request #1393 from dbgate/feature/script-filter
Add cloud content list integration for connection label resolution
2026-03-18 13:55:40 +01:00
CI workflows
87ace375bb chore: auto-update github workflows 2026-03-18 12:54:58 +00:00
CI workflows
d010020f3b Update pro ref 2026-03-18 12:54:34 +00:00
Jan Prochazka
c60227a98f SYNC: Merge pull request #85 from dbgate/feature/proxy-configuration 2026-03-18 12:54:21 +00:00
Stela Augustinova
2824681bff Refactor cloudIdToLabel assignment to use lodash's fromPairs for improved readability 2026-03-18 13:47:45 +01:00
Stela Augustinova
073a3e3946 Add cloud content list integration for connection label resolution 2026-03-18 11:23:31 +01:00
CI workflows
93e91127a0 chore: auto-update github workflows 2026-03-18 08:03:38 +00:00
CI workflows
b60a6cff56 Update pro ref 2026-03-18 08:03:23 +00:00
Jan Prochazka
1f3b1963d9 SYNC: errors assign 2026-03-18 08:03:13 +00:00
SPRINX0\prochazka
4915f57abb v7.1.3-premium-beta.6 2026-03-17 15:35:35 +01:00
Jan Prochazka
97c6fc97d5 Merge pull request #1392 from dbgate/feature/duckdb-integration-test
Synchronize client and instance disconnection methods
2026-03-17 15:34:51 +01:00
Stela Augustinova
b68421bbc3 Synchronize client and instance disconnection methods 2026-03-17 14:45:57 +01:00
SPRINX0\prochazka
2d10559754 v7.1.3-premium-beta.5 2026-03-17 13:38:35 +01:00
CI workflows
b398a7b546 chore: auto-update github workflows 2026-03-17 11:58:40 +00:00
CI workflows
1711d2102d Update pro ref 2026-03-17 11:58:24 +00:00
Jan Prochazka
97cea230f3 SYNC: Merge pull request #83 from dbgate/feature/transaction-isolation 2026-03-17 11:58:10 +00:00
CI workflows
b6a0fe9465 chore: auto-update github workflows 2026-03-17 11:46:56 +00:00
CI workflows
06c50659bb Update pro ref 2026-03-17 11:46:39 +00:00
Jan Prochazka
244b47f548 SYNC: Merge pull request #84 from dbgate/feature/proxy-configuration 2026-03-17 11:46:28 +00:00
Jan Prochazka
b72a244d93 Merge pull request #1389 from dbgate/feature/duckdb-query-result
Fix getColumnsInfo loop to iterate from start to end
2026-03-17 09:55:59 +01:00
Jan Prochazka
c1e069d4dc Merge pull request #1391 from dbgate/feature/script-filter
Refactor connection selection to use a dropdown instead of a button f…
2026-03-17 09:50:01 +01:00
Stela Augustinova
f99994085a Refactor connection selection to use a dropdown instead of a button for improved usability 2026-03-17 09:22:18 +01:00
Stela Augustinova
32fd0dd78c Update @duckdb/node-api dependency to version 1.5.0-r.1 2026-03-16 15:52:01 +01:00
Jan Prochazka
a557b6b2b4 Merge pull request #1388 from dbgate/feature/script-filter
Feature/script filter
2026-03-16 15:27:49 +01:00
Stela Augustinova
e84583c776 Fix getColumnsInfo loop to iterate from start to end 2026-03-16 15:09:31 +01:00
Stela Augustinova
a548b0d543 Refactor connection label assignment to use logical OR for fallback 2026-03-16 15:05:45 +01:00
Stela Augustinova
de94f15383 Fix file reading to correctly handle bytes read from file 2026-03-16 14:41:38 +01:00
Stela Augustinova
7045d986ef Fix file handle management to ensure proper closure in file reading process 2026-03-16 14:31:43 +01:00
Stela Augustinova
de7ae9cf09 Refactor connection filter options 2026-03-16 14:17:06 +01:00
Stela Augustinova
ab3d6888dc Enhance file reading and connection filtering in SavedFilesList component 2026-03-16 14:08:19 +01:00
Stela Augustinova
98a70891f3 Refactor file reading 2026-03-16 08:12:35 +01:00
Stela Augustinova
52e7326a2c Enhance file listing to support front matter parsing and connection filtering 2026-03-16 08:02:03 +01:00
Jan Prochazka
bfd2e3b07a Merge pull request #1382 from dbgate/feature/add-files-button
Enhance drag-and-drop functionality to support Electron file paths
2026-03-12 12:41:31 +01:00
Stela Augustinova
799f5e30d3 Enhance drag-and-drop functionality to support Electron file paths 2026-03-12 10:14:47 +01:00
SPRINX0\prochazka
d3e544c3c0 v7.1.3-premium-beta.4 2026-03-11 08:55:53 +01:00
CI workflows
866fd55834 chore: auto-update github workflows 2026-03-10 10:17:13 +00:00
CI workflows
74ce1fba32 Update pro ref 2026-03-10 10:16:57 +00:00
Jan Prochazka
a11b93b4cc SYNC: Merge pull request #80 from dbgate/feature/loading-fix 2026-03-10 10:16:46 +00:00
CI workflows
066f2baa03 chore: auto-update github workflows 2026-03-10 09:50:36 +00:00
Stela Augustinova
e02396280f SYNC: Add port mappings for DynamoDB and fix formatting in e2e-pro.yaml 2026-03-10 09:50:18 +00:00
CI workflows
a654c80746 chore: auto-update github workflows 2026-03-10 09:32:53 +00:00
CI workflows
3b50f4bd7c Update pro ref 2026-03-10 09:32:34 +00:00
CI workflows
cc1f77f5bc chore: auto-update github workflows 2026-03-10 08:23:51 +00:00
CI workflows
381fce4a82 Update pro ref 2026-03-10 08:23:35 +00:00
Jan Prochazka
bc3be97cee SYNC: Merge pull request #81 from dbgate/feature/dynamo-e2e 2026-03-10 08:22:32 +00:00
Jan Prochazka
1c389208a7 Merge pull request #1378 from dbgate/feature/add-files-button
Import getElectron in ElectronFilesInput component
2026-03-10 09:19:34 +01:00
SPRINX0\prochazka
cbeed2d3d0 v7.1.3-alpha.3 2026-03-09 10:20:49 +01:00
SPRINX0\prochazka
3d974ad144 v7.1.3-alpha.2 2026-03-09 10:01:50 +01:00
SPRINX0\prochazka
749042a05d set version 2026-03-09 09:59:53 +01:00
SPRINX0\prochazka
52413b82ee v7.1.3-alpha.1 2026-03-09 09:22:26 +01:00
SPRINX0\prochazka
212a7ec083 used exact version 2026-03-09 09:21:57 +01:00
SPRINX0\prochazka
cee94fe113 added missing package 2026-03-09 09:20:48 +01:00
Stela Augustinova
e1ead2519a Import getElectron in ElectronFilesInput component 2026-03-09 07:35:34 +01:00
Jan Prochazka
80330a25ac Merge pull request #1372 from dbgate/feature/export-diagram
Add diagram export to png
2026-03-05 10:32:35 +01:00
Stela Augustinova
508470e970 Added import 2026-03-05 10:02:57 +01:00
Stela Augustinova
bc64b4b5c7 Update ToolStripDropDownButton label to use translation for export 2026-03-04 15:36:40 +01:00
Jan Prochazka
48d8494ead SYNC: added CLAUDE.md 2026-03-04 07:42:30 +00:00
SPRINX0\prochazka
2a51d2ed96 SYNC: fix: enhance date handling in zipDataRow function 2026-03-03 16:13:49 +00:00
Stela Augustinova
cfabcc7bf6 Fix import name for ToolStripDropDownButton in DiagramTab.svelte 2026-03-03 17:08:13 +01:00
Stela Augustinova
90fc8fd0fc Add diagram export to png 2026-03-03 16:54:46 +01:00
SPRINX0\prochazka
ff54533e33 v7.1.2 2026-03-02 15:53:28 +01:00
SPRINX0\prochazka
2072f0b5ba SYNC: don't use random data in testing REST service 2026-03-02 14:10:12 +00:00
Jan Prochazka
6efc720a45 SYNC: Merge pull request #78 from dbgate/feature/aitest 2026-03-02 13:28:57 +00:00
SPRINX0\prochazka
c7cb1efe9c v7.1.2-premium-beta.2 2026-03-02 12:59:39 +01:00
SPRINX0\prochazka
e193531246 changelog 2026-03-02 12:58:03 +01:00
CI workflows
2aa53f414e chore: auto-update github workflows 2026-03-02 11:57:43 +00:00
CI workflows
843c15d754 Update pro ref 2026-03-02 11:57:27 +00:00
SPRINX0\prochazka
fb19582088 v7.1.2-premium-beta.1 2026-03-02 10:34:53 +01:00
SPRINX0\prochazka
8040466cbe text 2026-03-02 10:34:16 +01:00
CI workflows
302b4d7acd chore: auto-update github workflows 2026-03-02 09:33:33 +00:00
CI workflows
a8ccc24d46 Update pro ref 2026-03-02 09:33:16 +00:00
Jan Prochazka
b2fb071a7b SYNC: Merge pull request #73 from dbgate/feature/openai-upgrade 2026-03-02 09:33:04 +00:00
SPRINX0\prochazka
204d7b97d5 chore: update CHANGELOG for version 7.1.1 enhancements and fixes 2026-02-27 16:08:33 +01:00
SPRINX0\prochazka
f3da709aac v7.1.1 2026-02-27 15:34:12 +01:00
SPRINX0\prochazka
0ab8afb838 v7.1.1-packer-beta.3 2026-02-27 13:36:37 +01:00
SPRINX0\prochazka
d50999547f v7.1.1-premium-beta.2 2026-02-27 13:36:14 +01:00
CI workflows
04741b0eba chore: auto-update github workflows 2026-02-27 12:35:44 +00:00
SPRINX0\prochazka
ba86fe32e7 comment out azure build 2026-02-27 13:35:24 +01:00
CI workflows
9deb7d7fdc chore: auto-update github workflows 2026-02-27 12:34:08 +00:00
CI workflows
55eb64e5ca Update pro ref 2026-02-27 12:33:52 +00:00
Jan Prochazka
a5f50f3f2b SYNC: Merge pull request #68 from dbgate/feature/dynamodb-plugin 2026-02-27 12:33:39 +00:00
Jan Prochazka
47214eb5b3 SYNC: Merge pull request #72 from dbgate/feature-firebird-fixes 2026-02-27 12:24:38 +00:00
CI workflows
599509d417 chore: auto-update github workflows 2026-02-27 08:20:08 +00:00
CI workflows
9d366fc359 Update pro ref 2026-02-27 08:19:53 +00:00
SPRINX0\prochazka
0e1ed0bde6 SYNC: upgraded dbgate-query-splitter 2026-02-27 08:19:41 +00:00
CI workflows
6ad7824bf2 chore: auto-update github workflows 2026-02-27 08:06:47 +00:00
CI workflows
1174f51c07 Update pro ref 2026-02-27 08:06:31 +00:00
Jan Prochazka
1950dda1ab SYNC: Merge pull request #70 from dbgate/feature/new-gql-query 2026-02-27 08:06:19 +00:00
Jan Prochazka
8231b6d5be SYNC: Merge pull request #71 from dbgate/feature/reset-virtual-scroll 2026-02-27 08:03:36 +00:00
Jan Prochazka
0feacbe6eb Merge pull request #1368 from dbgate/feature/driver-selection
Set default selected item to 'general' in SettingsTab and WidgetIconP…
2026-02-27 08:21:16 +01:00
Jan Prochazka
80b5f5adca SYNC: Merge pull request #65 from dbgate/feature/filter-bigint 2026-02-26 08:48:23 +00:00
CI workflows
13650f36e6 chore: auto-update github workflows 2026-02-26 08:47:23 +00:00
CI workflows
3f58d99069 Update pro ref 2026-02-26 08:47:03 +00:00
CI workflows
0c8a025cf6 chore: auto-update github workflows 2026-02-26 08:33:59 +00:00
CI workflows
5014df4859 Update pro ref 2026-02-26 08:33:42 +00:00
SPRINX0\prochazka
34a491e2ef v7.1.1-premium-beta.1 2026-02-25 14:07:03 +01:00
Jan Prochazka
884e4ca88e SYNC: Merge pull request #67 from dbgate/feature/connfix 2026-02-25 13:01:09 +00:00
CI workflows
a670c5e86c chore: auto-update github workflows 2026-02-25 12:54:58 +00:00
CI workflows
af1fba79be Update pro ref 2026-02-25 12:54:40 +00:00
Jan Prochazka
ac44de0bf4 SYNC: Merge pull request #66 from dbgate/team-premium-permis-fix-2 2026-02-25 12:54:28 +00:00
Stela Augustinova
f013a241ce Merge pull request #1367 from dbgate/feature/disable-cell-data-view
Add setting to disable automatic Cell Data View opening
2026-02-25 10:31:42 +01:00
Stela Augustinova
0e29a7206d Prevent unnecessary updates in handleUserChange when the selected item remains unchanged 2026-02-25 09:27:50 +01:00
Stela Augustinova
689b3f299c Prevent unnecessary updates in handleUserChange when the selected item remains unchanged 2026-02-25 09:20:08 +01:00
Stela Augustinova
02ccb990bd Remove default selected item from SettingsTab in stdCommands and WidgetIconPanel 2026-02-25 09:18:51 +01:00
Stela Augustinova
61fe4f0d57 Set default selected item to 'general' in SettingsTab and WidgetIconPanel 2026-02-25 09:09:17 +01:00
Stela Augustinova
0a920195d5 Add setting to disable automatic Cell Data View opening 2026-02-25 07:14:31 +01:00
190 changed files with 10601 additions and 6746 deletions

View File

@@ -47,7 +47,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -47,7 +47,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
@@ -90,14 +90,6 @@ jobs:
prerelease: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run `packer init` for Azure
run: |
cd ../dbgate-merged/packer
packer init ./azure-ubuntu.pkr.hcl
- name: Run `packer build` for Azure
run: |
cd ../dbgate-merged/packer
packer build ./azure-ubuntu.pkr.hcl
- name: Run `packer init` for AWS
run: |
cd ../dbgate-merged/packer
@@ -114,16 +106,6 @@ jobs:
AWS_ACCESS_KEY_ID: ${{secrets.AWS_ACCESS_KEY_ID}}
AWS_SECRET_ACCESS_KEY: ${{secrets.AWS_SECRET_ACCESS_KEY}}
AWS_DEFAULT_REGION: ${{secrets.AWS_DEFAULT_REGION}}
- name: Delete old Azure VMs
run: |
cd ../dbgate-merged/packer
chmod +x delete-old-azure-images.sh
./delete-old-azure-images.sh
env:
AZURE_CLIENT_ID: ${{secrets.AZURE_CLIENT_ID}}
AZURE_CLIENT_SECRET: ${{secrets.AZURE_CLIENT_SECRET}}
AZURE_TENANT_ID: ${{secrets.AZURE_TENANT_ID}}
AZURE_SUBSCRIPTION_ID: ${{secrets.AZURE_SUBSCRIPTION_ID}}
- name: Delete old AMIs (AWS)
run: |
cd ../dbgate-merged/packer

View File

@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -35,7 +35,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -56,7 +56,10 @@ jobs:
working-directory: packages/sqltree
run: |
npm publish --tag "$NPM_TAG"
- name: Publish rest
working-directory: packages/rest
run: |
npm publish --tag "$NPM_TAG"
- name: Publish api
working-directory: packages/api
run: |

View File

@@ -30,7 +30,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: a7c7947b1253fa0b2104ea86a65450d81dc114a0
ref: 5baa88d0ad253537298e911868579bae0835888d
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
@@ -132,6 +132,10 @@ jobs:
image: redis
ports:
- '16011:6379'
dynamodb:
image: amazon/dynamodb-local
ports:
- '16015:8000'
mssql:
image: mcr.microsoft.com/mssql/server
ports:

View File

@@ -1,16 +1,58 @@
# ChangeLog
Builds:
- docker - build
- npm - npm package dbgate-serve
- app - classic electron app
- mac - application for macOS
- linux - application for linux
- win - application for Windows
- docker - build
- npm - npm package dbgate-serve
- app - classic electron app
- mac - application for macOS
- linux - application for linux
- win - application for Windows
## 7.1.5
- FIXED: Issues with cloud and file loading
- ADDED: Support for displaying MongoDB UUID #1394
- ADDED: SVG icon sanitization
## 7.1.4
- FIXED: NPM installation failure #1375
## 7.1.3
- FIXED: "Add files" button in import/export #1373
- FIXED: Importing XLSX files #1379
- ADDED: Ability to set default transaction isolation level for connections #1376
- ADDED: Option to set transaction isolation level directly in Query Tab #1376
- ADDED: Filtering of SQL Scripts by connection and database name #1377
- ADDED: Proxy configuration support for OData, OpenAPI and GraphQL (Premium) #1381
- CHANGED: Updated DuckDB version to 1.5.0 #1386
- FIXED: DuckDB column order in query result #1385
- FIXED: Administration panel not displayed for authorized users (Team Premium) #1374
## 7.1.2
- ADDED: GraphQL chat - AI chat with GraphQL endpoint (Premium)
- FIXED: Error "400 Provider returned error" in Database Chat (Premium)
- CHANGED: Upgraded AI components to latest versions, improved stability and performance of AI features (Premium)
- ADDED: New LLM models available (GPT-5.1 Codex Mini - now default), Claude Haiku 4.5
- CHANGED: Upgraded some internal building components (svelte-preprocess, typescript)
## 7.1.1
- CHANGED: Fixed some DynamoDB issues, improved filtering performance
- FIXED: Afilter filter scroll issue #1370
- FIXED: Team Premium - filtering by connection in database and table permissions
- FIXED: Team Premium - Creating role and user in PostgreSQL - settings is remembered without reopening new role/user
- FIXED: Team Premium - don't show errors "Connection permission not granted" when no connection is selected
- FIXED: Firebird - improved connectivity & table loading #1324
- ADDED: New GraphQL query option, changed GraphQL query icon (Premium)
## 7.1.0
- ADDED: Support for Amazon DynamoDB (Premium)
- ADDED: Connect to API endpoints - OpenAPI (Swagger), GraphQL and oData (Premium)
- ADDED: Connect to API endpoints - OpenAPI (Swagger), GraphQL and oData (Premium)
- FIXED: Redis key list infinite loading when first key hierarchy segment is numeric (e.g. "0:profile:1234") #1363
- FIXED: Sum of PostgreSQL numeric values always 0 #1354
- FIXED: SQL SERVER Table structure key duplication #1351
@@ -20,10 +62,12 @@ Builds:
- CHANGED: Used rolldown bundler instead of legacy rollup
## 7.0.6
- ADDED: Reset password for Team Premium edition
- ADDED: Encrypting passwords sent to frontend when using SHELL_CONNECTION=1 in Docker Community edition #1357
## 7.0.4
- FIXED: MS SQL server export to CSV does not convert bit FALSE to 0 #1276
- ADDED: MySQL FULLTEXT support #1305
- FIXED: Error messages in Chinese will display garbled characters(MS SQL over ODBC) #1321
@@ -36,6 +80,7 @@ Builds:
- CHANGED: Improved custom connection color palette
## 7.0.3
- FIXED: Optimalized loading MySQL primary keys #1261
- FIXED: Test connection now works for MS Entra authentication #1315
- FIXED: SQL Server - Unable to use 'Is Empty or Null' or 'Has Not Empty Value' filters on a field with data type TEXT #1338
@@ -47,6 +92,7 @@ Builds:
- CHANGED: Upgraded node for DbGate AWS distribution
## 7.0.1
- FIXED: Foreign key actions not detected on PostgreSQL #1323
- FIXED: Vulnerabilities in bundled dependencies: axios, cross-spawn, glob #1322
- FIXED: The JsonB field in the cell data view always displays as null. #1320
@@ -57,6 +103,7 @@ Builds:
- ADDED: Default editor theme is part of application theme now
## 7.0.0
- CHANGED: New design of application, new theme system
- ADDED: Theme AI assistant - create custom themes using AI (Premium)
- CHANGED: Themes are now defined in JSON files, custom themes could be shared via DbGate Cloud
@@ -72,12 +119,15 @@ Builds:
- ADDED: Widget for currently opened tabs
## 6.8.2
- FIXED: Initialize storage database from envoronment variables failed with PostgreSQL
## 6.8.1
- FIXED: Won't navigate to the relevant field on click of a field in columns #1303
## 6.8.0
- ADDED: Form cell view for detailed data inspection and editing in data grids, with multi-row bulk editing support
- CHANGED: Cell data sidebar moved to right side, now is part of data grid
- FIXED: Improved widget resizing algorithm
@@ -96,11 +146,13 @@ Builds:
- ADDED: Import connections from environment variables (Team Premium)
## 6.7.3
- FIXED: Fixed problem in analyser core - in PostgreSQL, after dropping table, dropped table still appeared in structure
- FIXED: PostgreSQL numeric columns do not align right #1254
- ADDED: Custom thousands separator #1213
## 6.7.2
- CHANGED: Settings modal redesign - now is settings opened in tab instead of modal, similarily as in VSCode
- FIXED: Fixed search in table shortcuts #1273
- CHANGED: Improved foreign key editor UX
@@ -110,6 +162,7 @@ Builds:
- CHANGED: Improved storage of settings, especially for Team Premium edition
## 6.7.1
- ADDED: LANGUAGE environment variable for the web version. #1266
- ADDED: New localizations (Italian, Portugese (Brazil), Japanese)
- ADDED: Option to detect language from browser settings in web version
@@ -123,6 +176,7 @@ Builds:
- ADDED: Other files types supported in Team Premium edition (diagrams, query design, perspectives, import/export jobs, shell scripts, database compare jobs)
## 6.7.0
- ADDED: Added localization support, now you can use DbGate in multiple languages (French, Spanish, German, Czech, Slovak, Simplified Chinese) #347 #705 #939 #1079
- CHANGED: Solved many issues with binary fields, huge performance improvements in binary fields processing
- FIXED: Export to CSV produces empty file #1247
@@ -136,13 +190,16 @@ Builds:
- FIXED: Horizontal scrolling on macOS trackpad/Magic Mouse #1250
## 6.6.12
- FIXED: Cannot paste license key on Mac (special commands like copy/paste were disabled on license screen)
## 6.6.11
- FIXED: Fixed theming on application startup
- CHANGED: Improved licensing page
## 6.6.10
- FIXED: License from environment variable is not refreshed #1245
- FIXED: connection closing / reconnecting #1237
- ADDED: retain history across multiple queries #1236
@@ -150,19 +207,22 @@ Builds:
- FIXED: Not possible to scroll the data view horizontally by pressing shift and scroll mouse middle button on Mac #453
- FIXED: Expired trial workflow (Premium)
- ADDED: Column name collision resolving #1234 (MySQL)
## 6.6.8
- CHANGED: Windows executable now uses Azure trusted signing certificate
- CHANGED: NPM packages now use GitHub OIDC provenance signing for better security
- CHANGED: Some features moved to Premium edition (master/detail views, FK lookups, column expansion, split view, advanced export/import, data archives, grouping, macros)
## 6.6.6
- ADDED: Allow disable/re-enable filter #1174
- ADDED: Close right side tabs #1219
- ADDED: Ability disable execute current line in query editor #1209
- ADDED: Support for Redis Cluster #1204 (Premium)
## 6.6.5
- ADDED: SQL AI assistant - powered by database chat, could help you to write SQL queries (Premium)
- ADDED: Explain SQL error (powered by AI) (Premium)
- ADDED: Database chat (and SQL AI Assistant) now supports showing charts (Premium)
@@ -171,6 +231,7 @@ Builds:
- FIXED: Cannot open up large JSON file #1215
## 6.6.4
- ADDED: AI Database chat now supports much more LLM models. (Premium)
- ADDED: Possibility to use your own API key with OPENAI-compatible providers (OpenRouter, Antropic...)
- ADDED: Possibility to use self-hosted own LLM (eg. Llama)
@@ -184,11 +245,13 @@ Builds:
- CHANGED: Community edition now supports shared folders in read-only mode
## 6.6.3
- FIXED: Error “db.getCollection(…).renameCollection is not a function” when renaming collection in dbGate #1198
- FIXED: Can't list databases from Azure SQL SERVER #1197
- ADDED: Save zoom level in electron apps
## 6.6.2
- ADDED: List of processes, ability to kill process (Server summary) #1178
- ADDED: Database and table permissions (Team Premium edition)
- ADDED: Redis search box - Scan all #1191
@@ -198,6 +261,7 @@ Builds:
- FIXED: Executing queries for SQLite crash #1195
## 6.6.1
- ADDED: Support for Mongo shell (Premium) - #1114
- FIXED: Support for BLOB in Oracle #1181
- ADDED: Connect to named SQL Server instance #340
@@ -207,12 +271,14 @@ Builds:
- CHANGED: Improved logging system, added related database and connection to logs metadata
## 6.6.0
- ADDED: Database chat - AI powered chatbot, which knows your database (Premium)
- ADDED: Firestore support (Premium)
- REMOVED: Query AI assistant (replaced by Database Chat) (Premium)
- FIXED: Chart permissions were ignored (Premium)
- REMOVED: Query AI assistant (replaced by Database Chat) (Premium)
- FIXED: Chart permissions were ignored (Premium)
## 6.5.6
- ADDED: New object window - quick access to most common functions
- ADDED: Possibility to disable split query by empty line #1162
- ADDED: Possibility to opt out authentication #1152
@@ -221,6 +287,7 @@ Builds:
- FIXED: Fixed some minor problems of charts
## 6.5.5
- ADDED: Administer cloud folder window
- CHANGED: Cloud menu redesign
- ADDED: Audit log (for Team Premium edition)
@@ -230,25 +297,29 @@ Builds:
- ADDED: Added chart data type detection
- FIXED: Fixed chart displaying problems
- FIXED: Fixed exporting chart to HTML
- CHANGED: Choose COUNT measure without selecting underlying ID field (use virtual __count)
- CHANGED: Choose COUNT measure without selecting underlying ID field (use virtual \_\_count)
- FIXED: Problems with authentification administration, especially for Postgres storage
- CHANGED: Anonymous autentification (in Team Premium) is now by default disabled
- CHANGED: Anonymous autentification (in Team Premium) is now by default disabled
## 6.5.3
- CHANGED: Improved DbGate Cloud sign-in workflow
- FIXED: Some fixes and error handling in new charts engine
- ADDED: Charts - ability to choose aggregate function
- CHANGED: Improved About window
## 6.5.2
- CHANGED: Autodetecting charts is disabled by default #1145
- CHANGED: Improved chart displaying workflow
- ADDED: Ability to close chart
## 6.5.1
- FIXED: DbGate Cloud e-mail sign-in method for desktop clients
## 6.5.0
- ADDED: DbGate cloud - online storage for connections, SQL scripts and other objects
- ADDED: Public knowledge base - common SQL scripts for specific DB engines (table sizes, index stats etc.)
- ADDED: Query results could be visualised in charts (Premium)
@@ -269,7 +340,7 @@ Builds:
## 6.4.2
- ADDED: Source label to docker container #1105
- ADDED: Source label to docker container #1105
- FIXED: DbGate restart needed to take effect after trigger is created/deleted on mariadb #1112
- ADDED: View PostgreSQL query console output #1108
- FIXED: Single quote generete MySql error #1107
@@ -279,6 +350,7 @@ Builds:
- FIXED: Fixed loading Redis keys with :: in key name
## 6.4.0
- ADDED: DuckDB support
- ADDED: Data deployer (Premium)
- ADDED: Compare data between JSON lines file in archive and database table
@@ -300,6 +372,7 @@ Builds:
- CHANGED: Amazon and Azure instalations are not auto-upgraded by default
## 6.3.3
- CHANGED: New administration UI, redesigned administration of users, connections and roles
- ADDED: Encrypting passwords in team-premium edition
- ADDED: Show scale bar on map #1090
@@ -309,6 +382,7 @@ Builds:
- ADDED: Line Wrap for JSON viewer #768
### 6.3.2
- ADDED: "Use system theme" switch, use changed system theme without restart #1084
- ADDED: "Skip SETNAME instruction" option for Redis #1077
- FIXED: Clickhouse views are now available even for user with limited permissions #1076
@@ -321,6 +395,7 @@ Builds:
- FIXED: Correctly end connection process after succesful/unsuccesful connect
### 6.3.0
- ADDED: Support for libSQL and Turso (Premium)
- ADDED: Native backup and restore database for MySQL and PostgreSQL (Premium)
- REMOVED: DbGate internal dump export for MySQL (replaced with call of mysqldump)
@@ -332,6 +407,7 @@ Builds:
- FIXED: Linux Appimage crash => A JavaScript error occurred in the main process #1065 , #1067
### 6.2.1
- ADDED: Commit/rollback and autocommit in scripts #1039
- FIXED: Doesn't import all the records from MongoDB #1044
- ADDED: Show server name alongside database name in title of the tab group #1041
@@ -344,6 +420,7 @@ Builds:
- CHANGED: Upgraded SQLite engine version
### 6.2.0
- ADDED: Query AI Assistant (Premium)
- ADDED: Cassandra database support
- ADDED: XML cell data view
@@ -356,13 +433,16 @@ Builds:
- CHANGED: Open real executed query, when datagrid shows loading error
### 6.1.6
- FIXED: Hotfix build process for premium edition
### 6.1.5
- FIXED: Serious security hotfix (for Docker and NPM, when using LOGIN and PASSWORD environment variables or LOGIN_PASSWORD_xxx)
- no changes for desktop app and for Team premium edition, when using storage DB
### 6.1.4
- CHANGED: Show Data/Structure button in one place #1015
- ADDED: Data view coloring (every second row) #1014
- ADDED: Pin icon for tab in preview mode (#1013)
@@ -377,11 +457,12 @@ Builds:
- ADDED: Redis JSON format for String values #852
### 6.1.3
- FIXED: Fulltext search now shows correctly columns and SQL code lines
- ADDED: Configuration of SSH tunnel local host (IPv4 vs IPv6). Should fix majority of SSH tunnel problems
- FIXED: Handled SSH tunnel connection error, now it shows error instead of connecting forever
- ADDED: Support of triggers (SQLite)
- ADDED: Create, drop trigger
- ADDED: Create, drop trigger
- ADDED: Support for MySQL scheduled events
- FIXED: Cannot connect to DB using askUser/askPassword mode #995
- FIXED: Filtering in Oracle #992
@@ -389,6 +470,7 @@ Builds:
- ADDED: Introduced E2E Cypress tests, test refactor
### 6.1.1
- ADDED: Trigger support (SQL Server, PostgreSQL, MySQL, Oracle)
- FIXED: PostgreSQL and Oracle export #970
- FIXED: Cursor Becomes Stuck When Escaping "Case" #954
@@ -396,6 +478,7 @@ Builds:
- FIXED: Search in packed list
### 6.1.0
- ADDED: Fulltext search in DB model and connections, highlight searched names
- ADDED: Tab preview mode configuration #963
- CHANGED: Single-click to open server connection/database + ability to configure this #959
@@ -412,6 +495,7 @@ Builds:
- ADDED: Display comment into tables and column list #755
### 6.0.0
- ADDED: Order or filter the indexes for huge tables #922
- ADDED: Empty string filters
- CHANGED: (Premium) Workflow for new installation (used in Docker and AWS distribution)
@@ -444,6 +528,7 @@ Builds:
- ADDED: Show SQL quick view
### 5.5.6
- FIXED: DbGate process consumes 100% after UI closed - Mac, Linux (#917, #915)
- FIXED: Correctly closing connection behind SSH tunnel (#920)
- FIXED: Updating MongoDB documents on MongoDB 4 (#916)
@@ -451,6 +536,7 @@ Builds:
- FIXED: (Premium) Better handling of connection storage errors
### 5.5.5
- ADDED: AWS IAM authentication for MySQL, MariaDB, PostgreSQL (Premium)
- FIXED: Datitme filtering #912
- FIXED: Load redis keys
@@ -461,6 +547,7 @@ Builds:
- FIXED: Save connection params in administration for MS SQL and Postgres storages (Team Premium)
### 5.5.4
- FIXED: correct handling when use LOGIN and PASSWORD env variables #903
- FIXED: fixed problems in dbmodel commandline tool
- ADDED: dbmodel - allow connection defined in environment variables
@@ -472,6 +559,7 @@ Builds:
- ADDED: (Premium) Show purchase button after trial license is expired
### 5.5.3
- FIXED: Separate schema mode #894 - for databases with many schemas
- FIXED: Sort by UUID column in POstgreSQL #895
- ADDED: Load pg_dump outputs #893
@@ -481,9 +569,11 @@ Builds:
- FIXED: MS Entra authentication for Azure SQL
### 5.5.2
- FIXED: MySQL, PostgreSQL readonly conections #900
### 5.5.1
- ADDED: Clickhouse support (#532)
- ADDED: MySQL - specify table engine, show table engine in table list
- FIXED: Hidden primary key name in PK editor for DB engines with anonymous PK (MySQL)
@@ -511,6 +601,7 @@ Builds:
- ADDED: (Premium) MS Entra authentization for Azure SQL databases
### 5.4.4
- CHANGED: Improved autoupdate, notification is now in app
- CHANGED: Default behaviour of autoupdate, new version is downloaded after click of "Download" button
- ADDED: Ability to configure autoupdate (check only, check+download, don't check)
@@ -519,14 +610,17 @@ Builds:
- FIXED: Fixes following issues: #886, #865, #782, #375
### 5.4.2
- FIXED: DbGate now works correctly with Oracle 10g
- FIXED: Fixed update channel for premium edition
### 5.4.1
- FIXED: Broken older plugins #881
- ADDED: Premium edition - "Start trial" button
### 5.4.0
- ADDED: Support for CosmosDB (Premium only)
- ADDED: Administration UI (Premium only)
- ADDED: New application icon
@@ -543,10 +637,12 @@ Builds:
- FIXED: Script with escaped backslash causes erro #880
### 5.3.4
- FIXED: On blank system does not start (window does not appear) #862
- FIXED: Missing Execute, Export bar #861
### 5.3.3
- FIXED: The application Window is not visible when openning after changing monitor configuration. #856
- FIXED: Multi column filter is broken for Postgresql #855
- ADDED: Do not display internal timescaledb objects in postgres databases #839
@@ -554,12 +650,14 @@ Builds:
- FIXED: Cannot filter by uuid field in psql #538
### 5.3.1
- FIXED: Column sorting on query tab not working #819
- FIXED: Postgres Connection stays in "Loading database structure" until reloading the page #826
- FIXED: Cannot read properties of undefined (reading 'length') on Tables #824
- FIXED: Redshift doesn't show tables when connected #816
### 5.3.0
- CHANGED: New Oracle driver, much better Oracle support. Works now also in docker distribution
- FIXED: Connection to oracle with service name #809
- ADDED: Connect to redis using a custom username #807
@@ -568,18 +666,20 @@ Builds:
- ADDED: Switch connection for opened file #814
### 5.2.9
- FIXED: PostgresSQL doesn't show tables when connected #793 #805
- FIXED: MongoDB write operations fail #798 #802
- FIXED: Elecrron app logging losed most of log messages
- FIXED: Connection error with SSH tunnel
- FIXED: Connection error with SSH tunnel
- ADDED: option to disable autoupgrades (with --disable-auto-upgrade)
- ADDED: Send error context to github gist
### 5.2.8
- FIXED: file menu save and save as not working
- FIXED: query editor on import/export screen overlaps with selector
- FIXED: Fixed inconsistencies in max/unmaximize window buttons
- FIXED: shortcut for select all
- FIXED: shortcut for select all
- FIXED: download with auth header
- CHANGED: Upgraded database drivers for mysql, postgres, sqlite, mssql, mongo, redis
- CHANGED: Upgraded electron version (now using v30)
@@ -596,8 +696,8 @@ Builds:
- ADDED: Button for discard/reset changes (#759)
- FIXED: Don't show error dialog when subprocess fails, as DbGate handles this correctly (#751, #746, #542, #272)
### 5.2.7
- FIXED: fix body overflow when context menu height great than viewport #592
- FIXED: Pass signals in entrypoint.sh #596
- FIXED: Remove missing links to jenasoft #625
@@ -608,6 +708,7 @@ Builds:
- CHANGED: Improved stability of electron client on Windows and Mac (fewer EPIPE errors)
### 5.2.6
- FIXED: DbGate creates a lot of .tmp.node files in the temp directory #561
- FIXED: Typo in datetimeoffset dataType #556
- FIXED: SQL export is using the wrong hour formatting #537
@@ -615,6 +716,7 @@ Builds:
- FIXED: MongoDB password could contain special characters #560
### 5.2.5
- ADDED: Split Windows #394
- FIXED: Postgres index asc/desc #514
- FIXED: Excel export not working since 5.2.3 #511
@@ -623,9 +725,11 @@ Builds:
- FIXED: Solved some minor problems with widget collapsing
### 5.2.4
- FIXED: npm version crash (#508)
### 5.2.3
- ADDED: Search entire table (multi column filter) #491
- ADDED: OracleDB - connection to toher than default ports #496
- CHANGED: OracleDB - status of support set to experimental
@@ -657,8 +761,8 @@ Builds:
- FIXED: Fixed some scenarios using tables from different DBs
- FIXED: Sessions with long-running queries are not killed
### 5.2.2
- FIXED: Optimalized load DB structure for PostgreSQL #451
- ADDED: Auto-closing query connections after configurable (15 minutes default) no-activity interval #468
- ADDED: Set application-name connection parameter (for PostgreSQL and MS SQL) for easier identifying of DbGate connections
@@ -669,8 +773,8 @@ Builds:
- FIXED: crash on Windows and Mac after system goes in suspend mode #458
- ADDED: dbmodel standalone NPM package (https://www.npmjs.com/package/dbmodel) - deploy database via commandline tool
### 5.2.1
- FIXED: client_id param in OAuth
- ADDED: OAuth scope parameter
- FIXED: login page - password was not sent, when submitting by pressing ENTER
@@ -678,6 +782,7 @@ Builds:
- FIXED: Export modal - fixed crash when selecting different database
### 5.2.0
- ADDED: Oracle database support #380
- ADDED: OAuth authentification #407
- ADDED: Active directory (Windows) authentification #261
@@ -699,7 +804,7 @@ Builds:
- ADDED: Perspective designer supports joins from MongoDB nested documents and arrays
- FIXED: Perspective designer joins on MongoDB ObjectId fields
- ADDED: Filtering columns in designer (query designer, diagram designer, perspective designer)
- FIXED: Clone MongoDB rows without _id attribute #404
- FIXED: Clone MongoDB rows without \_id attribute #404
- CHANGED: Improved cell view with GPS latitude, longitude fields
- ADDED: SQL: ALTER VIEW and SQL:ALTER PROCEDURE scripts
- ADDED: Ctrl+F5 refreshes data grid also with database structure #428
@@ -708,8 +813,8 @@ Builds:
- ADDED: Rename, remove connection folder, memoize opened state after app restart #425
- FIXED: Show SQLServer alter store procedure #435
### 5.1.6
- ADDED: Connection folders support #274
- ADDED: Keyboard shortcut to hide result window and show/hide the side toolbar #406
- ADDED: Ability to show/hide query results #406
@@ -721,6 +826,7 @@ Builds:
- CHANGED: More strict timeouts to kill database and server connections (reduces resource consumption)
### 5.1.5
- ADDED: Support perspectives for MongoDB - MongoDB query designer
- ADDED: Show JSON content directly in the overview #395
- CHANGED: OSX Command H shortcut for hiding window #390
@@ -731,6 +837,7 @@ Builds:
- ADDED: connect via socket - configurable via environment variables #358
### 5.1.4
- ADDED: Drop database commands #384
- ADDED: Customizable Redis key separator #379
- ADDED: ARM support for docker images
@@ -739,6 +846,7 @@ Builds:
- ADDED: Unsaved marker for SQL files
### 5.1.3
- ADDED: Editing multiline cell values #378 #371 #359
- ADDED: Truncate table #333
- ADDED: Perspectives - show row count
@@ -747,6 +855,7 @@ Builds:
- FIXED: Correct error line numbers returned from queries
### 5.1.2
- FIXED: MongoDb any export function does not work. #373
- ADDED: Query Designer short order more flexibility #372
- ADDED: Form View move between records #370
@@ -760,6 +869,7 @@ Builds:
- ADDED: Perspectives - cells without joined data are gray
### 5.1.1
- ADDED: Perspective designer
- FIXED: NULL,NOT NULL filter datatime columns #356
- FIXED: Recognize computed columns on SQL server #354
@@ -769,32 +879,35 @@ Builds:
- ADDED: Custom editor font size #345
- ADDED: Ability to open perspective files
### 5.1.0
- ADDED: Perspectives (docs: https://dbgate.org/docs/perspectives.html )
- CHANGED: Upgraded SQLite engine version (driver better-sqlite3: 7.6.2)
- CHANGED: Upgraded ElectronJS version (from version 13 to version 17)
- CHANGED: Upgraded all dependencies with current available minor version updates
- CHANGED: By default, connect on click #332˝
- CHANGED: Improved keyboard navigation, when editing table data #331
- ADDED: Option to skip Save changes dialog #329
- ADDED: Option to skip Save changes dialog #329
- FIXED: Unsigned column doesn't work correctly. #324
- FIXED: Connect to MS SQL with domain user now works also under Linux and Mac #305
### 5.0.9
- FIXED: Fixed problem with SSE events on web version
- ADDED: Added menu command "New query designer"
- ADDED: Added menu command "New ER diagram"
### 5.0.8
- ADDED: SQL Server - support using domain logins under Linux and Mac #305
- ADDED: Permissions for connections #318
- ADDED: Ability to change editor front #308
- ADDED: Custom expression in query designer #306
- ADDED: OR conditions in query designer #321
- ADDED: Ability to configure settings view environment variables #304
### 5.0.7
- FIXED: Fixed some problems with SSH tunnel (upgraded SSH client) #315
- FIXED: Fixed MognoDB executing find query #312
- ADDED: Interval filters for date/time columns #311
@@ -802,8 +915,9 @@ Builds:
- ADDED: connecting option Trust server certificate for SQL Server #305
- ADDED: Autorefresh, reload table every x second #303
- FIXED(app): Changing editor theme and font size in Editor Themes #300
### 5.0.6
- ADDED: Search in columns
- CHANGED: Upgraded mongodb driver
- ADDED: Ability to reset view, when data load fails
@@ -811,6 +925,7 @@ Builds:
- FIXED: Fixed some NPM package problems
### 5.0.5
- ADDED: Visualisation geographics objects on map #288
- ADDED: Support for native SQL as default value inside yaml files #296
- FIXED: Postgres boolean columns don't filter correctly #298
@@ -818,10 +933,11 @@ Builds:
- FIXED: Handle error when reading deleted archive
### 5.0.3
- CHANGED: Optimalization of loading DB structure for PostgreSQL, MySQL #273
- CHANGED: Upgraded mysql driver #293
- CHANGED: Better UX when defining SSH port #291
- ADDED: Database object menu from tab
- ADDED: Database object menu from tab
- CHANGED: Ability to close file uploader
- FIXED: Correct handling of NUL values in update keys
- CHANGED: Upgraded MS SQL tedious driver
@@ -831,13 +947,17 @@ Builds:
- ADDED: Configurable object actions #255
- ADDED: Multiple sort criteria #235
- ADDED(app): Open JSON file
### 5.0.2
- FIXED: Cannot use SSH Tunnel after update #291
### 5.0.1
- FIXED(app): Can't Click Sidebar Menu Item #287
### 5.0.0
- CHANGED: Connection workflow, connections are opened on tabs instead of modals
- ADDED: Posibility to connect to DB without saving connection
- ADDED(mac): Support for SQLite on Mac M1
@@ -850,6 +970,7 @@ Builds:
- FIXED: Removed SSL tab on Redis connection (SSL is not supported for Redis)
### 4.8.8
- CHANGED: New app icon
- ADDED: SQL dump, SQL import - also from/to saved queries
- FIXED(mac): Fixed crash when reopening main window
@@ -858,6 +979,7 @@ Builds:
- ADDED(app): Browse tabs in reverse order with Ctrl+Shift+Tab #245
### 4.8.7
- ADDED: MySQL dump/backup database
- ADDED: Import SQL dump from file or from URL
- FIXED(mac): Fixed Cmd+C, Cmd+V, Cmd+X - shortcuts for copy/cut/paste #270
@@ -866,6 +988,7 @@ Builds:
- ADDED: Support for dockerhost network name under docker #271
### 4.8.4
- FIXED(mac): Fixed build for macOS arm64 #259
- FIXED(mac): Fixed opening SQLite files on macOS #243
- FIXED(mac): Fixed opening PEM certificates on macOS #206
@@ -877,6 +1000,7 @@ Builds:
- ADDED: Added menu command "Tools/Change to recent database"
### 4.8.3
- FIXED: filters in query result and NDJSON/archive viewer
- ADDED: Added select values from query result and NDJSON/archive viewer
- ADDED: tab navigation in datagrid #254
@@ -886,19 +1010,24 @@ Builds:
- ADDED: Data type + reference link in column manager
- FIXED(win,linux,mac): Unable to change theme after installing plugin #244
### 4.8.2
- ADDED: implemented missing redis search key logic
### 4.8.2
### 4.8.1
- FIXED: fixed crash after disconnecting from all DBs
- ADDED: implemented missing redis search key logic
### 4.8.1
- FIXED: fixed crash after disconnecting from all DBs
### 4.8.0
- ADDED: Redis support (support stream type), removed experimental status
- ADDED: Redis readonly support
- ADDED: Explicit NDJSON support, when opening NDJSON/JSON lines file, table data are immediately shown, without neccesarity to import
- ADDED(win,linux,mac): Opening developer tools when crashing without reload app
### 4.7.4
- ADDED: Experimental Redis support (full support is planned to version 4.8.0)
- ADDED: Experimental Redis support (full support is planned to version 4.8.0)
- ADDED: Read-only connections
- FIXED: MongoDB filters
- ADDED: MongoDB column value selection
@@ -906,13 +1035,14 @@ Builds:
- ADDED: Fuzzy search #246
- ADDED(docker, npm): New permissions
- FIXED(npm): NPM build no longer allocates additonal ports
- CHANGED(npm): renamed NPM package dbgate => dbgate-serve
- CHANGED(npm): renamed NPM package dbgate => dbgate-serve
- CHANGED(docker): custom JavaScripts and connections defined in scripts are now prohibited by default, use SHELL_CONNECTION and SHELL_SCRIPTING environment variables for allowing this
- ADDED(docker, npm): Better documentation of environment variables configuration, https://dbgate.org/docs/env-variables.html
- ADDED(docker): support for multiple users with different permissions
- ADDED(docker): logout operation
### 4.7.3
- CHANGED: Export menu redesign, quick export menu merged with old export menu
- REMOVED: Quick export menu
- ADDED: Export column mapping
@@ -927,6 +1057,7 @@ Builds:
- ADDED: NPM dist accepts .env configuration
### 4.7.2
- CHANGED: documentation URL - https://dbgate.org/docs/
- CHANGED: Close button available for all tab groups - #238
- ADDED: Search function for the Keyboard Shortcuts overview - #239
@@ -935,7 +1066,8 @@ Builds:
- FIXED: bug in cache subsystem
### 4.7.1
- FIXED: Fixed connecting to MS SQL server running in docker container from DbGate running in docker container #236
- FIXED: Fixed connecting to MS SQL server running in docker container from DbGate running in docker container #236
- FIXED: Fixed export MongoDB collections into Excel and CSV #240
- ADDED: Added support for docker volumes to persiste connections, when not using configuration via env variables #232
- ADDED: DbGate in Docker can run in subdirectory #228
@@ -945,7 +1077,9 @@ Builds:
- ADDED: Improved fullscreen state, title bar with menu is hidden, menu is in hamburger menu, like in web version
- ADDED: Theme choose dialog (added as tab in settings)
- FIXED: Fixed crash when clicking on application layers #231
### 4.7.0
- CHANGED: Changed main menu style, menu and title bar is in one line (+ability to switch to system menu)
- REMOVED: Removed main toolbar, use main menu or tab related bottom tool instead
- ADDED: Added tab related context bottom toolbar
@@ -964,11 +1098,13 @@ Builds:
- ADDED: Better work with JSON lines file, added JSONL editor with preview
### 4.6.3
- FIXED: Fixed Windows build
- FIXED: Fixed crash, when there is invalid value in browser local storage
- FIXED: Fixed plugin description display, where author name or description is not correctly filled
### 4.6.2
- FIXED: Fixed issues of XML import plugin
- ADDED: Split columns macro (available in data sheet editor)
- CHANGED: Accepting non standard plugins names (which doesn't start with dbgate-plugin-)
@@ -980,6 +1116,7 @@ Builds:
- FIXED: Fixed configuring connection to SQLite with environment variables #215
### 4.6.1
- ADDED: Ability to configure SSH tunnel over environment variables #210 (for docker container)
- ADDED: XML export and import
- ADDED: Archive file - show and edit source text file
@@ -995,20 +1132,23 @@ Builds:
- CHANGED: UX improvements of table editor
### 4.6.0
- ADDED: ER diagrams #118
- Generate diagram from table or for database
- Automatic layout
- Diagram styles - colors, select columns to display, optional displaying data type or nullability
- Export diagram to HTML file
- Generate diagram from table or for database
- Automatic layout
- Diagram styles - colors, select columns to display, optional displaying data type or nullability
- Export diagram to HTML file
- FIXED: Mac latest build link #204
### 4.5.1
- FIXED: MongoId detection
- FIXED: #203 disabled spellchecker
- FIXED: Prevented display filters in form view twice
- FIXED: Query designer fixes
### 4.5.0
- ADDED: #220 functions, materialized views and stored procedures in code completion
- ADDED: Query result in statusbar
- ADDED: Highlight and execute current query
@@ -1026,6 +1166,7 @@ Builds:
- FIXED: Fixed delete dependency cycle detection (delete didn't work for some tables)
### 4.4.4
- FIXED: Database colors
- CHANGED: Precise work with MongoDB ObjectId
- FIXED: Run macro works on MongoDB collection data editor
@@ -1040,6 +1181,7 @@ Builds:
- ADDED: Show change log after app upgrade
### 4.4.3
- ADDED: Connection and database colors
- ADDED: Ability to pin connection or table
- ADDED: MongoDb: create, drop collection from menu
@@ -1057,6 +1199,7 @@ Builds:
- CHANGED: Save widget visibility and size
### 4.4.2
- ADDED: Open SQL script from SQL confirm
- CHANGED: Better looking statusbar
- ADDED: Create table from database popup menu
@@ -1066,6 +1209,7 @@ Builds:
- ADDED: Support for Command key on Mac (#199)
### 4.4.1
- FIXED: #188 Fixed problem with datetime values in PostgreSQL and mysql
- ADDED: #194 Close tabs by DB
- FIXED: Improved form view width calculations
@@ -1079,6 +1223,7 @@ Builds:
- ADDED: Row count information moved into status bar, when only one grid on tab is used (typical case)
### 4.4.0
- ADDED: Database structure compare, export report to HTML
- ADDED: Experimental: Deploy DB structure changes between databases
- ADDED: Lookup dialog, available in table view on columns with foreign key
@@ -1095,21 +1240,25 @@ Builds:
- FIXED: Fixed import into SQLite and PostgreSQL databases, added integration test for this
### 4.3.4
- FIXED: Delete row with binary ID in MySQL (#182)
- ADDED: Using 'ODBC Driver 17 for SQL Server' or 'SQL Server Native Client 11.0', when connecting to MS SQL using windows auth #183
### 4.3.3
- ADDED: Generate SQL from data (#176 - Copy row as INSERT/UPDATE statement)
- ADDED: Datagrid keyboard column operations (Ctrl+F - find column, Ctrl+H - hide column) #180
- FIXED: Make window remember that it was maximized
- FIXED: Fixed lost focus after copy to clipboard and after inserting SQL join
### 4.3.2
- FIXED: Sorted database list in PostgreSQL (#178)
- FIXED: Loading stricture of PostgreSQL database, when it contains indexes on expressions (#175)
- ADDED: Hotkey Shift+Alt+F for formatting SQL code
### 4.3.1
- FIXED: #173 Using key phrase for SSH key file connection
- ADDED: #172 Abiloity to quick search within database names
- ADDED: Database search added to command palette (Ctrl+P)
@@ -1117,24 +1266,28 @@ Builds:
- ADDED: DELETE cascade option - ability to delete all referenced rows, when deleting rows
### 4.3.0
- ADDED: Table structure editor
- ADDED: Index support
- ADDED: Unique constraint support
- ADDED: Context menu for drop/rename table/columns and for drop view/procedure/function
- ADDED: Added support for Windows arm64 platform
- FIXED: Search by _id in MongoDB
- FIXED: Search by \_id in MongoDB
### 4.2.6
- FIXED: Fixed MongoDB import
- ADDED: Configurable thousands separator #136
- ADDED: Using case insensitive text search in postgres
### 4.2.5
- FIXED: Fixed crash when using large model on some installations
- FIXED: Postgre SQL CREATE function
- FIXED: Postgre SQL CREATE function
- FIXED: Analysing of MySQL when modifyDate is not known
### 4.2.4
- ADDED: Query history
- ADDED: One-click exports in desktop app
- ADDED: JSON array export
@@ -1146,23 +1299,27 @@ Builds:
- CHANGED: Introduced package dbgate-query-splitter, instead of sql-query-identifier and @verycrazydog/mysql-parse
### 4.2.3
- ADDED: ARM builds for MacOS and Linux
- ADDED: Filter by columns in form view
### 4.2.2
- CHANGED: Further startup optimalization (approx. 2 times quicker start of electron app)
### 4.2.1
- FIXED: Fixed+optimalized app startup (esp. on Windows)
### 4.2.0
- ADDED: Support of SQLite database
- ADDED: Support of Amazon Redshift database
- ADDED: Support of CockcroachDB
- CHANGED: DB Model is not auto-refreshed by default, refresh could be invoked from statusbar
- FIXED: Fixed race conditions on startup
- FIXED: Fixed broken style in data grid under strange circumstances
- ADDED: Configure connections with commandline arguments #108
- ADDED: Configure connections with commandline arguments #108
- CHANGED: Optimalized algorithm of incremental DB model updates
- CHANGED: Loading queries from PostgreSQL doesn't need cursors, using streamed query instead
- ADDED: Disconnect command
@@ -1171,9 +1328,11 @@ Builds:
- ADDED: Cosmetic improvements of MariaDB support
### 4.1.11
- FIX: Fixed crash of API process when using SSH tunnel connection (race condition)
### 4.1.11
- FIX: fixed processing postgre query containing $$
- FIX: fixed postgre analysing procedures & functions
- FIX: patched svelte crash #105
@@ -1186,6 +1345,7 @@ Builds:
- CHANGED: Toolbar design - current tab related commands are delimited
### 4.1.10
- ADDED: Default database option in connectin settings #96 #92
- FIX: Bundle size optimalization for Windows #97
- FIX: Popup menu placement on smaller displays #94
@@ -1196,22 +1356,32 @@ Builds:
- ADDED: Show database server version in status bar
- ADDED: Show detailed info about error, when connect to database fails
- ADDED: Portable ZIP distribution for Windows #84
### 4.1.9
- FIX: Incorrect row count info in query result #83
### 4.1.1
- CHANGED: Default plugins are now part of installation
### 4.1.0
- ADDED: MongoDB support
- ADDED: Configurable keyboard shortcuts
- ADDED: JSON row cell data view
- FIX: Fixed some problems from migration to Svelte
### 4.0.3
- FIX: fixes for FireFox (mainly incorrent handle of bind:clientHeight, replaces with resizeobserver)
### 4.0.2
- FIX: fixed docker and NPM build
### 4.0.0
- CHANGED: Excahnged React with Svelte. Changed theme colors. Huge speed and memory optimalization
- ADDED: SQL Generator (CREATE, INSERT, DROP)
- ADDED: Command palette (F1). Introduced commands, extended some context menus
@@ -1223,6 +1393,7 @@ Builds:
- FIX: Solved reconnecting expired connection
### 3.9.6
- ADDED: Connect using SSH Tunnel
- ADDED: Connect using SSL
- ADDED: Database connection dialog redesigned
@@ -1232,4 +1403,5 @@ Builds:
- FIX: #62 - import, export executed from SNAP installs didn't work
### 3.9.5
- Start point of changelog

119
CLAUDE.md Normal file
View File

@@ -0,0 +1,119 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
DbGate is a cross-platform (no)SQL database manager supporting MySQL, PostgreSQL, SQL Server, Oracle, MongoDB, Redis, SQLite, and more. It runs as a web app (Docker/NPM), an Electron desktop app, or in a browser. The monorepo uses Yarn workspaces.
## Development Commands
```sh
yarn # install all packages (also builds TS libraries and plugins)
yarn start # run API (port 3000) + web (port 5001) concurrently
```
For more control, run these 3 commands in separate terminals:
```sh
yarn start:api # Express API on port 3000
yarn start:web # Svelte frontend on port 5001
yarn lib # watch-compile TS libraries and plugins
```
For Electron development:
```sh
yarn start:web # web on port 5001
yarn lib # watch TS libs/plugins
yarn start:app # Electron app
```
### Building
```sh
yarn build:lib # build all TS libraries (sqltree, tools, filterparser, datalib, rest)
yarn build:api # build API
yarn build:web # build web frontend
yarn ts # TypeScript type-check API and web
yarn prettier # format all source files
```
### Testing
Unit tests (in packages like `dbgate-tools`):
```sh
yarn workspace dbgate-tools test
```
Integration tests (requires Docker for database containers):
```sh
cd integration-tests
yarn test:local # run all tests
yarn test:local:path __tests__/alter-database.spec.js # run a single test file
```
E2E tests (Cypress):
```sh
yarn cy:open # open Cypress UI
cd e2e-tests && yarn cy:run:browse-data # run a specific spec headlessly
```
## Architecture
### Monorepo Structure
| Path | Package | Purpose |
|---|---|---|
| `packages/api` | `dbgate-api` | Express.js backend server |
| `packages/web` | `dbgate-web` | Svelte 4 frontend (built with Rolldown) |
| `packages/tools` | `dbgate-tools` | Shared TS utilities: SQL dumping, schema analysis, diffing, driver base classes |
| `packages/datalib` | `dbgate-datalib` | Grid display logic, changeset management, perspectives, chart definitions |
| `packages/sqltree` | `dbgate-sqltree` | SQL AST representation and dumping |
| `packages/filterparser` | `dbgate-filterparser` | Parses filter strings into SQL/Mongo conditions |
| `packages/rest` | `dbgate-rest` | REST connection support |
| `packages/types` | `dbgate-types` | TypeScript type definitions (`.d.ts` only) |
| `packages/aigwmock` | `dbgate-aigwmock` | Mock AI gateway server for E2E testing |
| `plugins/dbgate-plugin-*` | — | Database drivers and file format handlers |
| `app/` | — | Electron shell |
| `integration-tests/` | — | Jest-based DB integration tests (Docker) |
| `e2e-tests/` | — | Cypress E2E tests |
### API Backend (`packages/api`)
- Express.js server with controllers in `src/controllers/` — each file exposes REST endpoints via the `useController` utility
- Database connections run in child processes (`src/proc/`) to isolate crashes and long-running operations
- `src/shell/` contains stream-based data pipeline primitives (readers, writers, transforms) used for import/export and replication
- Plugin drivers are loaded dynamically via `requireEngineDriver`; each plugin in `plugins/` exports a driver conforming to `DriverBase` from `dbgate-tools`
### Frontend (`packages/web`)
- Svelte 4 components; builds with Rolldown (not Vite/Webpack)
- Global state in `src/stores.ts` using Svelte writable stores, with `writableWithStorage` / `writableWithForage` helpers for persistence
- API calls go through `src/utility/api.ts` (`apiCall`, `apiOff`, etc.) which handles auth, error display, and cache invalidation
- Tab system: each open editor/viewer is a "tab" tracked in `openedTabs` store; tab components live in `src/tabs/`
- Left-panel tree items are "AppObjects" in `src/appobj/`
- Metadata (table lists, column info) is loaded reactively via hooks in `src/utility/metadataLoaders.ts`
- Commands/keybindings are registered in `src/commands/`
### Plugin Architecture
Each `plugins/dbgate-plugin-*` package provides:
- **Frontend build** (`build:frontend`): bundled JS loaded by the web UI for query formatting, data rendering
- **Backend build** (`build:backend`): Node.js driver code loaded by the API for actual DB connections
Plugins are copied to `plugins/dist/` via `plugins:copydist` before building the app or Docker image.
### Key Conventions
- Error/message codes use `DBGM-00000` as placeholder — do not introduce new numbered `DBGM-NNNNN` codes
- Frontend uses **Svelte 4** (not Svelte 5)
- E2E test selectors use `data-testid` attribute with format `ComponentName_identifier`
- Prettier config: single quotes, 2-space indent, 120-char line width, trailing commas ES5
- Logging via `pinomin`; pipe through `pino-pretty` for human-readable output
### Translation System
```sh
yarn translations:extract # extract new strings
yarn translations:add-missing # add missing translations
yarn translations:check # check for issues
```

View File

@@ -4,5 +4,6 @@ module.exports = {
mssql: true,
oracle: true,
sqlite: true,
mongo: true
mongo: true,
dynamo: true,
};

View File

@@ -4,24 +4,56 @@ const fs = require('fs');
const baseDir = path.join(os.homedir(), '.dbgate');
const testApiPidFile = path.join(__dirname, 'tmpdata', 'test-api.pid');
const aigwmockPidFile = path.join(__dirname, 'tmpdata', 'aigwmock.pid');
function clearTestingData() {
if (fs.existsSync(testApiPidFile)) {
function readProcessStartTime(pid) {
if (process.platform === 'linux') {
try {
const pid = Number(fs.readFileSync(testApiPidFile, 'utf-8'));
if (Number.isInteger(pid) && pid > 0) {
process.kill(pid);
}
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
return stat.split(' ')[21] || null;
} catch (err) {
// ignore stale PID files and dead processes
}
try {
fs.unlinkSync(testApiPidFile);
} catch (err) {
// ignore cleanup errors
return null;
}
}
return null;
}
function isPidStillOurs(meta) {
if (!meta || !(meta.pid > 0)) return false;
if (process.platform === 'linux' && meta.startTime) {
const current = readProcessStartTime(meta.pid);
return current === meta.startTime;
}
return true;
}
function stopProcessByPidFile(pidFile) {
if (!fs.existsSync(pidFile)) return;
try {
const content = fs.readFileSync(pidFile, 'utf-8').trim();
let meta;
try {
meta = JSON.parse(content);
} catch (_) {
const pid = Number(content);
meta = Number.isInteger(pid) && pid > 0 ? { pid } : null;
}
if (isPidStillOurs(meta)) {
process.kill(meta.pid);
}
} catch (err) {
// ignore stale PID files and dead processes
}
try {
fs.unlinkSync(pidFile);
} catch (err) {
// ignore cleanup errors
}
}
function clearTestingData() {
stopProcessByPidFile(testApiPidFile);
stopProcessByPidFile(aigwmockPidFile);
if (fs.existsSync(path.join(baseDir, 'connections-e2etests.jsonl'))) {
fs.unlinkSync(path.join(baseDir, 'connections-e2etests.jsonl'));

View File

@@ -55,6 +55,9 @@ module.exports = defineConfig({
case 'redis':
serverProcess = exec('yarn start:redis');
break;
case 'ai-chat':
serverProcess = exec('yarn start:ai-chat');
break;
}
await waitOn({ resources: ['http://localhost:3000'] });

View File

@@ -0,0 +1,105 @@
Cypress.on('uncaught:exception', err => {
if (err.message.includes("Failed to execute 'importScripts' on 'WorkerGlobalScope'")) {
return false;
}
});
beforeEach(() => {
cy.visit('http://localhost:3000');
cy.viewport(1250, 900);
});
describe('Database Chat (MySQL)', () => {
it('Database chat - chart of popular genres', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_databaseChat').click();
cy.wait(1000);
cy.get('body').realType('show me chart of most popular genres');
cy.get('body').realPress('Enter');
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.testid('chart-canvas', { timeout: 30000 }).should($c =>
expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/)
);
cy.themeshot('database-chat-chart');
});
it('Database chat - find most popular artist', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_databaseChat').click();
cy.wait(1000);
cy.get('body').realType('find most popular artist');
cy.get('body').realPress('Enter');
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.contains('Iron Maiden', { timeout: 30000 });
cy.themeshot('database-chat-popular-artist');
});
});
describe('GraphQL Chat', () => {
it('GraphQL chat - list users', () => {
cy.contains('REST GraphQL').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_graphqlChat').click();
cy.wait(1000);
cy.get('body').realType('list all users');
cy.get('body').realPress('Enter');
cy.testid('GraphQlChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.contains('users', { timeout: 30000 });
cy.themeshot('graphql-chat-list-users');
});
it('GraphQL chat - product categories chart', () => {
cy.contains('REST GraphQL').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_graphqlChat').click();
cy.wait(1000);
cy.get('body').realType('show me a chart of product categories');
cy.get('body').realPress('Enter');
cy.testid('GraphQlChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.testid('chart-canvas', { timeout: 30000 }).should($c =>
expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/)
);
cy.themeshot('graphql-chat-categories-chart');
});
it('GraphQL chat - find most expensive product', () => {
cy.contains('REST GraphQL').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_graphqlChat').click();
cy.wait(1000);
cy.get('body').realType('find the most expensive product');
cy.get('body').realPress('Enter');
cy.testid('GraphQlChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.contains('products', { timeout: 30000 });
cy.themeshot('graphql-chat-expensive-product');
});
it('GraphQL chat - show all categories', () => {
cy.contains('REST GraphQL').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_graphqlChat').click();
cy.wait(1000);
cy.get('body').realType('show all categories');
cy.get('body').realPress('Enter');
cy.testid('GraphQlChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.contains('categories', { timeout: 30000 });
cy.themeshot('graphql-chat-all-categories');
});
it('Explain query error', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_query').click();
cy.wait(1000);
cy.get('body').realType('select * from Invoice2');
cy.contains('Execute').click();
cy.testid('MessageViewRow-explainErrorButton-1').click();
cy.testid('ChatCodeRenderer_useSqlButton', { timeout: 30000 });
cy.themeshot('explain-query-error');
});
});

View File

@@ -512,4 +512,43 @@ describe('Data browser data', () => {
cy.testid('DataFilterControl_input_ArtistId.Name').type('mich{enter}');
cy.themeshot('data-browser-filter-by-expanded');
});
it('DynamoDB', () => {
cy.contains('Dynamo-connection').click();
cy.contains('us-east-1').click();
cy.contains('Album').click();
cy.contains('Pearl Jam').click();
cy.themeshot('dynamodb-table-data');
cy.contains('Switch to JSON').click();
cy.themeshot('dynamodb-json-view');
cy.contains('Customer').click();
cy.testid('DataFilterControl_input_CustomerId').type('<=10{enter}');
cy.contains('Rows: 10');
cy.wait(1000);
cy.contains('Helena').click().rightclick();
cy.contains('Show cell data').click();
cy.contains('City: "Prague"');
cy.themeshot('dynamodb-query-json-view');
cy.contains('Switch to JSON').click();
cy.contains('Leonie').rightclick();
cy.contains('Edit document').click();
Array.from({ length: 11 }).forEach(() => cy.realPress('ArrowDown'));
Array.from({ length: 14 }).forEach(() => cy.realPress('ArrowRight'));
Array.from({ length: 7 }).forEach(() => cy.realPress('Delete'));
cy.realType('Italy');
cy.testid('EditJsonModal_saveButton').click();
cy.contains('Helena').rightclick();
cy.contains('Delete document').click();
cy.contains('Save').click();
cy.themeshot('dynamodb-save-changes');
cy.testid('SqlObjectList_addButton').click();
cy.contains('New collection/container').click();
cy.themeshot('dynamodb-new-collection');
});
});

View File

@@ -110,55 +110,6 @@ describe('Charts', () => {
cy.themeshot('new-object-window');
});
it.skip('Database chat - charts', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_databaseChat').click();
cy.wait(1000);
cy.get('body').realType('show me chart of most popular genres');
cy.get('body').realPress('{enter}');
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.testid('chart-canvas', { timeout: 30000 }).should($c =>
expect($c[0].toDataURL()).to.match(/^data:image\/png;base64/)
);
cy.themeshot('database-chat-chart');
});
it.skip('Database chat', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_databaseChat').click();
cy.wait(1000);
cy.get('body').realType('find most popular artist');
cy.get('body').realPress('{enter}');
cy.testid('DatabaseChatTab_executeAllQueries', { timeout: 30000 }).click();
cy.wait(30000);
// cy.contains('Iron Maiden');
cy.themeshot('database-chat');
// cy.testid('DatabaseChatTab_promptInput').click();
// cy.get('body').realType('I need top 10 songs with the biggest income');
// cy.get('body').realPress('{enter}');
// cy.contains('Hot Girl', { timeout: 20000 });
// cy.wait(1000);
// cy.themeshot('database-chat');
});
it.skip('Explain query error', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.testid('TabsPanel_buttonNewObject').click();
cy.testid('NewObjectModal_query').click();
cy.wait(1000);
cy.get('body').realType('select * from Invoice2');
cy.contains('Execute').click();
cy.testid('MessageViewRow-explainErrorButton-1').click();
cy.testid('ChatCodeRenderer_useSqlButton', { timeout: 30000 });
cy.themeshot('explain-query-error');
});
it('Switch language', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();

View File

@@ -52,6 +52,9 @@ function multiTest(testProps, testDefinition) {
if (localconfig.mongo && !testProps.skipMongo) {
it('MongoDB', () => testDefinition('Mongo-connection', 'my_guitar_shop', 'mongo@dbgate-plugin-mongo'));
}
if (localconfig.dynamo && !testProps.skipMongo) {
it('DynamoDB', () => testDefinition('Dynamo-connection', null, 'dynamodb@dbgate-plugin-dynamodb'));
}
}
describe('Transactions', () => {

View File

@@ -5,14 +5,14 @@ services:
restart: always
environment:
POSTGRES_PASSWORD: Pwd2020Db
ports:
ports:
- 16000:5432
mariadb:
image: mariadb
command: --default-authentication-plugin=mysql_native_password
restart: always
ports:
ports:
- 16004:3306
environment:
- MYSQL_ROOT_PASSWORD=Pwd2020Db
@@ -20,21 +20,21 @@ services:
mysql-ssh-login:
build: containers/mysql-ssh-login
restart: always
ports:
ports:
- 16017:3306
- "16012:22"
- '16012:22'
mysql-ssh-keyfile:
build: containers/mysql-ssh-keyfile
restart: always
ports:
ports:
- 16007:3306
- "16008:22"
- '16008:22'
dex:
build: containers/dex
ports:
- "16009:5556"
- '16009:5556'
mongo:
image: mongo:4.4.29
@@ -50,6 +50,11 @@ services:
ports:
- 16011:6379
dynamodb:
image: amazon/dynamodb-local
ports:
- 16015:8000
mssql:
image: mcr.microsoft.com/mssql/server
restart: always

14
e2e-tests/env/ai-chat/.env vendored Normal file
View File

@@ -0,0 +1,14 @@
CONNECTIONS=mysql,graphql
LOCAL_AI_GATEWAY=true
LABEL_mysql=MySql-connection
SERVER_mysql=localhost
USER_mysql=root
PASSWORD_mysql=Pwd2020Db
PORT_mysql=16004
ENGINE_mysql=mysql@dbgate-plugin-mysql
LABEL_graphql=REST GraphQL
ENGINE_graphql=graphql@rest
APISERVERURL1_graphql=http://localhost:4444/graphql/noauth

View File

@@ -1,4 +1,4 @@
CONNECTIONS=mysql,postgres,mongo
CONNECTIONS=mysql,postgres,mongo,dynamo
LABEL_mysql=MySql-connection
SERVER_mysql=localhost
@@ -22,3 +22,9 @@ USER_mongo=root
PASSWORD_mongo=Pwd2020Db
PORT_mongo=16010
ENGINE_mongo=mongo@dbgate-plugin-mongo
LABEL_dynamo=Dynamo-connection
SERVER_dynamo=localhost
PORT_dynamo=16015
AUTH_TYPE_dynamo=onpremise
ENGINE_dynamo=dynamodb@dbgate-plugin-dynamodb

View File

@@ -1,4 +1,4 @@
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite,mongo
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite,mongo,dynamo
LOG_CONNECTION_SENSITIVE_VALUES=true
LABEL_mysql=MySql-connection
@@ -43,3 +43,10 @@ PASSWORD_mongo=Pwd2020Db
PORT_mongo=16010
ENGINE_mongo=mongo@dbgate-plugin-mongo
LABEL_dynamo=Dynamo-connection
SERVER_dynamo=localhost
PORT_dynamo=16015
AUTH_TYPE_dynamo=onpremise
DATABASE_dynamo=localhost
ENGINE_dynamo=dynamodb@dbgate-plugin-dynamodb

168
e2e-tests/init/ai-chat.js Normal file
View File

@@ -0,0 +1,168 @@
const fs = require('fs');
const path = require('path');
const { spawn, spawnSync } = require('child_process');
const rootDir = path.resolve(__dirname, '..', '..');
const testApiDir = path.join(rootDir, 'test-api');
const aigwmockDir = path.join(rootDir, 'packages', 'aigwmock');
const tmpDataDir = path.resolve(__dirname, '..', 'tmpdata');
const testApiPidFile = path.join(tmpDataDir, 'test-api.pid');
const aigwmockPidFile = path.join(tmpDataDir, 'aigwmock.pid');
const isWindows = process.platform === 'win32';
const dbgateApi = require('dbgate-api');
dbgateApi.initializeApiEnvironment();
const dbgatePluginMysql = require('dbgate-plugin-mysql');
dbgateApi.registerPlugins(dbgatePluginMysql);
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// --- MySQL setup (same as charts init) ---
async function initMySqlDatabase(dbname, inputFile) {
const connection = {
server: process.env.SERVER_mysql,
user: process.env.USER_mysql,
password: process.env.PASSWORD_mysql,
port: process.env.PORT_mysql,
engine: 'mysql@dbgate-plugin-mysql',
};
await dbgateApi.executeQuery({
connection,
sql: `DROP DATABASE IF EXISTS ${dbname}`,
});
await dbgateApi.executeQuery({
connection,
sql: `CREATE DATABASE ${dbname}`,
});
await dbgateApi.importDatabase({
connection: { ...connection, database: dbname },
inputFile,
});
}
// --- Process management helpers ---
function readProcessStartTime(pid) {
if (process.platform === 'linux') {
try {
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
return stat.split(' ')[21] || null;
} catch (err) {
return null;
}
}
return null;
}
function isPidStillOurs(meta) {
if (!meta || !(meta.pid > 0)) return false;
if (process.platform === 'linux' && meta.startTime) {
const current = readProcessStartTime(meta.pid);
return current === meta.startTime;
}
return true;
}
function stopProcess(pidFile) {
if (!fs.existsSync(pidFile)) return;
try {
const content = fs.readFileSync(pidFile, 'utf-8').trim();
let meta;
try {
meta = JSON.parse(content);
} catch (_) {
const pid = Number(content);
meta = Number.isInteger(pid) && pid > 0 ? { pid } : null;
}
if (isPidStillOurs(meta)) {
process.kill(meta.pid);
}
} catch (err) {
// ignore stale pid or already terminated
}
try {
fs.unlinkSync(pidFile);
} catch (err) {
// ignore
}
}
function ensureDependencies(dir, checkFile) {
if (fs.existsSync(checkFile)) return;
const command = isWindows ? 'cmd.exe' : 'yarn';
const args = isWindows ? ['/c', 'yarn install --silent'] : ['install', '--silent'];
const result = spawnSync(command, args, {
cwd: dir,
stdio: 'inherit',
env: process.env,
});
if (result.status !== 0) {
throw new Error(`DBGM-00297 Failed to install dependencies in ${dir}`);
}
}
function startBackgroundProcess(dir, pidFile, port) {
const command = isWindows ? 'cmd.exe' : 'yarn';
const args = isWindows ? ['/c', 'yarn start'] : ['start'];
const child = spawn(command, args, {
cwd: dir,
env: { ...process.env, PORT: String(port) },
detached: true,
stdio: 'ignore',
});
child.unref();
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
const meta = { pid: child.pid };
const startTime = readProcessStartTime(child.pid);
if (startTime) meta.startTime = startTime;
fs.writeFileSync(pidFile, JSON.stringify(meta));
}
async function waitForReady(url, timeoutMs = 30000) {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
try {
const response = await fetch(url);
if (response.ok) return;
} catch (err) {
// continue waiting
}
await delay(500);
}
throw new Error(`DBGM-00305 Server at ${url} did not start in time`);
}
// --- Main ---
async function run() {
// 1. Set up MyChinook MySQL database
console.log('[ai-chat init] Setting up MyChinook database...');
await initMySqlDatabase('MyChinook', path.resolve(path.join(__dirname, '../data/chinook-mysql.sql')));
// 2. Start test-api (GraphQL/REST server on port 4444)
console.log('[ai-chat init] Starting test-api on port 4444...');
stopProcess(testApiPidFile);
ensureDependencies(testApiDir, path.join(testApiDir, 'node_modules', 'swagger-jsdoc', 'package.json'));
startBackgroundProcess(testApiDir, testApiPidFile, 4444);
await waitForReady('http://localhost:4444/openapi.json');
console.log('[ai-chat init] test-api is ready');
// 3. Start aigwmock (AI Gateway mock on port 3110)
console.log('[ai-chat init] Starting aigwmock on port 3110...');
stopProcess(aigwmockPidFile);
ensureDependencies(aigwmockDir, path.join(aigwmockDir, 'node_modules', 'express', 'package.json'));
startBackgroundProcess(aigwmockDir, aigwmockPidFile, 3110);
await waitForReady('http://localhost:3110/openrouter/v1/models');
console.log('[ai-chat init] aigwmock is ready');
}
run().catch(err => {
console.error(err);
process.exit(1);
});

View File

@@ -8,6 +8,8 @@ const dbgatePluginMysql = require('dbgate-plugin-mysql');
dbgateApi.registerPlugins(dbgatePluginMysql);
const dbgatePluginPostgres = require('dbgate-plugin-postgres');
dbgateApi.registerPlugins(dbgatePluginPostgres);
const dbgatePluginDynamodb = require('dbgate-plugin-dynamodb');
dbgateApi.registerPlugins(dbgatePluginDynamodb);
async function initMySqlDatabase(dbname, inputFile) {
await dbgateApi.executeQuery({
@@ -125,6 +127,34 @@ async function initMongoDatabase(dbname, inputDirectory) {
// });
}
async function initDynamoDatabase(inputDirectory) {
const dynamodbConnection = {
server: process.env.SERVER_dynamo,
port: process.env.PORT_dynamo,
authType: 'onpremise',
engine: 'dynamodb@dbgate-plugin-dynamodb',
};
const driver = dbgatePluginDynamodb.drivers.find(d => d.engine === 'dynamodb@dbgate-plugin-dynamodb');
const pool = await driver.connect(dynamodbConnection);
const collections = await driver.listCollections(pool);
for (const collection of collections) {
await driver.dropTable(pool, collection);
}
await driver.disconnect(pool);
for (const file of fs.readdirSync(inputDirectory)) {
const pureName = path.parse(file).name;
const src = await dbgateApi.jsonLinesReader({ fileName: path.join(inputDirectory, file) });
const dst = await dbgateApi.tableWriter({
connection: dynamodbConnection,
pureName,
createIfNotExists: true,
});
await dbgateApi.copyStream(src, dst);
}
}
const baseDir = path.join(os.homedir(), '.dbgate');
async function copyFolder(source, target) {
@@ -148,6 +178,8 @@ async function run() {
await initMongoDatabase('MgChinook', path.resolve(path.join(__dirname, '../data/chinook-jsonl')));
await initMongoDatabase('MgRivers', path.resolve(path.join(__dirname, '../data/rivers-jsonl')));
await initDynamoDatabase(path.resolve(path.join(__dirname, '../data/chinook-jsonl')));
await copyFolder(
path.resolve(path.join(__dirname, '../data/chinook-jsonl')),
path.join(baseDir, 'archive-e2etests', 'default')

View File

@@ -7,6 +7,8 @@ const dbgatePluginMysql = require('dbgate-plugin-mysql');
dbgateApi.registerPlugins(dbgatePluginMysql);
const dbgatePluginPostgres = require('dbgate-plugin-postgres');
dbgateApi.registerPlugins(dbgatePluginPostgres);
const dbgatePluginDynamodb = require('dbgate-plugin-dynamodb');
dbgateApi.registerPlugins(dbgatePluginDynamodb);
async function createDb(connection, dropDbSql, createDbSql, database = 'my_guitar_shop', { dropDatabaseName } = {}) {
if (dropDbSql) {
@@ -125,6 +127,28 @@ async function run() {
{ dropDatabaseName: 'my_guitar_shop' }
);
}
if (localconfig.dynamo) {
const dynamodbConnection = {
server: process.env.SERVER_dynamo,
port: process.env.PORT_dynamo,
authType: 'onpremise',
engine: 'dynamodb@dbgate-plugin-dynamodb',
};
const driver = dbgatePluginDynamodb.drivers.find(d => d.engine === 'dynamodb@dbgate-plugin-dynamodb');
const pool = await driver.connect(dynamodbConnection);
const collections = await driver.listCollections(pool);
for (const collection of collections) {
await driver.dropTable(pool, collection);
}
await driver.disconnect(pool);
await dbgateApi.importDbFromFolder({
connection: dynamodbConnection,
folder: path.resolve(path.join(__dirname, '../data/my-guitar-shop')),
});
}
}
dbgateApi.runScript(run);

View File

@@ -27,7 +27,28 @@ async function waitForApiReady(timeoutMs = 30000) {
await delay(500);
}
throw new Error('DBGM-00000 test-api did not start on port 4444 in time');
throw new Error('DBGM-00306 test-api did not start on port 4444 in time');
}
function readProcessStartTime(pid) {
if (process.platform === 'linux') {
try {
const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf-8');
return stat.split(' ')[21] || null;
} catch (err) {
return null;
}
}
return null;
}
function isPidStillOurs(meta) {
if (!meta || !(meta.pid > 0)) return false;
if (process.platform === 'linux' && meta.startTime) {
const current = readProcessStartTime(meta.pid);
return current === meta.startTime;
}
return true;
}
function stopPreviousTestApi() {
@@ -36,9 +57,16 @@ function stopPreviousTestApi() {
}
try {
const pid = Number(fs.readFileSync(pidFile, 'utf-8'));
if (Number.isInteger(pid) && pid > 0) {
process.kill(pid);
const content = fs.readFileSync(pidFile, 'utf-8').trim();
let meta;
try {
meta = JSON.parse(content);
} catch (_) {
const pid = Number(content);
meta = Number.isInteger(pid) && pid > 0 ? { pid } : null;
}
if (isPidStillOurs(meta)) {
process.kill(meta.pid);
}
} catch (err) {
// ignore stale pid file or already terminated process
@@ -67,7 +95,10 @@ function startTestApi() {
child.unref();
fs.mkdirSync(path.dirname(pidFile), { recursive: true });
fs.writeFileSync(pidFile, String(child.pid));
const meta = { pid: child.pid };
const startTime = readProcessStartTime(child.pid);
if (startTime) meta.startTime = startTime;
fs.writeFileSync(pidFile, JSON.stringify(meta));
}
function ensureTestApiDependencies() {
@@ -85,7 +116,7 @@ function ensureTestApiDependencies() {
});
if (result.status !== 0) {
throw new Error('DBGM-00000 Failed to install test-api dependencies');
throw new Error('DBGM-00307 Failed to install test-api dependencies');
}
}

View File

@@ -25,6 +25,7 @@
"cy:run:cloud": "cypress run --spec cypress/e2e/cloud.cy.js",
"cy:run:charts": "cypress run --spec cypress/e2e/charts.cy.js",
"cy:run:redis": "cypress run --spec cypress/e2e/redis.cy.js",
"cy:run:ai-chat": "cypress run --spec cypress/e2e/ai-chat.cy.js",
"start:add-connection": "node clearTestingData && cd .. && node packer/build/bundle.js --listen-api --run-e2e-tests",
"start:portal": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/portal/.env node e2e-tests/init/portal.js && env-cmd -f e2e-tests/env/portal/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
"start:oauth": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/oauth/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
@@ -35,6 +36,7 @@
"start:cloud": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/cloud/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
"start:charts": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/charts/.env node e2e-tests/init/charts.js && env-cmd -f e2e-tests/env/charts/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
"start:redis": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/redis/.env node e2e-tests/init/redis.js && env-cmd -f e2e-tests/env/redis/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
"start:ai-chat": "node clearTestingData && cd .. && env-cmd -f e2e-tests/env/ai-chat/.env node e2e-tests/init/ai-chat.js && env-cmd -f e2e-tests/env/ai-chat/.env node packer/build/bundle.js --listen-api --run-e2e-tests",
"test:add-connection": "start-server-and-test start:add-connection http://localhost:3000 cy:run:add-connection",
"test:portal": "start-server-and-test start:portal http://localhost:3000 cy:run:portal",
"test:oauth": "start-server-and-test start:oauth http://localhost:3000 cy:run:oauth",
@@ -45,7 +47,8 @@
"test:cloud": "start-server-and-test start:cloud http://localhost:3000 cy:run:cloud",
"test:charts": "start-server-and-test start:charts http://localhost:3000 cy:run:charts",
"test:redis": "start-server-and-test start:redis http://localhost:3000 cy:run:redis",
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:rest && yarn test:team && yarn test:multi-sql && yarn test:cloud && yarn test:charts && yarn test:redis",
"test:ai-chat": "start-server-and-test start:ai-chat http://localhost:3000 cy:run:ai-chat",
"test": "yarn test:add-connection && yarn test:portal && yarn test:oauth && yarn test:browse-data && yarn test:rest && yarn test:team && yarn test:multi-sql && yarn test:cloud && yarn test:charts && yarn test:redis && yarn test:ai-chat",
"test:ci": "yarn test"
},
"dependencies": {}

View File

@@ -1 +1,2 @@
test-api.pid
test-api.pid
aigwmock.pid

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "7.1.0",
"version": "7.1.6",
"name": "dbgate-all",
"workspaces": [
"packages/*",

View File

@@ -0,0 +1,14 @@
{
"name": "dbgate-aigwmock",
"version": "1.0.0",
"description": "Mock AI Gateway server for E2E testing",
"main": "src/index.js",
"scripts": {
"start": "node src/index.js"
},
"license": "GPL-3.0",
"dependencies": {
"cors": "^2.8.6",
"express": "^5.2.1"
}
}

View File

@@ -0,0 +1,202 @@
const express = require('express');
const cors = require('cors');
const fs = require('fs');
const path = require('path');
const app = express();
app.use(cors());
app.use(express.json({ limit: '50mb' }));
const responses = JSON.parse(fs.readFileSync(path.join(__dirname, 'mockResponses.json'), 'utf-8'));
let callCounter = 0;
// GET /openrouter/v1/models
app.get('/openrouter/v1/models', (req, res) => {
res.json({
data: [{ id: 'mock-model', name: 'Mock Model' }],
preferredModel: 'mock-model',
});
});
// POST /openrouter/v1/chat/completions
app.post('/openrouter/v1/chat/completions', (req, res) => {
const messages = req.body.messages || [];
// Find the first user message (skip system messages)
const userMessage = messages.find(m => m.role === 'user');
if (!userMessage) {
return streamTextResponse(res, "I don't have enough context to help. Please ask a question.");
}
// Count assistant messages to determine the current step
const assistantCount = messages.filter(m => m.role === 'assistant').length;
// Find matching scenario by regex
const scenario = responses.scenarios.find(s => {
const regex = new RegExp(s.match, 'i');
return regex.test(userMessage.content);
});
if (!scenario) {
console.log(`[aigwmock] No scenario matched for: "${userMessage.content}"`);
return streamTextResponse(res, "I'm a mock AI assistant. I don't have a prepared response for that question.");
}
const step = scenario.steps[assistantCount];
if (!step) {
console.log(`[aigwmock] No more steps for scenario (step ${assistantCount})`);
return streamTextResponse(res, "I've completed my analysis of this topic.");
}
console.log(`[aigwmock] Scenario matched: "${scenario.match}", step ${assistantCount}, type: ${step.type}`);
if (step.type === 'tool_calls') {
return streamToolCallResponse(res, step.tool_calls);
} else {
return streamTextResponse(res, step.content);
}
});
function streamTextResponse(res, content) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const id = `chatcmpl-mock-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
// Split content into chunks for realistic streaming
const chunkSize = 20;
const chunks = [];
for (let i = 0; i < content.length; i += chunkSize) {
chunks.push(content.substring(i, i + chunkSize));
}
// Send initial role chunk
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [{ index: 0, delta: { role: 'assistant', content: '' }, finish_reason: null }],
});
// Send content chunks
for (const chunk of chunks) {
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [{ index: 0, delta: { content: chunk }, finish_reason: null }],
});
}
// Send finish
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
});
res.write('data: [DONE]\n\n');
res.end();
}
function streamToolCallResponse(res, toolCalls) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
});
const id = `chatcmpl-mock-${Date.now()}`;
const created = Math.floor(Date.now() / 1000);
for (let i = 0; i < toolCalls.length; i++) {
const tc = toolCalls[i];
const callId = `call_mock_${++callCounter}`;
const args = JSON.stringify(tc.arguments);
if (i === 0) {
// First tool call: include role
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [
{
index: 0,
delta: {
role: 'assistant',
content: null,
tool_calls: [{ index: i, id: callId, type: 'function', function: { name: tc.name, arguments: '' } }],
},
finish_reason: null,
},
],
});
} else {
// Additional tool calls
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [
{
index: 0,
delta: {
tool_calls: [{ index: i, id: callId, type: 'function', function: { name: tc.name, arguments: '' } }],
},
finish_reason: null,
},
],
});
}
// Stream the arguments
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [
{
index: 0,
delta: {
tool_calls: [{ index: i, function: { arguments: args } }],
},
finish_reason: null,
},
],
});
}
// Send finish with tool_calls reason
writeSSE(res, {
id,
object: 'chat.completion.chunk',
created,
model: 'mock-model',
choices: [{ index: 0, delta: {}, finish_reason: 'tool_calls' }],
});
res.write('data: [DONE]\n\n');
res.end();
}
function writeSSE(res, data) {
res.write(`data: ${JSON.stringify(data)}\n\n`);
}
const port = process.env.PORT || 3110;
app.listen(port, () => {
console.log(`[aigwmock] AI Gateway mock server listening on port ${port}`);
});

View File

@@ -0,0 +1,193 @@
{
"scenarios": [
{
"match": "chart.*popular.*genre|popular.*genre.*chart|most popular genre",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Genre" } }
]
},
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Track" } }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_sql_select",
"arguments": {
"sql": "SELECT g.Name AS genre, COUNT(t.TrackId) AS track_count FROM Genre g JOIN Track t ON g.GenreId = t.GenreId GROUP BY g.Name ORDER BY track_count DESC LIMIT 10"
}
}
]
},
{
"type": "text",
"content": "Here is a chart showing the most popular genres by track count:\n\n```chart\n{\"type\":\"bar\",\"data\":{\"labels\":[\"Rock\",\"Latin\",\"Metal\",\"Alternative & Punk\",\"Jazz\",\"Blues\",\"Classical\",\"R&B/Soul\",\"Reggae\",\"Pop\"],\"datasets\":[{\"label\":\"Track Count\",\"data\":[1297,579,374,332,130,81,74,61,58,48]}]},\"options\":{\"plugins\":{\"title\":{\"display\":true,\"text\":\"Most Popular Genres by Track Count\"}}}}\n```"
}
]
},
{
"match": "most popular artist|popular artist|top artist",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Artist" } }
]
},
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Album" } }
]
},
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Track" } }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_sql_select",
"arguments": {
"sql": "SELECT ar.Name AS artist, COUNT(t.TrackId) AS track_count FROM Artist ar JOIN Album al ON ar.ArtistId = al.ArtistId JOIN Track t ON al.AlbumId = t.AlbumId GROUP BY ar.Name ORDER BY track_count DESC LIMIT 10"
}
}
]
},
{
"type": "text",
"content": "The most popular artist by number of tracks is **Iron Maiden** with 213 tracks, followed by **U2** with 135 tracks and **Led Zeppelin** with 114 tracks."
}
]
},
{
"match": "list.*user|show.*user|get.*user",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "graphql_introspect_schema", "arguments": {} }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_graphql_query",
"arguments": {
"query": "{ users { id firstName lastName email } }"
}
}
]
},
{
"type": "text",
"content": "Here are the users from the GraphQL API. The system contains multiple registered users with their names and email addresses."
}
]
},
{
"match": "chart.*product.*categor|product.*categor.*chart|chart.*categor",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "graphql_introspect_schema", "arguments": {} }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_graphql_query",
"arguments": {
"query": "{ products { category } }"
}
}
]
},
{
"type": "text",
"content": "Here is a bar chart showing the distribution of products across categories:\n\n```chart\n{\"type\":\"bar\",\"data\":{\"labels\":[\"Electronics\",\"Clothing\",\"Books\",\"Home & Garden\",\"Sports\",\"Toys\"],\"datasets\":[{\"label\":\"Number of Products\",\"data\":[35,30,33,38,32,32]}]},\"options\":{\"plugins\":{\"title\":{\"display\":true,\"text\":\"Products by Category\"}}}}\n```"
}
]
},
{
"match": "most expensive product|expensive.*product|highest price",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "graphql_introspect_schema", "arguments": {} }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_graphql_query",
"arguments": {
"query": "{ products { id name price category } }"
}
}
]
},
{
"type": "text",
"content": "Based on the query results, I found the most expensive product in the system. The product details are shown in the query results above."
}
]
},
{
"match": "show.*categor|list.*categor|all.*categor",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "graphql_introspect_schema", "arguments": {} }
]
},
{
"type": "tool_calls",
"tool_calls": [
{
"name": "execute_graphql_query",
"arguments": {
"query": "{ categories { id name description active } }"
}
}
]
},
{
"type": "text",
"content": "Here are all the categories available in the system. Each category has a name, description, and active status indicating whether it is currently in use."
}
]
},
{
"match": "Explain the following error|doesn't exist|does not exist",
"steps": [
{
"type": "tool_calls",
"tool_calls": [
{ "name": "get_table_schema", "arguments": { "table": "Invoice" } }
]
},
{
"type": "text",
"content": "The error occurs because the table `Invoice2` does not exist in the `MyChinook` database. The correct table name is `Invoice`. Here is the corrected query:\n\n```sql\nSELECT * FROM Invoice\n```\n\nThe table name had a typo — `Invoice2` instead of `Invoice`. The `Invoice` table contains columns like `InvoiceId`, `CustomerId`, `InvoiceDate`, `Total`, and billing address fields."
}
]
}
]
}

View File

@@ -31,7 +31,7 @@
"cors": "^2.8.5",
"cross-env": "^6.0.3",
"dbgate-datalib": "^7.0.0-alpha.1",
"dbgate-query-splitter": "^4.11.9",
"dbgate-query-splitter": "^4.12.0",
"dbgate-rest": "^7.0.0-alpha.1",
"dbgate-sqltree": "^7.0.0-alpha.1",
"dbgate-tools": "^7.0.0-alpha.1",

View File

@@ -502,6 +502,9 @@ module.exports = {
_id: '__model',
};
}
if (!conid) {
return null;
}
await testConnectionPermission(conid, req);
return this.getCore({ conid, mask: true });
},

View File

@@ -15,6 +15,7 @@ const {
getLogger,
extractErrorLogData,
filterStructureBySchema,
serializeJsTypesForJsonStringify,
} = require('dbgate-tools');
const { html, parse } = require('diff2html');
const { handleProcessCommunication } = require('../utility/processComm');
@@ -94,10 +95,12 @@ module.exports = {
}
},
handle_response(conid, database, { msgid, ...response }) {
const [resolve, reject, additionalData] = this.requests[msgid];
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
const [resolve, reject, additionalData] = this.requests[msgid] || [];
if (resolve) {
resolve(response);
if (additionalData?.auditLogger) {
additionalData?.auditLogger(response);
}
}
delete this.requests[msgid];
},
@@ -224,12 +227,13 @@ module.exports = {
this.close(conid, database, false);
});
subprocess.send({
const connectMessage = serializeJsTypesForJsonStringify({
msgtype: 'connect',
connection: { ...connection, database },
structure: lastClosed ? lastClosed.structure : null,
globalSettings: await config.getSettings(),
});
subprocess.send(connectMessage);
return newOpened;
},
@@ -237,9 +241,10 @@ module.exports = {
sendRequest(conn, message, additionalData = {}) {
const msgid = crypto.randomUUID();
const promise = new Promise((resolve, reject) => {
this.requests[msgid] = [resolve, reject, additionalData];
this.requests[msgid] = [resolve, reject, additionalData, conn.conid, conn.database];
try {
conn.subprocess.send({ msgid, ...message });
const serializedMessage = serializeJsTypesForJsonStringify({ msgid, ...message });
conn.subprocess.send(serializedMessage);
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00115 Error sending request do process');
this.close(conn.conid, conn.database);
@@ -261,12 +266,12 @@ module.exports = {
},
sqlSelect_meta: true,
async sqlSelect({ conid, database, select, auditLogSessionGroup }, req) {
async sqlSelect({ conid, database, select, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
const res = await this.sendRequest(
opened,
{ msgtype: 'sqlSelect', select },
{ msgtype: 'sqlSelect', select, commandTimeout },
{
auditLogger:
auditLogSessionGroup && select?.from?.name?.pureName
@@ -341,9 +346,12 @@ module.exports = {
},
collectionData_meta: true,
async collectionData({ conid, database, options, auditLogSessionGroup }, req) {
async collectionData({ conid, database, options, commandTimeout, auditLogSessionGroup }, req) {
await testConnectionPermission(conid, req);
const opened = await this.ensureOpened(conid, database);
if (commandTimeout && options) {
options.commandTimeout = commandTimeout;
}
const res = await this.sendRequest(
opened,
{ msgtype: 'collectionData', options },
@@ -473,6 +481,7 @@ module.exports = {
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
const tablePermissions = await loadTablePermissionsFromRequest(req);
const databasePermissionRole = getDatabasePermissionRole(conid, database, databasePermissions);
const fieldsAndRoles = [
[changeSet.inserts, 'create_update_delete'],
[changeSet.deletes, 'create_update_delete'],
@@ -487,7 +496,7 @@ module.exports = {
operation.schemaName,
operation.pureName,
tablePermissions,
databasePermissions
databasePermissionRole
);
if (getTablePermissionRoleLevelIndex(role) < getTablePermissionRoleLevelIndex(requiredRole)) {
throw new Error('DBGM-00262 Permission not granted');
@@ -576,6 +585,24 @@ module.exports = {
};
},
pingDatabases_meta: true,
async pingDatabases({ databases }, req) {
if (!databases || !Array.isArray(databases)) return { status: 'ok' };
for (const { conid, database } of databases) {
if (!conid || !database) continue;
const existing = this.opened.find(x => x.conid == conid && x.database == database);
if (existing) {
try {
existing.subprocess.send({ msgtype: 'ping' });
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00308 Error pinging DB connection');
this.close(conid, database);
}
}
}
return { status: 'ok' };
},
refresh_meta: true,
async refresh({ conid, database, keepOpen }, req) {
await testConnectionPermission(conid, req);
@@ -618,6 +645,15 @@ module.exports = {
structure: existing.structure,
};
socket.emitChanged(`database-status-changed`, { conid, database });
// Reject all pending requests for this connection
for (const [msgid, entry] of Object.entries(this.requests)) {
const [resolve, reject, additionalData, reqConid, reqDatabase] = entry;
if (reqConid === conid && reqDatabase === database) {
reject('DBGM-00309 Database connection closed');
delete this.requests[msgid];
}
}
}
},

View File

@@ -15,7 +15,8 @@ const getDiagramExport = require('../utility/getDiagramExport');
const apps = require('./apps');
const getMapExport = require('../utility/getMapExport');
const dbgateApi = require('../shell');
const { getLogger } = require('dbgate-tools');
const { getLogger, getSqlFrontMatter } = require('dbgate-tools');
const yaml = require('js-yaml');
const platformInfo = require('../utility/platformInfo');
const { checkSecureFilePathsWithoutDirectory, checkSecureDirectories } = require('../utility/security');
const { copyAppLogsIntoFile, getRecentAppLogRecords } = require('../utility/appLogStore');
@@ -35,13 +36,46 @@ function deserialize(format, text) {
module.exports = {
list_meta: true,
async list({ folder }, req) {
async list({ folder, parseFrontMatter }, req) {
const loadedPermissions = await loadPermissionsFromRequest(req);
if (!hasPermission(`files/${folder}/read`, loadedPermissions)) return [];
const dir = path.join(filesdir(), folder);
if (!(await fs.exists(dir))) return [];
const files = (await fs.readdir(dir)).map(file => ({ folder, file }));
return files;
const fileNames = await fs.readdir(dir);
if (!parseFrontMatter) {
return fileNames.map(file => ({ folder, file }));
}
const result = [];
for (const file of fileNames) {
const item = { folder, file };
let fh;
try {
fh = await require('fs').promises.open(path.join(dir, file), 'r');
const buf = new Uint8Array(512);
const { bytesRead } = await fh.read(buf, 0, 512, 0);
let text = Buffer.from(buf.buffer, 0, bytesRead).toString('utf-8');
if (text.includes('-- >>>') && !text.includes('-- <<<')) {
const stat = await fh.stat();
const fullSize = Math.min(stat.size, 4096);
if (fullSize > 512) {
const fullBuf = new Uint8Array(fullSize);
const { bytesRead: fullBytesRead } = await fh.read(fullBuf, 0, fullSize, 0);
text = Buffer.from(fullBuf.buffer, 0, fullBytesRead).toString('utf-8');
}
}
const fm = getSqlFrontMatter(text, yaml);
if (fm?.connectionId) item.connectionId = fm.connectionId;
if (fm?.databaseName) item.databaseName = fm.databaseName;
} catch (e) {
// ignore read errors for individual files
} finally {
if (fh) await fh.close().catch(() => {});
}
result.push(item);
}
return result;
},
listAll_meta: true,
@@ -257,6 +291,13 @@ module.exports = {
return true;
},
exportDiagramPng_meta: true,
async exportDiagramPng({ filePath, pngBase64 }) {
const base64 = pngBase64.replace(/^data:image\/png;base64,/, '');
await fs.writeFile(filePath, Buffer.from(base64, 'base64'));
return true;
},
getFileRealPath_meta: true,
async getFileRealPath({ folder, file }, req) {
const loadedPermissions = await loadPermissionsFromRequest(req);

View File

@@ -1,5 +1,8 @@
const { filterName } = require('dbgate-tools');
const { filterName, getLogger, extractErrorLogData } = require('dbgate-tools');
const logger = getLogger('jsldata');
const { jsldir, archivedir } = require('../utility/directories');
const fs = require('fs');
const path = require('path');
const lineReader = require('line-reader');
const _ = require('lodash');
const { __ } = require('lodash/fp');
@@ -149,6 +152,10 @@ module.exports = {
getRows_meta: true,
async getRows({ jslid, offset, limit, filters, sort, formatterFunction }) {
const fileName = getJslFileName(jslid);
if (!fs.existsSync(fileName)) {
return [];
}
const datastore = await this.ensureDatastore(jslid, formatterFunction);
return datastore.getRows(offset, limit, _.isEmpty(filters) ? null : filters, _.isEmpty(sort) ? null : sort);
},
@@ -159,6 +166,72 @@ module.exports = {
return fs.existsSync(fileName);
},
streamRows_meta: {
method: 'get',
raw: true,
},
streamRows(req, res) {
const { jslid } = req.query;
if (!jslid) {
res.status(400).json({ apiErrorMessage: 'Missing jslid' });
return;
}
// Reject file:// jslids — they resolve to arbitrary server-side paths
if (jslid.startsWith('file://')) {
res.status(403).json({ apiErrorMessage: 'Forbidden jslid scheme' });
return;
}
const fileName = getJslFileName(jslid);
if (!fs.existsSync(fileName)) {
res.status(404).json({ apiErrorMessage: 'File not found' });
return;
}
// Dereference symlinks and normalize case (Windows) before the allow-list check.
// realpathSync is safe here because existsSync confirmed the file is present.
// path.resolve() alone cannot dereference symlinks, so a symlink inside an allowed
// root could otherwise point to an arbitrary external path.
const normalize = p => (process.platform === 'win32' ? p.toLowerCase() : p);
const resolveRoot = r => { try { return fs.realpathSync(r); } catch { return path.resolve(r); } };
let realFile;
try {
realFile = fs.realpathSync(fileName);
} catch {
res.status(403).json({ apiErrorMessage: 'Forbidden path' });
return;
}
const allowedRoots = [jsldir(), archivedir()].map(r => normalize(resolveRoot(r)) + path.sep);
const isAllowed = allowedRoots.some(root => normalize(realFile).startsWith(root));
if (!isAllowed) {
logger.warn({ jslid, realFile }, 'DBGM-00000 streamRows rejected path outside allowed roots');
res.status(403).json({ apiErrorMessage: 'Forbidden path' });
return;
}
res.setHeader('Content-Type', 'application/x-ndjson');
res.setHeader('Cache-Control', 'no-cache');
const stream = fs.createReadStream(realFile, 'utf-8');
req.on('close', () => {
stream.destroy();
});
stream.on('error', err => {
logger.error(extractErrorLogData(err), 'DBGM-00000 Error streaming JSONL file');
if (!res.headersSent) {
res.status(500).json({ apiErrorMessage: 'Stream error' });
} else {
res.end();
}
});
stream.pipe(res);
},
getStats_meta: true,
getStats({ jslid }) {
const file = `${getJslFileName(jslid)}.stats`;

View File

@@ -171,7 +171,7 @@ module.exports = {
const databasePermissions = await loadDatabasePermissionsFromRequest(req);
const res = [];
for (const db of opened?.databases ?? []) {
const databasePermissionRole = getDatabasePermissionRole(db.id, db.name, databasePermissions);
const databasePermissionRole = getDatabasePermissionRole(conid, db.name, databasePermissions);
if (databasePermissionRole != 'deny') {
res.push({
...db,

View File

@@ -228,6 +228,19 @@ module.exports = {
return { state: 'ok' };
},
setIsolationLevel_meta: true,
async setIsolationLevel({ sesid, level }) {
const session = this.opened.find(x => x.sesid == sesid);
if (!session) {
throw new Error('Invalid session');
}
logger.info({ sesid, level }, 'DBGM-00315 Setting transaction isolation level');
session.subprocess.send({ msgtype: 'setIsolationLevel', level });
return { state: 'ok' };
},
executeReader_meta: true,
async executeReader({ conid, database, sql, queryName, appFolder }) {
const { sesid } = await this.create({ conid, database });

View File

@@ -234,12 +234,12 @@ async function handleRunOperation({ msgid, operation, useTransaction }, skipRead
}
}
async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false) {
async function handleQueryData({ msgid, sql, range, commandTimeout }, skipReadonlyCheck = false) {
await waitConnected();
const driver = requireEngineDriver(storedConnection);
try {
if (!skipReadonlyCheck) ensureExecuteCustomScript(driver);
const res = await driver.query(dbhan, sql, { range });
const res = await driver.query(dbhan, sql, { range, commandTimeout });
process.send({ msgtype: 'response', msgid, ...serializeJsTypesForJsonStringify(res) });
} catch (err) {
process.send({
@@ -250,11 +250,11 @@ async function handleQueryData({ msgid, sql, range }, skipReadonlyCheck = false)
}
}
async function handleSqlSelect({ msgid, select }) {
async function handleSqlSelect({ msgid, select, commandTimeout }) {
const driver = requireEngineDriver(storedConnection);
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
return handleQueryData({ msgid, sql: dmp.s, range: select.range }, true);
return handleQueryData({ msgid, sql: dmp.s, range: select.range, commandTimeout }, true);
}
async function handleDriverDataCore(msgid, callMethod, { logName }) {

View File

@@ -77,6 +77,38 @@ async function handleStopProfiler({ jslid }) {
currentProfiler = null;
}
async function handleSetIsolationLevel({ level }) {
lastActivity = new Date().getTime();
await waitConnected();
const driver = requireEngineDriver(storedConnection);
if (!driver.setTransactionIsolationLevel) {
process.send({ msgtype: 'done', skipFinishedMessage: true });
return;
}
if (driver.isolationLevels && level && !driver.isolationLevels.includes(level)) {
process.send({
msgtype: 'info',
info: {
message: `Isolation level "${level}" is not supported by this driver. Supported levels: ${driver.isolationLevels.join(', ')}`,
severity: 'error',
},
});
process.send({ msgtype: 'done', skipFinishedMessage: true });
return;
}
executingScripts++;
try {
await driver.setTransactionIsolationLevel(dbhan, level);
process.send({ msgtype: 'done', controlCommand: 'setIsolationLevel' });
} finally {
executingScripts--;
}
}
async function handleExecuteControlCommand({ command }) {
lastActivity = new Date().getTime();
@@ -210,6 +242,7 @@ const messageHandlers = {
connect: handleConnect,
executeQuery: handleExecuteQuery,
executeControlCommand: handleExecuteControlCommand,
setIsolationLevel: handleSetIsolationLevel,
executeReader: handleExecuteReader,
startProfiler: handleStartProfiler,
stopProfiler: handleStopProfiler,

View File

@@ -698,6 +698,30 @@ module.exports = {
"columnName": "id_original",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "connections",
"columnName": "httpProxyUrl",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "connections",
"columnName": "httpProxyUser",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "connections",
"columnName": "httpProxyPassword",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "connections",
"columnName": "defaultIsolationLevel",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [

View File

@@ -132,7 +132,35 @@ async function connectUtility(driver, storedConnection, connectionMode, addition
}
connection.ssl = await extractConnectionSslParams(connection);
connection.axios = axios.default;
const proxyUrl = String(connection.httpProxyUrl ?? '').trim();
const proxyUser = String(connection.httpProxyUser ?? '').trim();
const proxyPassword = String(connection.httpProxyPassword ?? '').trim();
if (!proxyUrl && (proxyUser || proxyPassword)) {
throw new Error('DBGM-00329 Proxy user or password is set but proxy URL is missing');
}
if (proxyUrl) {
let parsedProxy;
try {
const parsed = new URL(proxyUrl.includes('://') ? proxyUrl : `http://${proxyUrl}`);
parsedProxy = {
protocol: parsed.protocol.replace(':', ''),
host: parsed.hostname,
port: parsed.port ? parseInt(parsed.port, 10) : (parsed.protocol === 'https:' ? 443 : 80),
};
const username = connection.httpProxyUser ?? parsed.username;
const rawPassword = connection.httpProxyPassword ?? parsed.password;
const password = decryptPasswordString(rawPassword);
if (username) {
parsedProxy.auth = { username, password: password ?? '' };
}
} catch (err) {
throw new Error(`DBGM-00334 Invalid proxy URL "${proxyUrl}": ${err && err.message ? err.message : err}`);
}
connection.axios = axios.default.create({ proxy: parsedProxy });
} else {
connection.axios = axios.default;
}
const conn = await driver.connect({ conid: connectionLoaded?._id, ...connection, ...additionalOptions });
return conn;

View File

@@ -101,7 +101,7 @@ function decryptObjectPasswordField(obj, field, encryptor = null) {
return obj;
}
const fieldsToEncrypt = ['password', 'sshPassword', 'sshKeyfilePassword', 'connectionDefinition'];
const fieldsToEncrypt = ['password', 'sshPassword', 'sshKeyfilePassword', 'connectionDefinition', 'httpProxyPassword'];
const additionalFieldsToMask = [
'databaseUrl',
'server',

View File

@@ -96,8 +96,9 @@ async function loadFilePermissionsFromRequest(req) {
}
function matchDatabasePermissionRow(conid, database, permissionRow) {
if (permissionRow.connection_id) {
if (conid != permissionRow.connection_id) {
const connectionIdentifier = permissionRow.connection_conid ?? permissionRow.connection_id;
if (connectionIdentifier) {
if (conid != connectionIdentifier) {
return false;
}
}

View File

@@ -30,7 +30,7 @@ export const graphQlDriver: EngineDriver = {
icon: '<svg version="1.1" id="GraphQL_Logo" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 400 400" enable-background="new 0 0 400 400" xml:space="preserve"><g><g><g><rect x="122" y="-0.4" transform="matrix(-0.866 -0.5 0.5 -0.866 163.3196 363.3136)" fill="#E535AB" width="16.6" height="320.3"/></g></g><g><g><rect x="39.8" y="272.2" fill="#E535AB" width="320.3" height="16.6"/></g></g><g><g><rect x="37.9" y="312.2" transform="matrix(-0.866 -0.5 0.5 -0.866 83.0693 663.3409)" fill="#E535AB" width="185" height="16.6"/></g></g><g><g><rect x="177.1" y="71.1" transform="matrix(-0.866 -0.5 0.5 -0.866 463.3409 283.0693)" fill="#E535AB" width="185" height="16.6"/></g></g><g><g><rect x="122.1" y="-13" transform="matrix(-0.5 -0.866 0.866 -0.5 126.7903 232.1221)" fill="#E535AB" width="16.6" height="185"/></g></g><g><g><rect x="109.6" y="151.6" transform="matrix(-0.5 -0.866 0.866 -0.5 266.0828 473.3766)" fill="#E535AB" width="320.3" height="16.6"/></g></g><g><g><rect x="52.5" y="107.5" fill="#E535AB" width="16.6" height="185"/></g></g><g><g><rect x="330.9" y="107.5" fill="#E535AB" width="16.6" height="185"/></g></g><g><g><rect x="262.4" y="240.1" transform="matrix(-0.5 -0.866 0.866 -0.5 126.7953 714.2875)" fill="#E535AB" width="14.5" height="160.9"/></g></g><path fill="#E535AB" d="M369.5,297.9c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C373.5,259.9,379.2,281.2,369.5,297.9"/><path fill="#E535AB" d="M90.9,137c-9.6,16.7-31,22.4-47.7,12.8c-16.7-9.6-22.4-31-12.8-47.7c9.6-16.7,31-22.4,47.7-12.8 C94.8,99,100.5,120.3,90.9,137"/><path fill="#E535AB" d="M30.5,297.9c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6,16.7,3.9,38-12.8,47.7 C61.4,320.3,40.1,314.6,30.5,297.9"/><path fill="#E535AB" d="M309.1,137c-9.6-16.7-3.9-38,12.8-47.7c16.7-9.6,38-3.9,47.7,12.8c9.6-16.7,3.9-38-12.8,47.7 C340.1,159.4,318.7,153.7,309.1,137"/><path fill="#E535AB" d="M200,395.8c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,380.1,219.3,395.8,200,395.8"/><path fill="#E535AB" d="M200,74c-19.3,0-34.9-15.6-34.9-34.9c0-19.3,15.6-34.9,34.9-34.9c19.3,0,34.9,15.6,34.9,34.9 C234.9,58.4,219.3,74,200,74"/></g></svg>',
showConnectionField: (field, values) => {
if (apiDriverBase.showAuthConnectionField(field, values)) return true;
if (apiDriverBase.showConnectionField(field, values)) return true;
if (field === 'apiServerUrl1') return true;
return false;
},

View File

@@ -24,7 +24,7 @@ function resolveServiceRoot(contextUrl: string | undefined, fallbackUrl: string)
async function loadODataServiceDocument(dbhan: any) {
if (!dbhan?.connection?.apiServerUrl1) {
throw new Error('DBGM-00000 OData endpoint URL is not configured');
throw new Error('DBGM-00330 OData endpoint URL is not configured');
}
const response = await dbhan.axios.get(dbhan.connection.apiServerUrl1, {
@@ -33,11 +33,11 @@ async function loadODataServiceDocument(dbhan: any) {
const document = response?.data;
if (!document || typeof document !== 'object') {
throw new Error('DBGM-00000 OData service document is empty or invalid');
throw new Error('DBGM-00331 OData service document is empty or invalid');
}
if (!document['@odata.context']) {
throw new Error('DBGM-00000 OData service document does not contain @odata.context');
throw new Error('DBGM-00332 OData service document does not contain @odata.context');
}
return document;
@@ -60,7 +60,7 @@ export const oDataDriver: EngineDriver = {
apiServerUrl1Label: 'OData Service URL',
showConnectionField: (field, values) => {
if (apiDriverBase.showAuthConnectionField(field, values)) return true;
if (apiDriverBase.showConnectionField(field, values)) return true;
if (field === 'apiServerUrl1') return true;
return false;
},

View File

@@ -39,7 +39,7 @@ export const openApiDriver: EngineDriver = {
loadApiServerUrl2Options: true,
showConnectionField: (field, values) => {
if (apiDriverBase.showAuthConnectionField(field, values)) return true;
if (apiDriverBase.showConnectionField(field, values)) return true;
if (field === 'apiServerUrl1') return true;
if (field === 'apiServerUrl2') return true;
return false;

View File

@@ -39,4 +39,12 @@ export const apiDriverBase = {
}
return false;
},
showConnectionField: (field, values) => {
if (apiDriverBase.showAuthConnectionField(field, values)) return true;
if (field === 'httpProxyUrl') return true;
if (field === 'httpProxyUser') return true;
if (field === 'httpProxyPassword') return true;
return false;
},
};

View File

@@ -1,8 +1,55 @@
import type { SqlDumper } from 'dbgate-types';
import { Condition, BinaryCondition } from './types';
import { Condition, BinaryCondition, LikeCondition } from './types';
import { dumpSqlExpression } from './dumpSqlExpression';
import { dumpSqlSelect } from './dumpSqlCommand';
function dumpLikeAsFunctionCondition(dmp: SqlDumper, condition: LikeCondition) {
// For DynamoDB: contains() works only on string attributes
// For numeric values, search both as number and as string
const likeExpr = condition.right;
let isNumericValue = false;
let numericStringValue = '';
if (likeExpr.exprType === 'value' && typeof likeExpr.value === 'string') {
const cleanedStr = (likeExpr.value || '').replace(/%/g, '').trim();
// Only match valid decimal numbers (not Infinity, NaN, etc.)
isNumericValue = /^-?\d+(\.\d+)?$/.test(cleanedStr);
numericStringValue = cleanedStr;
} else if (likeExpr.exprType === 'value' && typeof likeExpr.value === 'number') {
isNumericValue = Number.isFinite(likeExpr.value);
numericStringValue = String(likeExpr.value);
}
if (isNumericValue) {
// For numeric values: (column = value OR contains(column, 'value'))
dmp.putRaw('(');
dumpSqlExpression(dmp, condition.left);
dmp.putRaw(' = ');
dmp.put('%s', numericStringValue);
dmp.putRaw(' OR contains(');
dumpSqlExpression(dmp, condition.left);
dmp.putRaw(', ');
dmp.put('%v', numericStringValue);
dmp.putRaw('))');
} else {
// String value: contains(column, value)
dmp.putRaw('contains(');
dumpSqlExpression(dmp, condition.left);
dmp.putRaw(', ');
if (likeExpr.exprType === 'value') {
let cleanValue = likeExpr.value;
if (typeof cleanValue === 'string') {
cleanValue = cleanValue.replace(/%/g, '');
}
dmp.put('%v', cleanValue);
} else {
dumpSqlExpression(dmp, likeExpr);
}
dmp.putRaw(')');
}
}
export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
switch (condition.conditionType) {
case 'binary':
@@ -51,9 +98,13 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
});
break;
case 'like':
dumpSqlExpression(dmp, condition.left);
dmp.put(dmp.dialect.ilike ? ' ^ilike ' : ' ^like ');
dumpSqlExpression(dmp, condition.right);
if (dmp.dialect.likeAsFunction) {
dumpLikeAsFunctionCondition(dmp, condition);
} else {
dumpSqlExpression(dmp, condition.left);
dmp.put(dmp.dialect.ilike ? ' ^ilike ' : ' ^like ');
dumpSqlExpression(dmp, condition.right);
}
break;
case 'notLike':
dumpSqlExpression(dmp, condition.left);

View File

@@ -33,7 +33,7 @@
},
"dependencies": {
"blueimp-md5": "^2.19.0",
"dbgate-query-splitter": "^4.11.9",
"dbgate-query-splitter": "^4.12.0",
"dbgate-sqltree": "^7.0.0-alpha.1",
"debug": "^4.3.4",
"json-stable-stringify": "^1.0.1",

View File

@@ -49,6 +49,26 @@ export function base64ToHex(base64String) {
return '0x' + hexString.toUpperCase();
}
export function base64ToUuid(base64String): string | null {
let binaryString: string;
try {
binaryString = atob(base64String);
} catch {
return null;
}
if (binaryString.length !== 16) {
return null;
}
const hex = Array.from(binaryString, c => c.charCodeAt(0).toString(16).padStart(2, '0')).join('');
return [
hex.slice(0, 8),
hex.slice(8, 12),
hex.slice(12, 16),
hex.slice(16, 20),
hex.slice(20, 32),
].join('-');
}
export function hexToBase64(hexString) {
const binaryString = hexString
.match(/.{1,2}/g)
@@ -57,6 +77,23 @@ export function hexToBase64(hexString) {
return btoa(binaryString);
}
const uuidPattern = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
const uuidRegex = new RegExp(`^${uuidPattern}$`);
const uuid3WrapperRegex = new RegExp(`^UUID3\\("(${uuidPattern})"\\)$`);
const uuid4WrapperRegex = new RegExp(`^UUID\\("(${uuidPattern})"\\)$`);
export function uuidToBase64(uuid: string): string | null {
if (!uuid || !uuidRegex.test(uuid)) {
return null;
}
const hex = uuid.replace(/-/g, '');
const binaryString = hex
.match(/.{1,2}/g)
.map(byte => String.fromCharCode(parseInt(byte, 16)))
.join('');
return btoa(binaryString);
}
export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
if (!_isString(value)) return value;
@@ -65,6 +102,20 @@ export function parseCellValue(value, editorTypes?: DataEditorTypesBehaviour) {
}
if (editorTypes?.parseHexAsBuffer) {
const mUuid3 = value.match(uuid3WrapperRegex);
if (mUuid3) {
const base64Uuid3 = uuidToBase64(mUuid3[1]);
if (base64Uuid3 != null) return { $binary: { base64: base64Uuid3, subType: '03' } };
}
const mUuid4 = value.match(uuid4WrapperRegex);
if (mUuid4) {
const base64Uuid4 = uuidToBase64(mUuid4[1]);
if (base64Uuid4 != null) return { $binary: { base64: base64Uuid4, subType: '04' } };
}
if (uuidRegex.test(value)) {
const base64UuidPlain = uuidToBase64(value);
if (base64UuidPlain != null) return { $binary: { base64: base64UuidPlain, subType: '04' } };
}
const mHex = value.match(/^0x([0-9a-fA-F][0-9a-fA-F])+$/);
if (mHex) {
return {
@@ -266,6 +317,18 @@ export function stringifyCellValue(
if (value === false) return { value: 'false', gridStyle: 'valueCellStyle' };
if (value?.$binary?.base64) {
const subType = value.$binary.subType;
if (subType === '03' || subType === '04') {
const uuidStr = base64ToUuid(value.$binary.base64);
if (uuidStr != null) {
if (intent === 'gridCellIntent' || intent === 'exportIntent' || intent === 'clipboardIntent' || intent === 'stringConversionIntent') {
return { value: uuidStr, gridStyle: 'valueCellStyle' };
}
// For editing intents: tag with subType so parseCellValue can round-trip it
const tag = subType === '03' ? 'UUID3' : 'UUID';
return { value: `${tag}("${uuidStr}")`, gridStyle: 'valueCellStyle' };
}
}
return {
value: base64ToHex(value.$binary.base64),
gridStyle: 'valueCellStyle',

View File

@@ -4,6 +4,7 @@ export interface SqlDialect {
rangeSelect?: boolean;
limitSelect?: boolean;
ilike?: boolean;
likeAsFunction?: boolean;
rowNumberOverPaging?: boolean;
topRecords?: boolean;
stringEscapeChar: string;

View File

@@ -59,6 +59,7 @@ export interface QueryOptions {
importSqlDump?: boolean;
range?: { offset: number; limit: number };
readonly?: boolean;
commandTimeout?: number;
}
export interface WriteTableOptions {
@@ -423,6 +424,10 @@ export interface EngineDriver<TClient = any, TDataBase = any> extends FilterBeha
engine: string;
conid?: string;
};
setTransactionIsolationLevel?(dbhan: DatabaseHandle<TClient, TDataBase>, level: string): Promise<void>;
isolationLevels?: string[];
defaultIsolationLevel?: string;
}
export interface DatabaseModification {

View File

@@ -29,11 +29,11 @@
"chartjs-plugin-datalabels": "^2.2.0",
"cross-env": "^7.0.3",
"dbgate-datalib": "^7.0.0-alpha.1",
"dbgate-query-splitter": "^4.11.9",
"dbgate-query-splitter": "^4.12.0",
"dbgate-rest": "^7.0.0-alpha.1",
"dbgate-sqltree": "^7.0.0-alpha.1",
"dbgate-tools": "^7.0.0-alpha.1",
"dbgate-types": "^7.0.0-alpha.1",
"dbgate-rest": "^7.0.0-alpha.1",
"diff": "^5.0.0",
"diff2html": "^3.4.13",
"file-selector": "^0.2.4",
@@ -54,27 +54,29 @@
"svelte": "^4.2.20",
"svelte-check": "^1.0.0",
"svelte-markdown": "^0.1.4",
"svelte-preprocess": "^4.9.5",
"svelte-preprocess": "^6.0.0",
"svelte-select": "^4.4.7",
"tailwindcss": "^4.1.18",
"tslib": "^2.3.1",
"typescript": "^4.4.3",
"typescript": "^5.7.0",
"uuid": "^3.4.0"
},
"dependencies": {
"@langchain/core": "^0.3.72",
"@langchain/langgraph": "^0.4.9",
"@langchain/openai": "^0.6.9",
"@langchain/core": "^1.1.29",
"@langchain/langgraph": "^1.2.0",
"@langchain/openai": "^1.2.11",
"@messageformat/core": "^3.4.0",
"chartjs-plugin-zoom": "^1.2.0",
"date-fns": "^4.1.0",
"debug": "^4.3.4",
"dom-to-image": "^2.6.0",
"dompurify": "^3.3.2",
"flatpickr": "^4.6.13",
"fuzzy": "^0.1.3",
"highlight.js": "^11.11.1",
"interval-operations": "^1.0.7",
"leaflet": "^1.8.0",
"openai": "^5.10.1",
"openai": "^6.24.0",
"wellknown": "^0.5.0",
"xml-formatter": "^3.6.4",
"zod": "^4.1.5"

View File

@@ -54,6 +54,9 @@ export default defineConfig([
cssEntryFileNames: 'bundle.css',
minify: production,
},
// dbgate-types is a TypeScript-only package (no runtime code).
// Mark it external so rolldown doesn't try to bundle it.
external: ['dbgate-types'],
platform: 'browser',
resolve: {
conditionNames: ['svelte', 'browser', 'import'],
@@ -122,7 +125,14 @@ export default defineConfig([
}),
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
preprocess: sveltePreprocess({
sourceMap: !production,
typescript: {
compilerOptions: {
verbatimModuleSyntax: true,
},
},
}),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,

View File

@@ -1,4 +1,5 @@
<script>
import _ from 'lodash';
import Link from '../elements/Link.svelte';
import { plusExpandIcon } from '../icons/expandIcons';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
<script lang="ts" context="module">
import { copyTextToClipboard } from '../utility/clipboard';
import { _t, _tval, DefferedTranslationResult } from '../translations';
import { _t, _tval, type DefferedTranslationResult } from '../translations';
import sqlFormatter from 'sql-formatter';
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);

View File

@@ -1,4 +1,5 @@
<script context="module">
import { __t } from '../translations';
registerCommand({
id: 'commandPalette.show',
category: __t('command.commandPalette', { defaultMessage: 'Command palette' }),
@@ -87,7 +88,7 @@
import { getLocalStorage } from '../utility/storageCache';
import registerCommand from './registerCommand';
import { formatKeyText, switchCurrentDatabase } from '../utility/common';
import { _tval, __t, _t } from '../translations';
import { _tval, _t } from '../translations';
import { getDriverIcon } from '../utility/driverIcons';
import { currentThemeType } from '../plugins/themes';

View File

@@ -15,7 +15,7 @@ import { get } from 'svelte/store';
import AboutModal from '../modals/AboutModal.svelte';
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery, { newDiagram, newPerspective, newQueryDesign } from '../query/newQuery';
import newQuery, { newDiagram, newPerspective, newQueryDesign, newGraphQlQuery } from '../query/newQuery';
import saveTabFile from '../utility/saveTabFile';
import openNewTab from '../utility/openNewTab';
import getElectron from '../utility/getElectron';
@@ -258,6 +258,20 @@ if (isProApp()) {
});
}
if (isProApp()) {
registerCommand({
id: 'new.graphqlQuery',
category: __t('command.new', { defaultMessage: 'New' }),
icon: 'img graphql',
name: __t('command.new.graphqlQuery', { defaultMessage: 'GraphQL Query' }),
menuName: __t('command.new.newGraphqlQuery', { defaultMessage: 'New GraphQL Query' }),
onClick: () => newGraphQlQuery(),
testEnabled: () =>
getCurrentDatabase() &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('graphql'),
});
}
if (isProApp()) {
registerCommand({
id: 'new.application',
@@ -752,6 +766,29 @@ if (isProApp()) {
});
},
});
registerCommand({
id: 'graphql.chat',
category: __t('command.database', { defaultMessage: 'Database' }),
name: __t('command.graphql.chat', { defaultMessage: 'GraphQL chat' }),
toolbar: true,
icon: 'icon ai',
testEnabled: () =>
getCurrentDatabase() != null &&
findEngineDriver(getCurrentDatabase()?.connection, getExtensions())?.databaseEngineTypes?.includes('graphql') &&
hasPermission('dbops/chat'),
onClick: () => {
openNewTab({
title: 'GraphQL Chat',
icon: 'img ai',
tabComponent: 'GraphQlChatTab',
props: {
conid: getCurrentDatabase()?.connection?._id,
database: getCurrentDatabase()?.name,
},
});
},
});
}
if (hasPermission('settings/change')) {

View File

@@ -1,4 +1,5 @@
<script context="module" lang="ts">
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
registerCommand({
@@ -103,17 +104,37 @@
async function loadRowCount(props) {
const { conid, database } = props;
const response = await apiCall('database-connections/collection-data', {
conid,
database,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
});
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
return response.count;
try {
const response = await Promise.race([
apiCall('database-connections/collection-data', {
conid,
database,
commandTimeout: 3000,
options: {
pureName: props.pureName,
countDocuments: true,
condition: buildConditionForGrid(props),
},
}),
timeoutPromise,
]);
if (response && typeof response === 'object' && (response as any).errorMessage) {
return { errorMessage: (response as any).errorMessage };
}
if (response && typeof response === 'object' && typeof (response as any).count === 'number') {
return (response as any).count;
}
return { errorMessage: 'Error loading row count' };
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>
@@ -140,8 +161,6 @@
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
import { openImportExportTab } from '../utility/importExportTools';
import { __t } from '../translations';
export let conid;
export let display;
export let database;

View File

@@ -1,4 +1,5 @@
<script lang="ts" context="module">
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('DataGrid');
registerCommand({
@@ -79,9 +80,10 @@
import registerCommand from '../commands/registerCommand';
import { registerMenu } from '../utility/contextMenu';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import { __t, _t } from '../translations';
import { _t } from '../translations';
import { isProApp } from '../utility/proTools';
import CellDataWidget from '../widgets/CellDataWidget.svelte';
import { useSettings } from '../utility/metadataLoaders';
export let config;
export let setConfig;
@@ -123,6 +125,7 @@
let cellViewWidth;
const collapsedLeftColumnStore =
getContext('collapsedLeftColumnStore') || writable(getLocalStorage('dataGrid_collapsedLeftColumn', false));
const settings = useSettings();
$: isFormView = !!config?.isFormView;
$: isJsonView = !!config?.isJsonView;
@@ -285,7 +288,11 @@
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
if (value[0]?.isSelectedFullRow && !isFormView) {
if (
value[0]?.isSelectedFullRow &&
!isFormView &&
!$settings?.['dataGrid.disableCellDataViewAutoOpen']
) {
cellDataViewVisible = true;
}
}}

View File

@@ -1,4 +1,5 @@
<script lang="ts" context="module">
import { __t } from '../translations';
const getCurrentDataGrid = () => getActiveComponent('DataGridCore');
registerCommand({
@@ -25,6 +26,18 @@
onClick: () => getCurrentDataGrid().deepRefresh(),
});
registerCommand({
id: 'dataGrid.fetchAll',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.fetchAll', { defaultMessage: 'Fetch all rows' }),
toolbarName: __t('command.datagrid.fetchAll.toolbar', { defaultMessage: 'Fetch all' }),
icon: 'icon download',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentDataGrid()?.canFetchAll(),
onClick: () => getCurrentDataGrid().fetchAll(),
});
registerCommand({
id: 'dataGrid.revertRowChanges',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
@@ -431,6 +444,7 @@
import CollapseButton from './CollapseButton.svelte';
import GenerateSqlFromDataModal from '../modals/GenerateSqlFromDataModal.svelte';
import { showModal } from '../modals/modalTools';
import FetchAllConfirmModal from '../modals/FetchAllConfirmModal.svelte';
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
import { findCommand } from '../commands/runCommand';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
@@ -446,13 +460,14 @@
import { openJsonLinesData } from '../utility/openJsonLinesData';
import contextMenuActivator from '../utility/contextMenuActivator';
import InputTextModal from '../modals/InputTextModal.svelte';
import { __t, _t, _tval } from '../translations';
import { _t, _tval } from '../translations';
import { isProApp } from '../utility/proTools';
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
import hasPermission from '../utility/hasPermission';
import macros from '../macro/macros';
export let onLoadNextData = undefined;
export let onFetchAllRows = undefined;
export let grider = undefined;
export let display: GridDisplay = undefined;
export let conid = undefined;
@@ -460,6 +475,8 @@
export let frameSelection = undefined;
export let isLoading = false;
export let allRowCount = undefined;
export let allRowCountError = undefined;
export let onReloadRowCount = undefined;
export let onReferenceSourceChanged = undefined;
export let onPublishedCellsChanged = undefined;
export let onReferenceClick = undefined;
@@ -470,6 +487,9 @@
export let errorMessage = undefined;
export let pureName = undefined;
export let schemaName = undefined;
export let isFetchingAll = false;
export let isFetchingFromDb = false;
export let fetchAllLoadedCount = 0;
export let allowDefineVirtualReferences = false;
export let formatterFunction;
export let passAllRows = null;
@@ -553,13 +573,22 @@
let previousMultiColumnFilter = undefined;
let selectedRows = [];
function resetVerticalScroll() {
firstVisibleRowScrollIndex = 0;
if (domVerticalScroll) {
domVerticalScroll.scroll(0);
}
}
$: if (display?.config) {
const currentFilters = JSON.stringify(display.config.filters);
const currentMultiColumnFilter = display.config.multiColumnFilter;
if (
const filtersChanged =
previousFilters !== '' &&
(previousFilters !== currentFilters || previousMultiColumnFilter !== currentMultiColumnFilter)
) {
(previousFilters !== currentFilters || previousMultiColumnFilter !== currentMultiColumnFilter);
if (filtersChanged) {
resetVerticalScroll();
const pkColumns = display?.baseTable?.primaryKey?.columns?.map(col => col.columnName) || [];
const usePK = pkColumns.length > 0;
@@ -635,6 +664,21 @@
return canRefresh() && !!conid && !!database;
}
export function canFetchAll() {
return !!onFetchAllRows && !isLoadedAll && !isFetchingAll && !isLoading;
}
export function fetchAll() {
if (!canFetchAll()) return;
const settings = $settingsValue || {};
if (settings['dataGrid.skipFetchAllConfirm']) {
onFetchAllRows();
} else {
showModal(FetchAllConfirmModal, { onConfirm: () => onFetchAllRows() });
}
}
export async function deepRefresh() {
callUnsubscribeDbRefresh();
await apiCall('database-connections/sync-model', { conid, database });
@@ -1965,6 +2009,7 @@
registerMenu(
{ command: 'dataGrid.refresh' },
{ command: 'dataGrid.fetchAll', hideDisabled: true },
{ placeTag: 'copy' },
{
text: _t('datagrid.copyAdvanced', { defaultMessage: 'Copy advanced' }),
@@ -2390,14 +2435,38 @@
<div class="row-count-label">
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {allRowCount.toLocaleString()}
</div>
{:else if allRowCountError && multipleGridsOnTab}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="row-count-label row-count-error" title={allRowCountError} on:click={onReloadRowCount}>
{_t('datagrid.rows', { defaultMessage: 'Rows' })}: {_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}
</div>
{/if}
{#if isLoading}
<LoadingInfo wrapper message="Loading data" />
{/if}
{#if isFetchingAll}
<LoadingInfo
wrapper
message={isFetchingFromDb
? _t('datagrid.fetchAll.progressDb', { defaultMessage: 'Fetching data from database...' })
: _t('datagrid.fetchAll.progress', {
defaultMessage: 'Fetching all rows... {count} loaded',
values: { count: fetchAllLoadedCount.toLocaleString() },
})}
/>
{/if}
{#if !tabControlHiddenTab && !multipleGridsOnTab && allRowCount != null}
<StatusBarTabItem text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${allRowCount.toLocaleString()}`} />
{:else if !tabControlHiddenTab && !multipleGridsOnTab && allRowCountError}
<StatusBarTabItem
text={`${_t('datagrid.rows', { defaultMessage: 'Rows' })}: ${_t('datagrid.rowCountMany', { defaultMessage: 'Many' })}`}
title={allRowCountError}
clickable
onClick={onReloadRowCount}
/>
{/if}
</div>
{/if}
@@ -2462,6 +2531,15 @@
opacity: 1;
}
.row-count-error {
cursor: pointer;
color: var(--theme-font-3);
}
.row-count-error:hover {
text-decoration: underline;
}
.selection-menu {
position: absolute;
background-color: var(--theme-datagrid-corner-label-background);

View File

@@ -1,219 +1,217 @@
<script context="module" lang="ts">
const getCurrentEditor = () => getActiveComponent('JslDataGridCore');
registerCommand({
id: 'jslTableGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
async function loadDataPage(props, offset, limit) {
const { jslid, display, formatterFunction } = props;
const response = await apiCall('jsldata/get-rows', {
jslid,
offset,
limit,
formatterFunction,
filters: display ? display.compileJslFilters() : null,
sort: display.config.sort,
});
return response;
}
function dataPageAvailable(props) {
return true;
}
async function loadRowCount(props) {
const { jslid } = props;
const response = await apiCall('jsldata/get-stats', { jslid });
return response.rowCount;
}
export let formatterPlugin;
export let formatterFunction;
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import { apiCall, apiOff, apiOn } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import useEffect from '../utility/useEffect';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
import { __t } from '../translations';
export let jslid;
export let display;
export let formatterFunction;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
export const activator = createActivator('JslDataGridCore', false);
export let setLoadedRows;
let publishedCells = [];
let loadedRows = [];
let domGrid;
let changeIndex = 0;
let rowCountLoaded = null;
const throttleLoadNext = _.throttle(() => domGrid?.resetLoadedAll(), 500);
const handleJslDataStats = stats => {
if (stats.changeIndex < changeIndex) return;
changeIndex = stats.changeIndex;
rowCountLoaded = stats.rowCount;
throttleLoadNext();
};
$: effect = useEffect(() => onJslId(jslid));
function onJslId(jslidVal) {
if (jslidVal) {
apiOn(`jsldata-stats-${jslidVal}`, handleJslDataStats);
return () => {
apiOff(`jsldata-stats-${jslidVal}`, handleJslDataStats);
};
} else {
return () => {};
}
}
$: $effect;
let grider;
$: {
if (macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells,
true
);
}
}
$: {
if (!macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
undefined,
undefined,
undefined,
true
);
}
}
// $: grider = new RowsArrayGrider(loadedRows);
export function exportGrid() {
const initialValues = {} as any;
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
initialValues.sourceStorageType = 'archive';
initialValues.sourceArchiveFolder = archiveMatch[1];
initialValues.sourceList = [archiveMatch[2]];
initialValues[`columns_${archiveMatch[2]}`] = display.getExportColumnMap();
} else {
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['query-data'];
initialValues[`columns_query-data`] = display.getExportColumnMap();
}
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
const quickExportHandler = fmt => async () => {
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
exportQuickExportFile(
archiveMatch[2],
{
functionName: 'archiveReader',
props: {
folderName: archiveMatch[1],
fileName: archiveMatch[2],
},
},
fmt,
display.getExportColumnMap()
);
} else {
exportQuickExportFile(
'Query',
{
functionName: 'jslDataReader',
props: {
jslid,
},
},
fmt,
display.getExportColumnMap()
);
}
};
registerQuickExportHandler(quickExportHandler);
registerMenu(() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'jslTableGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
setLoadedRows?.(rows);
}
</script>
<LoadingDataGridCore
bind:this={domGrid}
{...$$props}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
{grider}
{rowCountLoaded}
/>
const getCurrentEditor = () => getActiveComponent('JslDataGridCore');
registerCommand({
id: 'jslTableGrid.export',
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().exportGrid(),
});
async function loadDataPage(props, offset, limit) {
const { jslid, display, formatterFunction } = props;
const response = await apiCall('jsldata/get-rows', {
jslid,
offset,
limit,
formatterFunction,
filters: display ? display.compileJslFilters() : null,
sort: display.config.sort,
});
return response;
}
function dataPageAvailable(props) {
return true;
}
async function loadRowCount(props) {
const { jslid } = props;
const response = await apiCall('jsldata/get-stats', { jslid });
return response.rowCount;
}
export let formatterPlugin;
export let formatterFunction;
</script>
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import { apiCall, apiOff, apiOn } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import useEffect from '../utility/useEffect';
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
export let jslid;
export let display;
export let formatterFunction;
export let changeSetState;
export let dispatchChangeSet;
export let macroPreview;
export let macroValues;
export let onPublishedCellsChanged;
export const activator = createActivator('JslDataGridCore', false);
export let setLoadedRows;
let publishedCells = [];
let loadedRows = [];
let domGrid;
let changeIndex = 0;
let rowCountLoaded = null;
const throttleLoadNext = _.throttle(() => domGrid?.resetLoadedAll(), 500);
const handleJslDataStats = stats => {
if (stats.changeIndex < changeIndex) return;
changeIndex = stats.changeIndex;
rowCountLoaded = stats.rowCount;
throttleLoadNext();
};
$: effect = useEffect(() => onJslId(jslid));
function onJslId(jslidVal) {
if (jslidVal) {
apiOn(`jsldata-stats-${jslidVal}`, handleJslDataStats);
return () => {
apiOff(`jsldata-stats-${jslidVal}`, handleJslDataStats);
};
} else {
return () => {};
}
}
$: $effect;
let grider;
$: {
if (macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
macroPreview,
macroValues,
publishedCells,
true
);
}
}
$: {
if (!macroPreview) {
grider = new ChangeSetGrider(
loadedRows,
changeSetState,
dispatchChangeSet,
display,
undefined,
undefined,
undefined,
true
);
}
}
// $: grider = new RowsArrayGrider(loadedRows);
export function exportGrid() {
const initialValues = {} as any;
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
initialValues.sourceStorageType = 'archive';
initialValues.sourceArchiveFolder = archiveMatch[1];
initialValues.sourceList = [archiveMatch[2]];
initialValues[`columns_${archiveMatch[2]}`] = display.getExportColumnMap();
} else {
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['query-data'];
initialValues[`columns_query-data`] = display.getExportColumnMap();
}
openImportExportTab(initialValues);
// showModal(ImportExportModal, { initialValues });
}
const quickExportHandler = fmt => async () => {
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
exportQuickExportFile(
archiveMatch[2],
{
functionName: 'archiveReader',
props: {
folderName: archiveMatch[1],
fileName: archiveMatch[2],
},
},
fmt,
display.getExportColumnMap()
);
} else {
exportQuickExportFile(
'Query',
{
functionName: 'jslDataReader',
props: {
jslid,
},
},
fmt,
display.getExportColumnMap()
);
}
};
registerQuickExportHandler(quickExportHandler);
registerMenu(() =>
createQuickExportMenu(
quickExportHandler,
{
command: 'jslTableGrid.export',
},
{ tag: 'export' }
)
);
function handleSetLoadedRows(rows) {
loadedRows = rows;
setLoadedRows?.(rows);
}
</script>
<LoadingDataGridCore
bind:this={domGrid}
{...$$props}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
{grider}
{rowCountLoaded}

View File

@@ -1,14 +1,18 @@
<script lang="ts">
import { getIntSettingsValue } from '../settings/settingsTools';
import { onDestroy } from 'svelte';
import createRef from '../utility/createRef';
import { useSettings } from '../utility/metadataLoaders';
import { fetchAll, type FetchAllHandle } from '../utility/fetchAll';
import { apiCall } from '../utility/api';
import DataGridCore from './DataGridCore.svelte';
export let loadDataPage;
export let dataPageAvailable;
export let loadRowCount;
export let startFetchAll = null;
export let grider;
export let display;
export let masterLoadedTime = undefined;
@@ -25,9 +29,16 @@
let isLoadedAll = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
let domGrid;
let isFetchingAll = false;
let isFetchingFromDb = false;
let fetchAllLoadedCount = 0;
let fetchAllHandle: FetchAllHandle | null = null;
let readerJslid: string | null = null;
const loadNextDataRef = createRef(false);
const loadedTimeRef = createRef(null);
@@ -37,8 +48,14 @@
}
const handleLoadRowCount = async () => {
const rowCount = await loadRowCount($$props);
allRowCount = rowCount;
const result = await loadRowCount($$props);
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
};
async function loadNextData() {
@@ -89,11 +106,161 @@
// console.log('LOADED', nextRows, loadedRows);
}
async function fetchAllRows() {
if (isFetchingAll || isLoadedAll) return;
const jslid = ($$props as any).jslid;
if (jslid) {
// Already have a JSONL file (e.g. query tab) — read directly
fetchAllViaJslid(jslid);
} else if (startFetchAll) {
// SQL/table grid: execute full query → stream to JSONL → read from it
fetchAllViaReader();
} else {
fetchAllRowsLegacy();
}
}
function stopReader() {
if (readerJslid) {
apiCall('sessions/stop-loading-reader', { jslid: readerJslid });
readerJslid = null;
}
}
async function fetchAllViaReader() {
isFetchingAll = true;
isFetchingFromDb = true;
fetchAllLoadedCount = loadedRows.length;
errorMessage = null;
// Token guards against a reload/destroy that happens while we await startFetchAll.
// loadedTimeRef is already updated by reload(), so we reuse it as our token.
const token = loadedTime;
let jslid;
try {
jslid = await startFetchAll($$props);
} catch (err) {
if (loadedTime !== token) return; // reload() already reset state
errorMessage = err?.message ?? 'Failed to start data reader';
isFetchingAll = false;
isFetchingFromDb = false;
return;
}
// If reload()/onDestroy ran while we were awaiting, discard the result and
// immediately stop the reader that was just started on the server.
if (loadedTime !== token) {
if (jslid) apiCall('sessions/stop-loading-reader', { jslid });
return;
}
if (!jslid) {
errorMessage = 'Failed to start data reader';
isFetchingAll = false;
isFetchingFromDb = false;
return;
}
readerJslid = jslid;
fetchAllViaJslid(jslid);
}
function fetchAllViaJslid(jslid: string) {
if (!isFetchingAll) {
isFetchingAll = true;
fetchAllLoadedCount = loadedRows.length;
errorMessage = null;
}
const pageSize = getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000);
const buffer: any[] = [];
const jslLoadDataPage = async (offset: number, limit: number) => {
return apiCall('jsldata/get-rows', { jslid, offset, limit });
};
fetchAllHandle = fetchAll(
jslid,
jslLoadDataPage,
{
onPage(rows) {
if (rows.length > 0) isFetchingFromDb = false;
const processed = preprocessLoadedRow ? rows.map(preprocessLoadedRow) : rows;
buffer.push(...processed);
fetchAllLoadedCount = buffer.length;
},
onFinished() {
loadedRows = buffer;
isLoadedAll = true;
isFetchingAll = false;
isFetchingFromDb = false;
fetchAllHandle = null;
readerJslid = null;
if (allRowCount == null && !isRawMode) handleLoadRowCount();
},
onError(msg) {
errorMessage = msg;
isFetchingAll = false;
isFetchingFromDb = false;
fetchAllHandle = null;
stopReader();
},
},
pageSize
);
}
async function fetchAllRowsLegacy() {
isFetchingAll = true;
fetchAllLoadedCount = loadedRows.length;
errorMessage = null;
const pageSize = getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000);
const fetchStart = new Date().getTime();
loadedTimeRef.set(fetchStart);
// Accumulate into a local buffer to avoid O(n²) full-array copies each iteration.
const buffer = [...loadedRows];
try {
while (!isLoadedAll) {
const nextRows = await loadDataPage($$props, buffer.length, pageSize);
if (loadedTimeRef.get() !== fetchStart) {
// a reload was triggered; abort without overwriting loadedRows with stale data
return;
}
if (nextRows.errorMessage) {
errorMessage = nextRows.errorMessage;
break;
}
if (nextRows.length === 0) {
isLoadedAll = true;
break;
}
const processed = preprocessLoadedRow ? nextRows.map(preprocessLoadedRow) : nextRows;
buffer.push(...processed);
fetchAllLoadedCount = buffer.length;
}
// Single assignment triggers Svelte reactivity once for all accumulated rows.
loadedRows = buffer;
if (allRowCount == null && !isRawMode) handleLoadRowCount();
} finally {
isFetchingAll = false;
}
}
// $: griderProps = { ...$$props, sourceRows: loadProps.loadedRows };
// $: grider = griderFactory(griderProps);
function handleLoadNextData() {
if (!isLoadedAll && !errorMessage && (!grider.disableLoadNextPage || loadedRows.length == 0)) {
if (!isLoadedAll && !errorMessage && !isFetchingAll && (!grider.disableLoadNextPage || loadedRows.length == 0)) {
if (dataPageAvailable($$props)) {
// If not, callbacks to load missing metadata are dispatched
loadNextData();
@@ -102,13 +269,23 @@
}
function reload() {
if (fetchAllHandle) {
fetchAllHandle.cancel();
fetchAllHandle = null;
}
stopReader();
isFetchingFromDb = false;
allRowCount = null;
allRowCountError = null;
isLoading = false;
isFetchingAll = false;
fetchAllLoadedCount = 0;
loadedRows = [];
isLoadedAll = false;
loadedTime = new Date().getTime();
errorMessage = null;
loadNextDataRef.set(false);
loadedTimeRef.set(null);
// loadNextDataToken = 0;
}
@@ -122,6 +299,13 @@
}
}
onDestroy(() => {
if (fetchAllHandle) {
fetchAllHandle.cancel();
}
stopReader();
});
$: if (setLoadedRows) setLoadedRows(loadedRows);
</script>
@@ -129,9 +313,15 @@
{...$$props}
bind:this={domGrid}
onLoadNextData={handleLoadNextData}
onFetchAllRows={fetchAllRows}
{errorMessage}
{isLoading}
{isFetchingAll}
{isFetchingFromDb}
{fetchAllLoadedCount}
allRowCount={rowCountLoaded || allRowCount}
{allRowCountError}
onReloadRowCount={handleLoadRowCount}
{isLoadedAll}
{loadedTime}
{grider}

View File

@@ -1,11 +1,14 @@
<script context="module" lang="ts">
import { __t, _t } from '../translations'
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import hasPermission from '../utility/hasPermission';
import { __t, _t } from '../translations';
const getCurrentEditor = () => getActiveComponent('SqlDataGridCore');
registerCommand({
id: 'sqlDataGrid.openQuery',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.openQuery', { defaultMessage : 'Open query' }),
name: __t('command.openQuery', { defaultMessage: 'Open query' }),
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
onClick: () => getCurrentEditor().openQuery(),
});
@@ -13,7 +16,7 @@
registerCommand({
id: 'sqlDataGrid.export',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('common.export', { defaultMessage : 'Export' }),
name: __t('common.export', { defaultMessage: 'Export' }),
icon: 'icon export',
keyText: 'CtrlOrCommand+E',
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
@@ -24,8 +27,6 @@
<script lang="ts">
import _ from 'lodash';
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import registerCommand from '../commands/registerCommand';
import {
extractShellConnection,
extractShellConnectionHostable,
@@ -34,7 +35,7 @@
import { apiCall } from '../utility/api';
import { registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import createQuickExportMenu from '../utility/createQuickExportMenu';
import { exportQuickExportFile } from '../utility/exportFileTools';
import { getConnectionInfo } from '../utility/metadataLoaders';
@@ -42,7 +43,6 @@
import ChangeSetGrider from './ChangeSetGrider';
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import hasPermission from '../utility/hasPermission';
import { openImportExportTab } from '../utility/importExportTools';
import { getIntSettingsValue } from '../settings/settingsTools';
import OverlayDiffGrider from './OverlayDiffGrider';
@@ -211,13 +211,40 @@
const select = display.getCountQuery();
const response = await apiCall('database-connections/sql-select', {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const response = await Promise.race([
apiCall('database-connections/sql-select', {
conid,
database,
select,
commandTimeout: 3000,
}),
timeoutPromise,
]);
if (response.errorMessage) return { errorMessage: response.errorMessage };
return parseInt(response.rows[0].count);
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
async function startFetchAll(props) {
const { display, conid, database } = props;
const sql = display.getExportQuery();
if (!sql) return null;
const resp = await apiCall('sessions/execute-reader', {
conid,
database,
select,
sql,
});
return parseInt(response.rows[0].count);
if (!resp || resp.errorMessage) return null;
return resp.jslid;
}
</script>
@@ -226,6 +253,7 @@
{loadDataPage}
{dataPageAvailable}
{loadRowCount}
{startFetchAll}
setLoadedRows={handleSetLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;

View File

@@ -1,4 +1,7 @@
<script lang="ts" context="module">
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('Designer');
registerCommand({
@@ -16,8 +19,8 @@
registerCommand({
id: 'diagram.export',
category: __t('command.designer', { defaultMessage: 'Designer' }),
toolbarName: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram' }),
name: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram' }),
toolbarName: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram as HTML' }),
name: __t('command.designer.exportDiagram', { defaultMessage: 'Export diagram as HTML' }),
icon: 'icon report',
toolbar: true,
isRelatedToTab: true,
@@ -25,6 +28,17 @@
testEnabled: () => getCurrentEditor()?.canExport(),
});
registerCommand({
id: 'diagram.exportPng',
category: __t('command.designer', { defaultMessage: 'Designer' }),
name: __t('command.designer.exportDiagramPng', { defaultMessage: 'Export diagram as PNG' }),
icon: 'icon report',
toolbar: true,
isRelatedToTab: true,
onClick: () => getCurrentEditor().exportDiagramPng(),
testEnabled: () => getCurrentEditor()?.canExport(),
});
registerCommand({
id: 'diagram.deleteSelectedTables',
category: __t('command.designer', { defaultMessage: 'Designer' }),
@@ -49,11 +63,11 @@
import { tick } from 'svelte';
import contextMenu from '../utility/contextMenu';
import stableStringify from 'json-stable-stringify';
import registerCommand from '../commands/registerCommand';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import { GraphDefinition, GraphLayout } from './GraphLayout';
import { saveFileToDisk } from '../utility/exportFileTools';
import { apiCall } from '../utility/api';
import domtoimage from 'dom-to-image';
import moveDrag from '../utility/moveDrag';
import { rectanglesHaveIntersection } from './designerMath';
import { showModal } from '../modals/modalTools';
@@ -67,7 +81,7 @@
import { isProApp } from '../utility/proTools';
import dragScroll from '../utility/dragScroll';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import { __t, _t } from '../translations';
import { _t } from '../translations';
export let value;
export let onChange;
@@ -828,6 +842,34 @@
});
}
export async function exportDiagramPng() {
const rects = _.values(domTables).map((x: any) => x.getRect());
const contentWidth = rects.length > 0 ? _.max(rects.map((x: any) => x.right)) + 50 : canvasWidth;
const contentHeight = rects.length > 0 ? _.max(rects.map((x: any) => x.bottom)) + 50 : canvasHeight;
const scale = 2;
const backgroundColor = getComputedStyle(domWrapper).getPropertyValue('--theme-designer-background');
const pngBase64 = await domtoimage.toPng(domCanvas, {
width: contentWidth * scale,
height: contentHeight * scale,
style: {
transform: `scale(${scale})`,
transformOrigin: 'top left',
width: contentWidth + 'px',
height: contentHeight + 'px',
backgroundColor,
},
});
saveFileToDisk(
async filePath => {
await apiCall('files/export-diagram-png', {
filePath,
pngBase64,
});
},
{ formatLabel: 'PNG image', formatExtension: 'png', defaultFileName: 'diagram.png' }
);
}
const changeStyleFunc = (name, value) => () => {
callChange(current => {
return {

View File

@@ -1,170 +1,169 @@
<script context="module">
export function computeSplitterSize(initialValue, clientSize, customRatio, initialSizeRight) {
if (customRatio != null) {
return clientSize * customRatio;
}
if (initialSizeRight) {
return clientSize - initialSizeRight;
}
if (_.isString(initialValue) && initialValue.startsWith('~') && initialValue.endsWith('px'))
return clientSize - parseInt(initialValue.slice(1, -2));
if (_.isString(initialValue) && initialValue.endsWith('px')) return parseInt(initialValue.slice(0, -2));
if (_.isString(initialValue) && initialValue.endsWith('%'))
return (clientSize * parseFloat(initialValue.slice(0, -1))) / 100;
return clientSize / 2;
}
</script>
<script>
import _ from 'lodash';
import FontIcon from '../icons/FontIcon.svelte';
import splitterDrag from '../utility/splitterDrag';
export let isSplitter = true;
export let initialValue = undefined;
export let initialSizeRight = undefined;
export let hideFirst = false;
export let allowCollapseChild1 = false;
export let allowCollapseChild2 = false;
let collapsed1 = false;
let collapsed2 = false;
export let size = 0;
export let onChangeSize = null;
let clientWidth;
let customRatio = null;
$: size = computeSplitterSize(initialValue, clientWidth, customRatio, initialSizeRight);
$: if (onChangeSize) onChangeSize(size, clientWidth - size);
</script>
<div class="container" bind:clientWidth>
{#if !hideFirst}
<div
class="child1"
style={isSplitter
? collapsed1
? 'display:none'
: collapsed2
? 'flex:1'
: `width:${size}px; min-width:${size}px; max-width:${size}px}`
: `flex:1`}
>
<slot name="1" />
</div>
{/if}
{#if isSplitter}
{#if !hideFirst}
<div
class="horizontal-split-handle"
style={collapsed1 || collapsed2 ? 'display:none' : ''}
use:splitterDrag={'clientX'}
on:resizeSplitter={e => {
size += e.detail;
if (clientWidth > 0) customRatio = size / clientWidth;
}}
/>
{/if}
<div
class={collapsed1 ? 'child1' : 'child2'}
style={collapsed2 ? 'display:none' : collapsed1 ? 'flex:1' : 'child2'}
>
<slot name="2" />
</div>
{/if}
{#if allowCollapseChild1 && !collapsed2 && isSplitter}
{#if collapsed1}
<div
class="collapse"
style={`left: 0px`}
on:click={() => {
collapsed1 = false;
}}
>
<FontIcon icon="icon chevron-double-right" />
</div>
{:else}
<div
class="collapse"
style={`left: ${size - 16}px`}
on:click={() => {
collapsed1 = true;
}}
>
<FontIcon icon="icon chevron-double-left" />
</div>
{/if}
{/if}
{#if allowCollapseChild2 && !collapsed1 && isSplitter}
{#if collapsed2}
<div
class="collapse"
style={`right: 0px`}
on:click={() => {
collapsed2 = false;
}}
>
<FontIcon icon="icon chevron-double-left" />
</div>
{:else}
<div
class="collapse"
style={`left: ${size}px`}
on:click={() => {
collapsed2 = true;
}}
>
<FontIcon icon="icon chevron-double-right" />
</div>
{/if}
{/if}
</div>
<style>
.container {
flex: 1;
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.child1 {
display: flex;
position: relative;
overflow: hidden;
}
.child2 {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.collapse {
position: absolute;
bottom: 16px;
height: 40px;
width: 16px;
background: var(--theme-splitter-button-background);
display: flex;
flex-direction: column;
justify-content: center;
z-index: 100;
}
.collapse:hover {
color: var(--theme-splitter-button-foreground);
background: var(--theme-splitter-button-background-active);
cursor: pointer;
}
</style>
export function computeSplitterSize(initialValue, clientSize, customRatio, initialSizeRight) {
if (customRatio != null) {
return clientSize * customRatio;
}
if (initialSizeRight) {
return clientSize - initialSizeRight;
}
if (_.isString(initialValue) && initialValue.startsWith('~') && initialValue.endsWith('px'))
return clientSize - parseInt(initialValue.slice(1, -2));
if (_.isString(initialValue) && initialValue.endsWith('px')) return parseInt(initialValue.slice(0, -2));
if (_.isString(initialValue) && initialValue.endsWith('%'))
return (clientSize * parseFloat(initialValue.slice(0, -1))) / 100;
return clientSize / 2;
}
</script>
<script>
import FontIcon from '../icons/FontIcon.svelte';
import splitterDrag from '../utility/splitterDrag';
export let isSplitter = true;
export let initialValue = undefined;
export let initialSizeRight = undefined;
export let hideFirst = false;
export let allowCollapseChild1 = false;
export let allowCollapseChild2 = false;
let collapsed1 = false;
let collapsed2 = false;
export let size = 0;
export let onChangeSize = null;
let clientWidth;
let customRatio = null;
$: size = computeSplitterSize(initialValue, clientWidth, customRatio, initialSizeRight);
$: if (onChangeSize) onChangeSize(size, clientWidth - size);
</script>
<div class="container" bind:clientWidth>
{#if !hideFirst}
<div
class="child1"
style={isSplitter
? collapsed1
? 'display:none'
: collapsed2
? 'flex:1'
: `width:${size}px; min-width:${size}px; max-width:${size}px}`
: `flex:1`}
>
<slot name="1" />
</div>
{/if}
{#if isSplitter}
{#if !hideFirst}
<div
class="horizontal-split-handle"
style={collapsed1 || collapsed2 ? 'display:none' : ''}
use:splitterDrag={'clientX'}
on:resizeSplitter={e => {
size += e.detail;
if (clientWidth > 0) customRatio = size / clientWidth;
}}
/>
{/if}
<div
class={collapsed1 ? 'child1' : 'child2'}
style={collapsed2 ? 'display:none' : collapsed1 ? 'flex:1' : 'child2'}
>
<slot name="2" />
</div>
{/if}
{#if allowCollapseChild1 && !collapsed2 && isSplitter}
{#if collapsed1}
<div
class="collapse"
style={`left: 0px`}
on:click={() => {
collapsed1 = false;
}}
>
<FontIcon icon="icon chevron-double-right" />
</div>
{:else}
<div
class="collapse"
style={`left: ${size - 16}px`}
on:click={() => {
collapsed1 = true;
}}
>
<FontIcon icon="icon chevron-double-left" />
</div>
{/if}
{/if}
{#if allowCollapseChild2 && !collapsed1 && isSplitter}
{#if collapsed2}
<div
class="collapse"
style={`right: 0px`}
on:click={() => {
collapsed2 = false;
}}
>
<FontIcon icon="icon chevron-double-left" />
</div>
{:else}
<div
class="collapse"
style={`left: ${size}px`}
on:click={() => {
collapsed2 = true;
}}
>
<FontIcon icon="icon chevron-double-right" />
</div>
{/if}
{/if}
</div>
<style>
.container {
flex: 1;
display: flex;
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
}
.child1 {
display: flex;
position: relative;
overflow: hidden;
}
.child2 {
flex: 1;
display: flex;
position: relative;
overflow: hidden;
}
.collapse {
position: absolute;
bottom: 16px;
height: 40px;
width: 16px;
background: var(--theme-splitter-button-background);
display: flex;
flex-direction: column;
justify-content: center;
z-index: 100;
}
.collapse:hover {
color: var(--theme-splitter-button-foreground);
background: var(--theme-splitter-button-background-active);
cursor: pointer;
}

View File

@@ -1,4 +1,7 @@
<script lang="ts" context="module">
import _ from 'lodash';
import { isWktGeometry, stringifyCellValue } from 'dbgate-tools';
import wellknown from 'wellknown';
const LAT_PRIORITY_PATTERNS = [
/^lat$/i,
/^latitude$/i,
@@ -141,10 +144,7 @@
</script>
<script lang="ts">
import _ from 'lodash';
import 'leaflet/dist/leaflet.css';
import wellknown from 'wellknown';
import { isWktGeometry, stringifyCellValue } from 'dbgate-tools';
import MapView from './MapView.svelte';
export let selection;

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import SimpleFilesInput, { ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
import SimpleFilesInput, { type ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
import { parseFileAsString } from '../utility/parseFileAsString';
import { getFormContext } from './FormProviderCore.svelte';
import { createEventDispatcher } from 'svelte';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import SimpleFilesInput, { ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
import SimpleFilesInput, { type ProcessedFile } from '../impexp/SimpleFilesInput.svelte';
import { FileParseResult, parseFileAsJson } from '../utility/parseFileAsJson';
import { getFormContext } from './FormProviderCore.svelte';
import { createEventDispatcher } from 'svelte';

View File

@@ -1,4 +1,8 @@
<script lang="ts" context="module">
import { showModal } from '../modals/modalTools';
import EditJsonModal from '../modals/EditJsonModal.svelte';
import stableStringify from 'json-stable-stringify';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
export function editJsonRowDocument(grider, rowIndex) {
const rowData = grider.getRowData(rowIndex);
showModal(EditJsonModal, {
@@ -21,13 +25,8 @@
<script lang="ts">
import JSONTree from '../jsontree/JSONTree.svelte';
import EditJsonModal from '../modals/EditJsonModal.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { showModal } from '../modals/modalTools';
import { copyTextToClipboard } from '../utility/clipboard';
import { getContextMenu, registerMenu } from '../utility/contextMenu';
import stableStringify from 'json-stable-stringify';
export let rowIndex;
export let grider;

View File

@@ -1,153 +1,151 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('CollectionJsonView');
registerCommand({
id: 'collectionJsonView.expandAll',
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
name: __t('command.collectionData.expandAll', { defaultMessage: 'Expand all' }),
isRelatedToTab: true,
icon: 'icon expand-all',
onClick: () => getCurrentEditor().handleExpandAll(),
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isExpandedAll(),
});
registerCommand({
id: 'collectionJsonView.collapseAll',
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
name: __t('command.collectionData.collapseAll', { defaultMessage: 'Collapse all' }),
isRelatedToTab: true,
icon: 'icon collapse-all',
onClick: () => getCurrentEditor().handleCollapseAll(),
testEnabled: () => getCurrentEditor() != null && getCurrentEditor()?.isExpandedAll(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { onMount } from 'svelte';
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import ChangeSetGrider from '../datagrid/ChangeSetGrider';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import { loadCollectionDataPage } from '../datagrid/CollectionDataGridCore.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import Pager from '../elements/Pager.svelte';
import contextMenu, { getContextMenu, registerMenu } from '../utility/contextMenu';
import CollectionJsonRow from './CollectionJsonRow.svelte';
import { getIntSettingsValue } from '../settings/settingsTools';
import invalidateCommands from '../commands/invalidateCommands';
import { __t } from '../translations';
export let conid;
export let database;
export let cache;
export let display;
export let setConfig;
export let changeSetState;
export let dispatchChangeSet;
export let setLoadedRows;
export const activator = createActivator('CollectionJsonView', true);
let isLoading = false;
let loadedTime = null;
let expandAll = false;
let expandKey = 0;
let loadedRows = [];
let skip = 0;
let limit = getIntSettingsValue('dataGrid.collectionPageSize', 50, 5, 1000);
async function loadData() {
isLoading = true;
// @ts-ignore
loadedRows = await loadCollectionDataPage($$props, parseInt(skip) || 0, parseInt(limit) || 50);
if (setLoadedRows) setLoadedRows(loadedRows);
isLoading = false;
loadedTime = new Date().getTime();
}
$: if (cache?.refreshTime > loadedTime) {
loadData();
}
onMount(() => {
loadData();
});
registerMenu({ placeTag: 'switch' });
const menu = getContextMenu();
$: grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
// $: console.log('GRIDER', grider);
export function handleExpandAll() {
expandAll = true;
expandKey += 1;
invalidateCommands();
}
export function handleCollapseAll() {
expandAll = false;
expandKey += 1;
invalidateCommands();
}
export function isExpandedAll() {
return expandAll;
}
</script>
<div class="wrapper" use:contextMenu={menu}>
<div class="toolbar">
<Pager bind:skip bind:limit on:load={() => display.reload()} />
</div>
<div class="json">
{#key expandKey}
{#each _.range(0, grider.rowCount) as rowIndex}
<CollectionJsonRow {grider} {rowIndex} {expandAll} />
{/each}
{/key}
</div>
</div>
{#if isLoading}
<LoadingInfo wrapper message="Loading data" />
{/if}
<style>
.toolbar {
background: var(--theme-toolstrip-background);
display: flex;
border-bottom: var(--theme-toolstrip-border);
border-top: var(--theme-toolstrip-border);
margin-bottom: 3px;
}
.toolbar :global(input){
margin-top: 3px;
margin-bottom: 3px;
height: 26px;
}
.json {
overflow: auto;
flex: 1;
/* position: relative; */
}
.wrapper {
display: flex;
flex-direction: column;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
}
</style>
const getCurrentEditor = () => getActiveComponent('CollectionJsonView');
registerCommand({
id: 'collectionJsonView.expandAll',
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
name: __t('command.collectionData.expandAll', { defaultMessage: 'Expand all' }),
isRelatedToTab: true,
icon: 'icon expand-all',
onClick: () => getCurrentEditor().handleExpandAll(),
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isExpandedAll(),
});
registerCommand({
id: 'collectionJsonView.collapseAll',
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
name: __t('command.collectionData.collapseAll', { defaultMessage: 'Collapse all' }),
isRelatedToTab: true,
icon: 'icon collapse-all',
onClick: () => getCurrentEditor().handleCollapseAll(),
testEnabled: () => getCurrentEditor() != null && getCurrentEditor()?.isExpandedAll(),
});
</script>
<script lang="ts">
import _ from 'lodash';
import { onMount } from 'svelte';
import ChangeSetGrider from '../datagrid/ChangeSetGrider';
import createActivator from '../utility/createActivator';
import { loadCollectionDataPage } from '../datagrid/CollectionDataGridCore.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import Pager from '../elements/Pager.svelte';
import contextMenu, { getContextMenu, registerMenu } from '../utility/contextMenu';
import CollectionJsonRow from './CollectionJsonRow.svelte';
import { getIntSettingsValue } from '../settings/settingsTools';
import invalidateCommands from '../commands/invalidateCommands';
export let conid;
export let database;
export let cache;
export let display;
export let setConfig;
export let changeSetState;
export let dispatchChangeSet;
export let setLoadedRows;
export const activator = createActivator('CollectionJsonView', true);
let isLoading = false;
let loadedTime = null;
let expandAll = false;
let expandKey = 0;
let loadedRows = [];
let skip = 0;
let limit = getIntSettingsValue('dataGrid.collectionPageSize', 50, 5, 1000);
async function loadData() {
isLoading = true;
// @ts-ignore
loadedRows = await loadCollectionDataPage($$props, parseInt(skip) || 0, parseInt(limit) || 50);
if (setLoadedRows) setLoadedRows(loadedRows);
isLoading = false;
loadedTime = new Date().getTime();
}
$: if (cache?.refreshTime > loadedTime) {
loadData();
}
onMount(() => {
loadData();
});
registerMenu({ placeTag: 'switch' });
const menu = getContextMenu();
$: grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
// $: console.log('GRIDER', grider);
export function handleExpandAll() {
expandAll = true;
expandKey += 1;
invalidateCommands();
}
export function handleCollapseAll() {
expandAll = false;
expandKey += 1;
invalidateCommands();
}
export function isExpandedAll() {
return expandAll;
}
</script>
<div class="wrapper" use:contextMenu={menu}>
<div class="toolbar">
<Pager bind:skip bind:limit on:load={() => display.reload()} />
</div>
<div class="json">
{#key expandKey}
{#each _.range(0, grider.rowCount) as rowIndex}
<CollectionJsonRow {grider} {rowIndex} {expandAll} />
{/each}
{/key}
</div>
</div>
{#if isLoading}
<LoadingInfo wrapper message="Loading data" />
{/if}
<style>
.toolbar {
background: var(--theme-toolstrip-background);
display: flex;
border-bottom: var(--theme-toolstrip-border);
border-top: var(--theme-toolstrip-border);
margin-bottom: 3px;
}
.toolbar :global(input){
margin-top: 3px;
margin-bottom: 3px;
height: 26px;
}
.json {
overflow: auto;
flex: 1;
/* position: relative; */
}
.wrapper {
display: flex;
flex-direction: column;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;

View File

@@ -1,4 +1,7 @@
<script lang="ts" context="module">
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import { __t } from '../translations';
const getCurrentDataForm = () => getActiveComponent('FormView');
// registerCommand({
@@ -173,8 +176,6 @@
import { getContext } from 'svelte';
import invalidateCommands from '../commands/invalidateCommands';
import registerCommand from '../commands/registerCommand';
import DataGridCell from '../datagrid/DataGridCell.svelte';
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
import InplaceEditor from '../datagrid/InplaceEditor.svelte';
@@ -191,13 +192,13 @@
import { copyTextToClipboard, extractRowCopiedValue } from '../utility/clipboard';
import { isCtrlOrCommandKey } from '../utility/common';
import contextMenu, { getContextMenu, registerMenu } from '../utility/contextMenu';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import createReducer from '../utility/createReducer';
import keycodes from '../utility/keycodes';
import resizeObserver from '../utility/resizeObserver';
import openReferenceForm from './openReferenceForm';
import { useSettings } from '../utility/metadataLoaders';
import { _t, __t } from '../translations';
import { _t } from '../translations';
export let conid;
export let database;
@@ -205,6 +206,8 @@
export let setConfig;
export let focusOnVisible = false;
export let allRowCount;
export let allRowCountError = null;
export let onReloadRowCount = null;
export let rowCountBefore;
export let isLoading;
export let grider;
@@ -236,12 +239,12 @@
$: columnChunks = _.chunk(display?.formColumns || [], rowCount) as any[][];
$: rowCountInfo = getRowCountInfo(allRowCount, display);
$: rowCountInfo = getRowCountInfo(allRowCount, display, allRowCountError);
const settingsValue = useSettings();
$: gridColoringMode = $settingsValue?.['dataGrid.coloringMode'];
function getRowCountInfo(allRowCount) {
function getRowCountInfo(allRowCount, _display?, _allRowCountError?) {
if (rowCountNotAvailable) {
return _t('dataForm.rowCount', { defaultMessage: 'Row: {rowCount} / ???', values: { rowCount: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString() } });
}
@@ -251,6 +254,9 @@
}
return _t('dataForm.noData', { defaultMessage: 'No data' });
}
if (allRowCountError) {
return _t('dataForm.rowCountMany', { defaultMessage: 'Row: {current} / Many', values: { current: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString() } });
}
if (allRowCount == null || display == null) return _t('dataForm.loadingRowCount', { defaultMessage: 'Loading row count...' });
return _t('dataForm.rowCount', { defaultMessage: 'Row: {current} / {total}', values: { current: ((display.config.formViewRecordNumber || 0) + 1).toLocaleString(), total: allRowCount.toLocaleString() } });
}

View File

@@ -1,34 +1,33 @@
<script lang="ts" context="module">
async function loadRow(props, index) {
const { jslid, formatterFunction, display } = props;
const response = await apiCall('jsldata/get-rows', {
jslid,
offset: index,
limit: 1,
formatterFunction,
filters: display ? display.compileJslFilters() : null,
});
if (response.errorMessage) return response;
return response[0];
}
</script>
<script lang="ts">
import { apiCall } from '../utility/api';
import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.config.formViewRecordNumber || 0);
}
async function handleLoadRowCount() {
return null;
}
</script>
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} rowCountNotAvailable />
async function loadRow(props, index) {
const { jslid, formatterFunction, display } = props;
const response = await apiCall('jsldata/get-rows', {
jslid,
offset: index,
limit: 1,
formatterFunction,
filters: display ? display.compileJslFilters() : null,
});
if (response.errorMessage) return response;
return response[0];
}
</script>
<script lang="ts">
import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
export let display;
async function handleLoadRow() {
return await loadRow($$props, display.config.formViewRecordNumber || 0);
}
async function handleLoadRowCount() {
return null;
}
</script>

View File

@@ -22,6 +22,7 @@
let isLoadedCount = false;
let loadedTime = new Date().getTime();
let allRowCount = null;
let allRowCountError = null;
let errorMessage = null;
const handleLoadCurrentRow = async () => {
@@ -38,7 +39,14 @@
const handleLoadRowCount = async () => {
isLoadingCount = true;
allRowCount = await loadRowCountFunc();
const result = await loadRowCountFunc();
if (result != null && typeof result === 'object' && result.errorMessage) {
allRowCount = null;
allRowCountError = result.errorMessage;
} else {
allRowCount = result;
allRowCountError = null;
}
isLoadedCount = true;
isLoadingCount = false;
};
@@ -55,6 +63,7 @@
rowData = null;
loadedTime = new Date().getTime();
allRowCount = null;
allRowCountError = null;
errorMessage = null;
}
@@ -82,4 +91,4 @@
$: if (onReferenceSourceChanged && rowData) onReferenceSourceChanged([rowData], loadedTime);
</script>
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} onNavigate={handleNavigate} />
<FormView {...$$props} {grider} isLoading={isLoadingData} {allRowCount} {allRowCountError} onReloadRowCount={handleLoadRowCount} onNavigate={handleNavigate} />

View File

@@ -1,5 +1,6 @@
<script lang="ts" context="module">
async function loadRow(props, select) {
import { apiCall } from '../utility/api';
async function loadRow(props, select, options = {}) {
const { conid, database } = props;
if (!select) return null;
@@ -9,6 +10,7 @@
database,
select,
auditLogSessionGroup: 'data-form',
...options,
});
if (response.errorMessage) return response;
@@ -17,7 +19,6 @@
</script>
<script lang="ts">
import { apiCall } from '../utility/api';
import _ from 'lodash';
import LoadingFormView from './LoadingFormView.svelte';
@@ -28,8 +29,18 @@
}
async function handleLoadRowCount() {
const countRow = await loadRow($$props, display.getCountQuery());
return countRow ? parseInt(countRow.count) : null;
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
);
try {
const countRow = await Promise.race([
loadRow($$props, display.getCountQuery(), { commandTimeout: 3000 }),
timeoutPromise,
]);
return countRow ? parseInt(countRow.count) : null;
} catch (err) {
return { errorMessage: err.message || 'Error loading row count' };
}
}
</script>

View File

@@ -26,6 +26,8 @@
</script>
<script>
import DOMPurify from 'dompurify';
export let icon;
export let title = null;
export let padLeft = false;
@@ -34,6 +36,7 @@
export let colorClass = null;
$: iconValue = typeof icon === 'string' ? icon : icon?.light || icon?.dark || '';
$: isSvgString = iconValue.trim().startsWith('<svg');
$: sanitizedSvg = isSvgString ? DOMPurify.sanitize(iconValue, { USE_PROFILES: { svg: true, svgFilters: true } }) : '';
$: isTextIcon = iconValue.trim().startsWith('text ');
const iconNames = {
@@ -117,6 +120,7 @@
'icon undo': 'mdi mdi-undo',
'icon redo': 'mdi mdi-redo',
'icon save': 'mdi mdi-content-save',
'icon apply': 'mdi mdi-content-save-check',
'icon account': 'mdi mdi-account',
'icon sql-file': 'mdi mdi-file',
'icon web': 'mdi mdi-web',
@@ -129,6 +133,7 @@
'icon data-deploy': 'mdi mdi-database-settings',
'icon team-file': 'mdi mdi-account-file',
'icon team-folder': 'mdi mdi-account-details',
'icon graphql': 'mdi mdi-graphql',
'icon cloud-account': 'mdi mdi-account-remove-outline',
'icon cloud-account-connected': 'mdi mdi-account-check-outline',
@@ -371,12 +376,13 @@
'img team-file': 'mdi mdi-account-file color-icon-red',
'img team-folder': 'mdi mdi-account-details color-icon-blue',
'img table-backup': 'mdi mdi-cube color-icon-yellow',
'img graphql': 'mdi mdi-graphql color-icon-magenta',
};
</script>
{#if isSvgString}
<span class="svg-inline" class:padLeft class:padRight {title} {style} on:click data-testid={$$props['data-testid']}>
{@html iconValue}
{@html sanitizedSvg}
</span>
{:else if isTextIcon}
{@const textIconParts = iconValue.trim().split(' ')}

View File

@@ -1,4 +1,6 @@
<script lang="ts" context="module">
import { extensions } from '../stores';
import { findFileFormat } from '../plugins/fileformats';
function getFileFilters(extensions, storageType) {
const res = [];
const format = findFileFormat(extensions, storageType);
@@ -12,10 +14,8 @@
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import { getFormContext } from '../forms/FormProviderCore.svelte';
import { findFileFormat } from '../plugins/fileformats';
import { extensions } from '../stores';
import getElectron from '../utility/getElectron';
import { addFilesToSourceList } from './ImportExportConfigurator.svelte';
import getElectron from '../utility/getElectron';
let isLoading = false;

View File

@@ -1,4 +1,8 @@
<script lang="ts" context="module">
import { extensions } from '../stores';
import getAsArray from '../utility/getAsArray';
import { findFileFormat } from '../plugins/fileformats';
import { apiCall } from '../utility/api';
async function addFileToSourceListDefault({ fileName, shortName, isDownload }, newSources, newValues) {
const sourceName = shortName;
newSources.push(sourceName);
@@ -61,10 +65,6 @@
import FontIcon from '../icons/FontIcon.svelte';
import ColumnMapModal from '../modals/ColumnMapModal.svelte';
import { showModal } from '../modals/modalTools';
import { findFileFormat } from '../plugins/fileformats';
import { extensions } from '../stores';
import { apiCall } from '../utility/api';
import getAsArray from '../utility/getAsArray';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { setUploadListener } from '../utility/uploadFiles';
import { createPreviewReader, getTargetName } from './createImpExpScript';

View File

@@ -7,7 +7,7 @@
import JsonUiMarkdown from './JsonUiMarkdown.svelte';
import JsonUiTextBlock from './JsonUiTextBlock.svelte';
import JsonUiTickList from './JsonUiTickList.svelte';
import { JsonUiBlock } from './jsonuitypes';
import type { JsonUiBlock } from './jsonuitypes';
export let blocks: JsonUiBlock[] = [];
export let passProps = {};

View File

@@ -1,4 +1,9 @@
<script context="module" lang="ts">
import { apiCall } from '../utility/api';
import { showModal } from './modalTools';
import ErrorMessageModal from './ErrorMessageModal.svelte';
import { showSnackbarSuccess } from '../utility/snackbar';
import _ from 'lodash';
export async function saveScriptToDatabase({ conid, database }, sql, syncModel = true, logMessage = null) {
const resp = await apiCall('database-connections/run-script', {
conid,
@@ -38,7 +43,6 @@
</script>
<script>
import _ from 'lodash';
import { writable } from 'svelte/store';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
@@ -48,12 +52,8 @@
import FontIcon from '../icons/FontIcon.svelte';
import newQuery from '../query/newQuery';
import SqlEditor from '../query/SqlEditor.svelte';
import { apiCall } from '../utility/api';
import { showSnackbarSuccess } from '../utility/snackbar';
import ErrorMessageModal from './ErrorMessageModal.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal, showModal } from './modalTools';
import { closeCurrentModal } from './modalTools';
import { _t } from '../translations';
export let sql;

View File

@@ -0,0 +1,74 @@
<script lang="ts">
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import TemplatedCheckboxField from '../forms/TemplatedCheckboxField.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
import { apiCall } from '../utility/api';
import { _t } from '../translations';
export let onConfirm;
const SKIP_SETTING_KEY = 'dataGrid.skipFetchAllConfirm';
let dontAskAgain = false;
</script>
<FormProvider>
<ModalBase {...$$restProps} data-testid="FetchAllConfirmModal">
<svelte:fragment slot="header">
{_t('datagrid.fetchAll.title', { defaultMessage: 'Fetch All Rows' })}
</svelte:fragment>
<div class="message">
<FontIcon icon="img warn" />
<span>
{_t('datagrid.fetchAll.warning', {
defaultMessage:
'This will load all remaining rows into memory. For large tables, this may consume a significant amount of memory and could affect application performance.',
})}
</span>
</div>
<div class="mt-2">
<TemplatedCheckboxField
label={_t('common.dontAskAgain', { defaultMessage: "Don't ask again" })}
templateProps={{ noMargin: true }}
checked={dontAskAgain}
on:change={e => {
dontAskAgain = e.detail;
apiCall('config/update-settings', { [SKIP_SETTING_KEY]: e.detail });
}}
data-testid="FetchAllConfirmModal_dontAskAgain"
/>
</div>
<svelte:fragment slot="footer">
<FormSubmit
value={_t('datagrid.fetchAll.confirm', { defaultMessage: 'Fetch All' })}
on:click={() => {
closeCurrentModal();
onConfirm();
}}
data-testid="FetchAllConfirmModal_okButton"
/>
<FormStyledButton
type="button"
value={_t('common.close', { defaultMessage: 'Close' })}
on:click={closeCurrentModal}
data-testid="FetchAllConfirmModal_closeButton"
/>
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.message {
display: flex;
align-items: flex-start;
gap: 8px;
line-height: 1.5;
}
</style>

View File

@@ -31,17 +31,23 @@
command: 'new.connection',
changeWidget: 'database',
testid: 'NewObjectModal_connection',
disabledMessage: _t('newObject.connectionLocalDisabled', { defaultMessage: 'You are not allowed to create new connections' }),
disabledMessage: _t('newObject.connectionLocalDisabled', {
defaultMessage: 'You are not allowed to create new connections',
}),
},
{
icon: 'icon cloud-connection',
colorClass: 'color-icon-blue',
title: _t('common.connectionOnCloud', { defaultMessage: 'Connection on Cloud' }),
description: _t('newObject.connectionOnCloudDescription', { defaultMessage: 'Database connection stored on DbGate Cloud' }),
description: _t('newObject.connectionOnCloudDescription', {
defaultMessage: 'Database connection stored on DbGate Cloud',
}),
command: 'new.connectionOnCloud',
changeWidget: 'cloud-private',
testid: 'NewObjectModal_connectionOnCloud',
disabledMessage: _t('newObject.connectionOnCloudDisabled', { defaultMessage: 'For creating connections on DbGate Cloud, you need to be logged in' }),
disabledMessage: _t('newObject.connectionOnCloudDisabled', {
defaultMessage: 'For creating connections on DbGate Cloud, you need to be logged in',
}),
},
{
icon: 'icon query-design',
@@ -50,7 +56,9 @@
description: _t('newObject.queryDesignerDescription', { defaultMessage: 'Design SQL queries visually' }),
command: 'new.queryDesign',
testid: 'NewObjectModal_queryDesign',
disabledMessage: _t('newObject.queryDesignerDisabled', { defaultMessage: 'Query Designer is not available for current database' }),
disabledMessage: _t('newObject.queryDesignerDisabled', {
defaultMessage: 'Query Designer is not available for current database',
}),
isProFeature: true,
},
{
@@ -60,13 +68,17 @@
description: _t('newObject.erDiagramDescription', { defaultMessage: 'Visualize database structure' }),
command: 'new.diagram',
testid: 'NewObjectModal_diagram',
disabledMessage: _t('newObject.erDiagramDisabled', { defaultMessage: 'ER Diagram is not available for current database' }),
disabledMessage: _t('newObject.erDiagramDisabled', {
defaultMessage: 'ER Diagram is not available for current database',
}),
},
{
icon: 'icon perspective',
colorClass: 'color-icon-yellow',
title: _t('common.perspective', { defaultMessage: 'Perspective' }),
description: _t('newObject.perspectiveDescription', { defaultMessage: 'Join complex data from multiple databases' }),
description: _t('newObject.perspectiveDescription', {
defaultMessage: 'Join complex data from multiple databases',
}),
command: 'new.perspective',
testid: 'NewObjectModal_perspective',
isProFeature: true,
@@ -78,25 +90,35 @@
description: _t('newObject.tableDescription', { defaultMessage: 'Create table in the current database' }),
command: 'new.table',
testid: 'NewObjectModal_table',
disabledMessage: _t('newObject.tableDisabled', { defaultMessage: 'Table creation is not available for current database' }),
disabledMessage: _t('newObject.tableDisabled', {
defaultMessage: 'Table creation is not available for current database',
}),
},
{
icon: 'icon sql-generator',
colorClass: 'color-icon-green',
title: _t('common.sqlGenerator', { defaultMessage: 'SQL Generator' }),
description: _t('newObject.sqlGeneratorDescription', { defaultMessage: 'Generate SQL scripts for database objects' }),
description: _t('newObject.sqlGeneratorDescription', {
defaultMessage: 'Generate SQL scripts for database objects',
}),
command: 'sql.generator',
testid: 'NewObjectModal_sqlGenerator',
disabledMessage: _t('newObject.sqlGeneratorDisabled', { defaultMessage: 'SQL Generator is not available for current database' }),
disabledMessage: _t('newObject.sqlGeneratorDisabled', {
defaultMessage: 'SQL Generator is not available for current database',
}),
},
{
icon: 'icon export',
colorClass: 'color-icon-green',
title: _t('common.exportDatabase', { defaultMessage: 'Export database' }),
description: _t('newObject.exportDescription', { defaultMessage: 'Export to file like CSV, JSON, Excel, or other DB' }),
description: _t('newObject.exportDescription', {
defaultMessage: 'Export to file like CSV, JSON, Excel, or other DB',
}),
command: 'database.export',
testid: 'NewObjectModal_databaseExport',
disabledMessage: _t('newObject.exportDisabled', { defaultMessage: 'Export is not available for current database' }),
disabledMessage: _t('newObject.exportDisabled', {
defaultMessage: 'Export is not available for current database',
}),
},
{
icon: 'icon compare',
@@ -105,7 +127,9 @@
description: _t('newObject.compareDescription', { defaultMessage: 'Compare database schemas' }),
command: 'database.compare',
testid: 'NewObjectModal_databaseCompare',
disabledMessage: _t('newObject.compareDisabled', { defaultMessage: 'Database comparison is not available for current database' }),
disabledMessage: _t('newObject.compareDisabled', {
defaultMessage: 'Database comparison is not available for current database',
}),
isProFeature: true,
},
{
@@ -115,9 +139,37 @@
description: _t('newObject.databaseChatDescription', { defaultMessage: 'Chat with your database using AI' }),
command: 'database.chat',
isProFeature: true,
disabledMessage: _t('newObject.databaseChatDisabled', { defaultMessage: 'Database chat is not available for current database' }),
disabledMessage: _t('newObject.databaseChatDisabled', {
defaultMessage: 'Database chat is not available for current database',
}),
testid: 'NewObjectModal_databaseChat',
},
{
icon: 'icon ai',
colorClass: 'color-icon-blue',
title: _t('common.graphqlChat', { defaultMessage: 'GraphQL Chat' }),
description: _t('newObject.graphqlChatDescription', { defaultMessage: 'Chat with your GraphQL API using AI' }),
command: 'graphql.chat',
isProFeature: true,
disabledMessage: _t('newObject.graphqlChatDisabled', {
defaultMessage: 'GraphQL chat is not available for current connection',
}),
testid: 'NewObjectModal_graphqlChat',
},
{
icon: 'icon graphql',
colorClass: 'color-icon-magenta',
title: _t('common.graphqlQuery', { defaultMessage: 'GraphQL Query' }),
description: _t('newObject.graphqlQueryDescription', {
defaultMessage: 'Write queries, choose attributes and arguments',
}),
isProFeature: true,
disabledMessage: _t('newObject.graphqlQueryDisabled', {
defaultMessage: 'GraphQL Query is not available for current database',
}),
testid: 'NewObjectModal_graphqlQuery',
command: 'new.graphqlQuery',
}
];
</script>
@@ -158,19 +210,19 @@
<style>
.wrapper {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
padding: 16px;
max-width: 820px;
margin: 0 auto;
}
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
padding: 16px;
max-width: 820px;
margin: 0 auto;
}
.create-header {
font-size: 150%;
text-transform: uppercase;
color: var(--theme-generic-font-grayed);
text-align: center;
margin: 12px 0 4px;
}
.create-header {
font-size: 150%;
text-transform: uppercase;
color: var(--theme-generic-font-grayed);
text-align: center;
margin: 12px 0 4px;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,246 +1,247 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('PerspectiveView');
registerCommand({
id: 'perspective.customJoin',
category: __t('perspective.category', { defaultMessage: 'Perspective' }),
name: __t('perspective.customJoin', { defaultMessage: 'Custom join' }),
keyText: 'CtrlOrCommand+J',
isRelatedToTab: true,
icon: 'icon custom-join',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().defineCustomJoin(),
});
// registerCommand({
// id: 'perspective.arrange',
// category: 'Perspective',
// icon: 'icon arrange',
// name: 'Arrange',
// toolbar: true,
// isRelatedToTab: true,
// testEnabled: () => getCurrentEditor()?.canArrange(),
// onClick: () => getCurrentEditor().arrange(),
// });
</script>
<script lang="ts">
import {
extractPerspectiveDatabases,
PerspectiveDataProvider,
PerspectiveTableNode,
PerspectiveTreeNode,
processPerspectiveDefaultColunns,
shouldProcessPerspectiveDefaultColunns,
} from 'dbgate-datalib';
import type { ChangePerspectiveConfigFunc, PerspectiveConfig } from 'dbgate-datalib';
import _ from 'lodash';
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import debug from 'debug';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import PerspectiveTree from './PerspectiveTree.svelte';
import PerspectiveTable from './PerspectiveTable.svelte';
import { apiCall } from '../utility/api';
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import { PerspectiveDataLoader } from 'dbgate-datalib';
import stableStringify from 'json-stable-stringify';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import { showModal } from '../modals/modalTools';
import CustomJoinModal from './CustomJoinModal.svelte';
import PerspectiveFilters from './PerspectiveFilters.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { useMultipleDatabaseInfo } from '../utility/useMultipleDatabaseInfo';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import PerspectiveDesigner from './PerspectiveDesigner.svelte';
import { tick } from 'svelte';
import { sleep } from '../utility/common';
import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
import { _t, __t } from '../translations';
const dbg = debug('dbgate:PerspectiveView');
export let conid;
export let database;
export let driver;
export let config: PerspectiveConfig;
export let setConfig: ChangePerspectiveConfigFunc;
let tempRootDesignerId: string = null;
export let loadedCounts;
export let cache;
let managerSize;
let filter;
export const activator = createActivator('PerspectiveView', true);
$: if (managerSize) setLocalStorage('perspectiveManagerWidth', managerSize);
function getInitialManagerSize() {
const width = getLocalStorage('perspectiveManagerWidth');
if (_.isNumber(width) && width > 30 && width < 500) {
return `${width}px`;
}
return '300px';
}
export function defineCustomJoin() {
if (!root) return;
showModal(CustomJoinModal, {
config,
setConfig,
conid,
database,
root,
});
}
// export function canArrange() {
// return !config.isArranged;
// }
// export function arrange() {
// // setConfig(cfg => ({
// // ...cfg,
// // isArranged: true,
// // }));
// runCommand('designer.arrange');
// }
let perspectiveDatabases = extractPerspectiveDatabases({ conid, database }, config);
$: {
const newDatabases = extractPerspectiveDatabases({ conid, database }, config);
if (stableStringify(newDatabases) != stableStringify(perspectiveDatabases)) {
perspectiveDatabases = newDatabases;
}
}
$: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases);
$: loader = new PerspectiveDataLoader(apiCall);
$: dataPatterns = usePerspectiveDataPatterns({ conid, database }, config, cache, $dbInfos, loader);
$: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId);
$: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null;
$: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: collectionInfo = rootDb?.collections.find(
x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName
);
$: dataProvider = new PerspectiveDataProvider(cache, loader, $dataPatterns);
$: root =
tableInfo || viewInfo || collectionInfo
? new PerspectiveTableNode(
tableInfo || viewInfo || collectionInfo,
$dbInfos,
config,
setConfig,
dataProvider,
{ conid, database },
null,
config.rootDesignerId
)
: null;
$: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId);
$: {
if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, $dataPatterns, conid, database)) {
setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, $dataPatterns, conid, database));
}
}
// $: console.log('PERSPECTIVE', config);
// $: console.log('VIEW ROOT', root);
// $: console.log('dataPatterns', $dataPatterns);
</script>
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('perspective.chooseData', { defaultMessage: "Choose data" })} name="perspectiveTree" height={'70%'}>
{#if tempRoot && tempRoot != root}
<div class="temp-root">
<div>
<FontIcon icon="img table" />
{tempRoot.title}
</div>
<InlineButton
on:click={() => {
tempRootDesignerId = tempRoot?.parentNode?.designerId;
}}>Go up</InlineButton
>
</div>
{/if}
<SearchBoxWrapper {filter}>
<SearchInput placeholder={_t('perspective.searchColumnOrTable', { defaultMessage: "Search column or table" })} bind:value={filter} />
<CloseSearchButton bind:filter />
</SearchBoxWrapper>
<ManagerInnerContainer width={managerSize}>
{#if root}
<PerspectiveTree {root} {tempRoot} {config} {setConfig} {conid} {database} {filter} />
{/if}
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('perspective.filters', { defaultMessage: "Filters" })} name="tableFilters">
<PerspectiveFilters {managerSize} {config} {setConfig} {conid} {database} {driver} {root} />
</WidgetColumnBarItem>
</WidgetColumnBar>
</div>
<svelte:fragment slot="2">
<VerticalSplitter allowCollapseChild1 allowCollapseChild2>
<svelte:fragment slot="1">
<PerspectiveDesigner
{config}
{conid}
{database}
{setConfig}
dbInfos={$dbInfos}
dataPatterns={$dataPatterns}
{root}
onClickTableHeader={designerId => {
sleep(100).then(() => {
tempRootDesignerId = designerId;
});
}}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<PerspectiveTable {root} {loadedCounts} {config} {setConfig} {conid} {database} />
</svelte:fragment>
</VerticalSplitter>
</svelte:fragment>
</HorizontalSplitter>
<style>
.left {
display: flex;
flex: 1;
background: var(--theme-altsidebar-background);
border-right: var(--theme-altsidebar-border);
}
.temp-root {
border: var(--theme-altsearchbox-border);
background: var(--theme-altsearchbox-background);
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 2px;
}
</style>
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('PerspectiveView');
registerCommand({
id: 'perspective.customJoin',
category: __t('perspective.category', { defaultMessage: 'Perspective' }),
name: __t('perspective.customJoin', { defaultMessage: 'Custom join' }),
keyText: 'CtrlOrCommand+J',
isRelatedToTab: true,
icon: 'icon custom-join',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().defineCustomJoin(),
});
// registerCommand({
// id: 'perspective.arrange',
// category: 'Perspective',
// icon: 'icon arrange',
// name: 'Arrange',
// toolbar: true,
// isRelatedToTab: true,
// testEnabled: () => getCurrentEditor()?.canArrange(),
// onClick: () => getCurrentEditor().arrange(),
// });
</script>
<script lang="ts">
import {
extractPerspectiveDatabases,
PerspectiveDataProvider,
PerspectiveTableNode,
PerspectiveTreeNode,
processPerspectiveDefaultColunns,
shouldProcessPerspectiveDefaultColunns,
} from 'dbgate-datalib';
import type { ChangePerspectiveConfigFunc, PerspectiveConfig } from 'dbgate-datalib';
import _ from 'lodash';
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import debug from 'debug';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import PerspectiveTree from './PerspectiveTree.svelte';
import PerspectiveTable from './PerspectiveTable.svelte';
import { apiCall } from '../utility/api';
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import { PerspectiveDataLoader } from 'dbgate-datalib';
import stableStringify from 'json-stable-stringify';
import createActivator from '../utility/createActivator';
import { showModal } from '../modals/modalTools';
import CustomJoinModal from './CustomJoinModal.svelte';
import PerspectiveFilters from './PerspectiveFilters.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { useMultipleDatabaseInfo } from '../utility/useMultipleDatabaseInfo';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import PerspectiveDesigner from './PerspectiveDesigner.svelte';
import { tick } from 'svelte';
import { sleep } from '../utility/common';
import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
import { _t } from '../translations';
const dbg = debug('dbgate:PerspectiveView');
export let conid;
export let database;
export let driver;
export let config: PerspectiveConfig;
export let setConfig: ChangePerspectiveConfigFunc;
let tempRootDesignerId: string = null;
export let loadedCounts;
export let cache;
let managerSize;
let filter;
export const activator = createActivator('PerspectiveView', true);
$: if (managerSize) setLocalStorage('perspectiveManagerWidth', managerSize);
function getInitialManagerSize() {
const width = getLocalStorage('perspectiveManagerWidth');
if (_.isNumber(width) && width > 30 && width < 500) {
return `${width}px`;
}
return '300px';
}
export function defineCustomJoin() {
if (!root) return;
showModal(CustomJoinModal, {
config,
setConfig,
conid,
database,
root,
});
}
// export function canArrange() {
// return !config.isArranged;
// }
// export function arrange() {
// // setConfig(cfg => ({
// // ...cfg,
// // isArranged: true,
// // }));
// runCommand('designer.arrange');
// }
let perspectiveDatabases = extractPerspectiveDatabases({ conid, database }, config);
$: {
const newDatabases = extractPerspectiveDatabases({ conid, database }, config);
if (stableStringify(newDatabases) != stableStringify(perspectiveDatabases)) {
perspectiveDatabases = newDatabases;
}
}
$: dbInfos = useMultipleDatabaseInfo(perspectiveDatabases);
$: loader = new PerspectiveDataLoader(apiCall);
$: dataPatterns = usePerspectiveDataPatterns({ conid, database }, config, cache, $dbInfos, loader);
$: rootObject = config?.nodes?.find(x => x.designerId == config?.rootDesignerId);
$: rootDb = rootObject ? $dbInfos?.[rootObject.conid || conid]?.[rootObject.database || database] : null;
$: tableInfo = rootDb?.tables.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: viewInfo = rootDb?.views.find(x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName);
$: collectionInfo = rootDb?.collections.find(
x => x.pureName == rootObject?.pureName && x.schemaName == rootObject?.schemaName
);
$: dataProvider = new PerspectiveDataProvider(cache, loader, $dataPatterns);
$: root =
tableInfo || viewInfo || collectionInfo
? new PerspectiveTableNode(
tableInfo || viewInfo || collectionInfo,
$dbInfos,
config,
setConfig,
dataProvider,
{ conid, database },
null,
config.rootDesignerId
)
: null;
$: tempRoot = root?.findNodeByDesignerId(tempRootDesignerId);
$: {
if (shouldProcessPerspectiveDefaultColunns(config, $dbInfos, $dataPatterns, conid, database)) {
setConfig(cfg => processPerspectiveDefaultColunns(cfg, $dbInfos, $dataPatterns, conid, database));
}
}
// $: console.log('PERSPECTIVE', config);
// $: console.log('VIEW ROOT', root);
// $: console.log('dataPatterns', $dataPatterns);
</script>
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('perspective.chooseData', { defaultMessage: "Choose data" })} name="perspectiveTree" height={'70%'}>
{#if tempRoot && tempRoot != root}
<div class="temp-root">
<div>
<FontIcon icon="img table" />
{tempRoot.title}
</div>
<InlineButton
on:click={() => {
tempRootDesignerId = tempRoot?.parentNode?.designerId;
}}>Go up</InlineButton
>
</div>
{/if}
<SearchBoxWrapper {filter}>
<SearchInput placeholder={_t('perspective.searchColumnOrTable', { defaultMessage: "Search column or table" })} bind:value={filter} />
<CloseSearchButton bind:filter />
</SearchBoxWrapper>
<ManagerInnerContainer width={managerSize}>
{#if root}
<PerspectiveTree {root} {tempRoot} {config} {setConfig} {conid} {database} {filter} />
{/if}
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('perspective.filters', { defaultMessage: "Filters" })} name="tableFilters">
<PerspectiveFilters {managerSize} {config} {setConfig} {conid} {database} {driver} {root} />
</WidgetColumnBarItem>
</WidgetColumnBar>
</div>
<svelte:fragment slot="2">
<VerticalSplitter allowCollapseChild1 allowCollapseChild2>
<svelte:fragment slot="1">
<PerspectiveDesigner
{config}
{conid}
{database}
{setConfig}
dbInfos={$dbInfos}
dataPatterns={$dataPatterns}
{root}
onClickTableHeader={designerId => {
sleep(100).then(() => {
tempRootDesignerId = designerId;
});
}}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<PerspectiveTable {root} {loadedCounts} {config} {setConfig} {conid} {database} />
</svelte:fragment>
</VerticalSplitter>
</svelte:fragment>
</HorizontalSplitter>
<style>
.left {
display: flex;
flex: 1;
background: var(--theme-altsidebar-background);
border-right: var(--theme-altsidebar-border);
}
.temp-root {
border: var(--theme-altsearchbox-border);
background: var(--theme-altsearchbox-background);
display: flex;
justify-content: space-between;
align-items: center;
padding-left: 2px;
}

View File

@@ -1,93 +1,90 @@
<script lang="ts" context="module">
async function loadPlugins(pluginsDict, installedPlugins) {
window['DBGATE_PACKAGES'] = {
'dbgate-tools': dbgateTools,
'dbgate-sqltree': sqlTree,
'dbgate-datalib': dataLib,
};
// neccessary for older plugins
window['DBGATE_TOOLS'] = dbgateTools;
const newPlugins = {};
for (const installed of installedPlugins || []) {
if (!_.keys(pluginsDict).includes(installed.name)) {
console.log('Loading module', installed.name);
loadingPluginStore.set({
loaded: false,
loadingPackageName: installed.name,
});
const resp = await apiCall('plugins/script', {
packageName: installed.name,
});
const module = eval(`${resp}; plugin`);
console.log('Loaded plugin', module);
const moduleContent = module.__esModule ? module.default : module;
newPlugins[installed.name] = moduleContent;
}
}
if (installedPlugins) {
loadingPluginStore.set({
loaded: true,
loadingPackageName: null,
});
}
return newPlugins;
}
function buildDrivers(plugins) {
const res = isProApp() ? [openApiDriver, oDataDriver, graphQlDriver] : [];
for (const { content } of plugins) {
if (content.drivers) res.push(...content.drivers);
}
return res;
}
function filterByEdition(arr) {
return arr.filter(x => !x.premiumOnly || isProApp());
}
export function buildExtensions(plugins) {
const extensions = {
plugins,
fileFormats: filterByEdition(buildFileFormats(plugins)),
drivers: filterByEdition(buildDrivers(plugins)),
quickExports: filterByEdition(buildQuickExports(plugins)),
};
return extensions;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { extensions, loadingPluginStore } from '../stores';
import { useInstalledPlugins } from '../utility/metadataLoaders';
import { buildFileFormats, buildQuickExports } from './fileformats';
import * as dbgateTools from 'dbgate-tools';
import * as sqlTree from 'dbgate-sqltree';
import * as dataLib from 'dbgate-datalib';
import { loadingPluginStore } from '../stores';
import { apiCall } from '../utility/api';
import { isProApp } from '../utility/proTools';
import { openApiDriver, oDataDriver, graphQlDriver } from 'dbgate-rest';
let pluginsDict = {};
const installedPlugins = useInstalledPlugins();
$: loadPlugins(pluginsDict, $installedPlugins).then(newPlugins => {
if (_.isEmpty(newPlugins)) return;
pluginsDict = _.pick(
{ ...pluginsDict, ...newPlugins },
$installedPlugins.map(y => y.name)
);
});
$: plugins = ($installedPlugins || [])
.map(manifest => ({
packageName: manifest.name,
manifest,
content: pluginsDict[manifest.name],
}))
.filter(x => x.content);
$: $extensions = buildExtensions(plugins);
</script>
import { buildFileFormats, buildQuickExports } from './fileformats';
async function loadPlugins(pluginsDict, installedPlugins) {
window['DBGATE_PACKAGES'] = {
'dbgate-tools': dbgateTools,
'dbgate-sqltree': sqlTree,
'dbgate-datalib': dataLib,
};
// neccessary for older plugins
window['DBGATE_TOOLS'] = dbgateTools;
const newPlugins = {};
for (const installed of installedPlugins || []) {
if (!_.keys(pluginsDict).includes(installed.name)) {
console.log('Loading module', installed.name);
loadingPluginStore.set({
loaded: false,
loadingPackageName: installed.name,
});
const resp = await apiCall('plugins/script', {
packageName: installed.name,
});
const module = eval(`${resp}; plugin`);
console.log('Loaded plugin', module);
const moduleContent = module.__esModule ? module.default : module;
newPlugins[installed.name] = moduleContent;
}
}
if (installedPlugins) {
loadingPluginStore.set({
loaded: true,
loadingPackageName: null,
});
}
return newPlugins;
}
function buildDrivers(plugins) {
const res = isProApp() ? [openApiDriver, oDataDriver, graphQlDriver] : [];
for (const { content } of plugins) {
if (content.drivers) res.push(...content.drivers);
}
return res;
}
function filterByEdition(arr) {
return arr.filter(x => !x.premiumOnly || isProApp());
}
export function buildExtensions(plugins) {
const extensions = {
plugins,
fileFormats: filterByEdition(buildFileFormats(plugins)),
drivers: filterByEdition(buildDrivers(plugins)),
quickExports: filterByEdition(buildQuickExports(plugins)),
};
return extensions;
}
</script>
<script lang="ts">
import { extensions } from '../stores';
import { useInstalledPlugins } from '../utility/metadataLoaders';
import * as dbgateTools from 'dbgate-tools';
import * as sqlTree from 'dbgate-sqltree';
import * as dataLib from 'dbgate-datalib';
let pluginsDict = {};
const installedPlugins = useInstalledPlugins();
$: loadPlugins(pluginsDict, $installedPlugins).then(newPlugins => {
if (_.isEmpty(newPlugins)) return;
pluginsDict = _.pick(
{ ...pluginsDict, ...newPlugins },
$installedPlugins.map(y => y.name)
);
});
$: plugins = ($installedPlugins || [])
.map(manifest => ({
packageName: manifest.name,
manifest,
content: pluginsDict[manifest.name],
}))
.filter(x => x.content);

View File

@@ -11,6 +11,7 @@ export default function newQuery({
initialData = undefined,
multiTabIndex = undefined,
fixCurrentConnection = false,
forceBindCurrentConnection = false,
...props
} = {}) {
const currentDb = getCurrentDatabase();
@@ -38,13 +39,14 @@ export default function newQuery({
tabComponent,
multiTabIndex,
focused: true,
props: driver?.supportExecuteQuery
? {
...props,
conid: connection._id,
database,
}
: props,
props:
driver?.supportExecuteQuery || forceBindCurrentConnection
? {
...props,
conid: connection._id,
database,
}
: props,
},
{ editor: initialData }
);
@@ -61,3 +63,13 @@ export function newDiagram() {
export function newPerspective() {
return newQuery({ tabComponent: 'PerspectiveTab', icon: 'img perspective', title: 'Perspective #' });
}
export function newGraphQlQuery() {
return newQuery({
tabComponent: 'GraphQlQueryTab',
icon: 'img graphql',
title: 'Query #',
initialData: 'query {\n}',
forceBindCurrentConnection: true,
});
}

View File

@@ -55,6 +55,12 @@
defaultMessage: 'Skip confirmation when saving collection data (NoSQL)',
})}
/>
<FormCheckboxField
name="dataGrid.skipFetchAllConfirm"
label={_t('settings.confirmations.skipFetchAllConfirm', {
defaultMessage: 'Skip confirmation when fetching all rows',
})}
/>
</FormValues>
</div>

View File

@@ -1,10 +1,13 @@
<script lang="ts">
import FormTextField from '../forms/FormTextField.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import FormPasswordField from '../forms/FormPasswordField.svelte';
import { extensions, openedConnections, openedSingleDatabaseConnections } from '../stores';
import { getFormContext } from '../forms/FormProviderCore.svelte';
import FormTextAreaField from '../forms/FormTextAreaField.svelte';
import FormArgumentList from '../forms/FormArgumentList.svelte';
import { _t } from '../translations';
import { useConfig } from '../utility/metadataLoaders';
export let isFormReadOnly;
@@ -17,20 +20,81 @@
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
$: advancedFields = driver?.getAdvancedConnectionFields ? driver?.getAdvancedConnectionFields() : null;
$: config = useConfig();
$: showConnectionFieldArgs = { config: $config };
$: showAllowedDatabases =
driver?.showConnectionField?.('allowedDatabases', $values, showConnectionFieldArgs) === true;
$: showProxy = driver?.showConnectionField?.('httpProxyUrl', $values, showConnectionFieldArgs) === true;
</script>
<FormTextAreaField
label={_t('connection.allowedDatabases', { defaultMessage: 'Allowed databases, one per line' })}
name="allowedDatabases"
disabled={isConnected || isFormReadOnly}
rows={8}
/>
<FormTextField
label={_t('connection.allowedDatabasesRegex', { defaultMessage: 'Allowed databases regular expression' })}
name="allowedDatabasesRegex"
disabled={isConnected || isFormReadOnly}
/>
{#if showAllowedDatabases}
<FormTextAreaField
label={_t('connection.allowedDatabases', { defaultMessage: 'Allowed databases, one per line' })}
name="allowedDatabases"
disabled={isConnected || isFormReadOnly}
rows={8}
/>
<FormTextField
label={_t('connection.allowedDatabasesRegex', { defaultMessage: 'Allowed databases regular expression' })}
name="allowedDatabasesRegex"
disabled={isConnected || isFormReadOnly}
/>
{/if}
{#if showProxy}
<FormTextField
label={_t('connection.httpProxyUrl', { defaultMessage: 'HTTP Proxy URL' })}
name="httpProxyUrl"
data-testid="ConnectionDriverFields_httpProxyUrl"
placeholder="http://proxy.example.com:8080"
disabled={isConnected || isFormReadOnly}
/>
<div class="row">
<div class="col-6 mr-1">
<FormTextField
label={_t('connection.httpProxyUser', { defaultMessage: 'HTTP Proxy User' })}
name="httpProxyUser"
data-testid="ConnectionDriverFields_httpProxyUser"
disabled={isConnected || isFormReadOnly}
templateProps={{ noMargin: true }}
/>
</div>
<div class="col-6 mr-1">
<FormPasswordField
label={_t('connection.httpProxyPassword', { defaultMessage: 'HTTP Proxy Password' })}
name="httpProxyPassword"
data-testid="ConnectionDriverFields_httpProxyPassword"
disabled={isConnected || isFormReadOnly}
templateProps={{ noMargin: true }}
/>
</div>
</div>
{/if}
{#if driver?.showConnectionField('defaultIsolationLevel', $values, showConnectionFieldArgs) && driver?.isolationLevels}
<FormSelectField
label={_t('connection.defaultIsolationLevel', { defaultMessage: 'Default isolation level' })}
isNative
name="defaultIsolationLevel"
defaultValue={driver.defaultIsolationLevel}
options={driver.isolationLevels.map(level => ({ label: level, value: level }))}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionAdvancedDriverFields_defaultIsolationLevel"
/>
{/if}
{#if advancedFields}
<FormArgumentList args={advancedFields} isReadOnly={isFormReadOnly} />
{/if}
<style>
.row {
margin: var(--dim-large-form-margin);
display: flex;
}
.col-6 {
flex: 1;
}
</style>

View File

@@ -11,7 +11,13 @@
import FormSelectField from '../forms/FormSelectField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import { extensions, getCurrentConfig, openedConnections, openedSingleDatabaseConnections, toggledDatabases } from '../stores';
import {
extensions,
getCurrentConfig,
openedConnections,
openedSingleDatabaseConnections,
toggledDatabases,
} from '../stores';
import getElectron from '../utility/getElectron';
import { useAuthTypes, useConfig } from '../utility/metadataLoaders';
import FormColorField from '../forms/FormColorField.svelte';
@@ -100,7 +106,7 @@
$extensions.drivers
// .filter(driver => !driver.isElectronOnly || electron)
.filter(driver => $toggledDatabases.get(driver.title))
.map((driver) => ({
.map(driver => ({
value: driver.engine,
label: driver.title,
})),

View File

@@ -100,6 +100,14 @@
})}
defaultValue={false}
/>
<FormCheckboxField
name="dataGrid.disableCellDataViewAutoOpen"
label={_t('settings.dataGrid.disableCellDataViewAutoOpen', {
defaultMessage: 'Disable automatic Cell Data View',
})}
defaultValue={false}
/>
</div>
<style>

View File

@@ -1,466 +1,466 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('TableEditor');
registerCommand({
id: 'tableEditor.addColumn',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addColumn', { defaultMessage: 'Add column' }),
icon: 'icon add-column',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable(),
onClick: () => getCurrentEditor().addColumn(),
});
registerCommand({
id: 'tableEditor.addPrimaryKey',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addPrimaryKey', { defaultMessage: 'Add primary key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.allowAddPrimaryKey(),
onClick: () => getCurrentEditor().addPrimaryKey(),
});
registerCommand({
id: 'tableEditor.addForeignKey',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addForeignKey', { defaultMessage: 'Add foreign key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitForeignKeys,
onClick: () => getCurrentEditor().addForeignKey(),
});
registerCommand({
id: 'tableEditor.addIndex',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addIndex', { defaultMessage: 'Add index' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitIndexes,
onClick: () => getCurrentEditor().addIndex(),
});
registerCommand({
id: 'tableEditor.addUnique',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addUnique', { defaultMessage: 'Add unique' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitUniqueConstraints,
onClick: () => getCurrentEditor().addUnique(),
});
</script>
<script lang="ts">
import { editorDeleteColumn, editorDeleteConstraint } from 'dbgate-tools';
import _ from 'lodash';
import { onMount, tick } from 'svelte';
import invalidateCommands from '../commands/invalidateCommands';
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import ColumnLabel from '../elements/ColumnLabel.svelte';
import ConstraintLabel from '../elements/ConstraintLabel.svelte';
import ForeignKeyObjectListControl from '../elements/ForeignKeyObjectListControl.svelte';
import Link from '../elements/Link.svelte';
import ObjectListControl from '../elements/ObjectListControl.svelte';
import { showModal } from '../modals/modalTools';
import useEditorData from '../query/useEditorData';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import { useDbCore } from '../utility/metadataLoaders';
import ColumnEditorModal from './ColumnEditorModal.svelte';
import ForeignKeyEditorModal from './ForeignKeyEditorModal.svelte';
import IndexEditorModal from './IndexEditorModal.svelte';
import PrimaryKeyEditorModal from './PrimaryKeyEditorModal.svelte';
import UniqueEditorModal from './UniqueEditorModal.svelte';
import ObjectFieldsEditor from '../elements/ObjectFieldsEditor.svelte';
import PrimaryKeyLikeListControl from './PrimaryKeyLikeListControl.svelte';
import { __t, _t } from '../translations';
export const activator = createActivator('TableEditor', true);
export let tableInfo;
export let setTableInfo;
export let dbInfo;
export let driver;
export let resetCounter;
export let isCreateTable;
export let schemaList;
$: isWritable = !!setTableInfo;
export function getIsWritable() {
return isWritable;
}
export function getDialect() {
return driver?.dialect;
}
export function addColumn() {
showModal(ColumnEditorModal, {
setTableInfo,
tableInfo,
driver,
onAddNext: async () => {
await tick();
addColumn();
},
});
}
export function allowAddPrimaryKey() {
return isWritable && !tableInfo?.primaryKey;
}
export function addPrimaryKey() {
showModal(PrimaryKeyEditorModal, {
setTableInfo,
tableInfo,
driver,
});
}
export function addForeignKey() {
showModal(ForeignKeyEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
});
}
export function addIndex() {
showModal(IndexEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
driver,
});
}
export function addUnique() {
showModal(UniqueEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
});
}
function getIndexTypeLabel(row) {
const indexType = row?.indexType?.toString()?.toUpperCase();
if (indexType === 'FULLTEXT') return 'FULLTEXT';
if (row?.isUnique) return 'UNIQUE';
if (indexType) return indexType;
return 'INDEX';
}
$: columns = tableInfo?.columns;
$: foreignKeys = tableInfo?.foreignKeys;
$: dependencies = tableInfo?.dependencies;
$: indexes = tableInfo?.indexes;
$: uniques = tableInfo?.uniques;
$: {
tableInfo;
invalidateCommands();
}
$: tableFormOptions = driver?.dialect?.getTableFormOptions?.(tableInfo?.objectId ? 'editTableForm' : 'newTableForm');
</script>
<div class="wrapper">
{#if tableInfo && (tableFormOptions || isCreateTable)}
{#key resetCounter}
<ObjectFieldsEditor
title={_t('tableEditor.tableproperties', { defaultMessage: 'Table properties' })}
fieldDefinitions={tableFormOptions ?? []}
pureNameTitle={isCreateTable ? _t('tableEditor.tablename', { defaultMessage: 'Table name' }) : null}
schemaList={isCreateTable && schemaList?.length >= 0 ? schemaList : null}
values={_.pick(tableInfo, ['schemaName', 'pureName', ...(tableFormOptions ?? []).map(x => x.name)])}
onChangeValues={vals => {
if (!_.isEmpty(vals) && setTableInfo) {
setTableInfo(tbl => ({ ...tbl, ...vals }));
}
}}
/>
{/key}
{/if}
<ObjectListControl
collection={columns?.map((x, index) => ({ ...x, ordinal: index + 1 }))}
title={_t('tableEditor.columnsCount', {
defaultMessage: 'Columns ({columnCount})',
values: { columnCount: columns?.length || 0 },
})}
emptyMessage={_t('tableEditor.nocolumnsdefined', { defaultMessage: 'No columns defined' })}
clickable
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })}
onAddNew={isWritable ? addColumn : null}
displayNameFieldName="columnName"
multipleItemsActions={[
{
text: _t('tableEditor.remove', { defaultMessage: 'Remove' }),
icon: 'icon delete',
onClick: selected => {
setTableInfo(tbl => {
const newColumns = tbl.columns.filter(x => !selected.find(y => y.columnName === x.columnName));
return { ...tbl, columns: newColumns };
});
},
},
{
text: _t('tableEditor.copynames', { defaultMessage: 'Copy names' }),
icon: 'icon copy',
onClick: selected => {
const names = selected.map(x => x.columnName).join('\n');
navigator.clipboard.writeText(names);
},
},
{
text: _t('tableEditor.copydefinitions', { defaultMessage: 'Copy definitions' }),
icon: 'icon copy',
onClick: selected => {
const names = selected.map(x => `${x.columnName} ${x.dataType}${x.notNull ? ' NOT NULL' : ''}`).join(',\n');
navigator.clipboard.writeText(names);
},
},
]}
columns={[
!driver?.dialect?.specificNullabilityImplementation && {
fieldName: 'notNull',
header: _t('tableEditor.nullability', { defaultMessage: 'Nullability' }),
sortable: true,
slot: 0,
},
{
fieldName: 'dataType',
header: _t('tableEditor.dataType', { defaultMessage: 'Data type' }),
sortable: true,
filterable: true,
},
{
fieldName: 'defaultValue',
header: _t('tableEditor.defaultValue', { defaultMessage: 'Default value' }),
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isSparse && {
fieldName: 'isSparse',
header: _t('tableEditor.isSparse', { defaultMessage: 'Is Sparse' }),
sortable: true,
slot: 1,
},
{
fieldName: 'computedExpression',
header: _t('tableEditor.computedExpression', { defaultMessage: 'Computed Expression' }),
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isPersisted && {
fieldName: 'isPersisted',
header: _t('tableEditor.isPersisted', { defaultMessage: 'Is Persisted' }),
sortable: true,
slot: 2,
},
driver?.dialect?.columnProperties?.isUnsigned && {
fieldName: 'isUnsigned',
header: _t('tableEditor.isUnsigned', { defaultMessage: 'Unsigned' }),
sortable: true,
slot: 4,
},
driver?.dialect?.columnProperties?.isZerofill && {
fieldName: 'isZerofill',
header: _t('tableEditor.isZeroFill', { defaultMessage: 'Zero fill' }),
sortable: true,
slot: 5,
},
driver?.dialect?.columnProperties?.columnComment && {
fieldName: 'columnComment',
header: _t('tableEditor.columnComment', { defaultMessage: 'Comment' }),
sortable: true,
filterable: true,
},
isWritable
? {
fieldName: 'actions',
filterable: false,
slot: 3,
}
: null,
]}
>
<svelte:fragment slot="0" let:row
>{row?.notNull
? _t('tableEditor.notnull', { defaultMessage: 'NOT NULL' })
: _t('tableEditor.null', { defaultMessage: 'NULL' })}</svelte:fragment
>
<svelte:fragment slot="1" let:row
>{row?.isSparse
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="2" let:row
>{row?.isPersisted
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="3" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteColumn(tbl, row));
}}>{_t('tableEditor.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
<svelte:fragment slot="4" let:row
>{row?.isUnsigned
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="5" let:row
>{row?.isZerofill
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="name" let:row><ColumnLabel {...row} forceIcon /></svelte:fragment>
</ObjectListControl>
<PrimaryKeyLikeListControl {tableInfo} {setTableInfo} {isWritable} {driver} />
{#if driver?.dialect?.sortingKeys}
<PrimaryKeyLikeListControl
{tableInfo}
{setTableInfo}
{isWritable}
{driver}
constraintLabel="sorting key"
constraintType="sortingKey"
/>
{/if}
{#if !driver?.dialect?.omitIndexes}
<ObjectListControl
collection={indexes}
onAddNew={isWritable && columns?.length > 0 ? addIndex : null}
title={_t('tableEditor.indexes', {
defaultMessage: 'Indexes ({indexCount})',
values: { indexCount: indexes?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.noindexdefined', { defaultMessage: 'No index defined' }) : null}
clickable
on:clickrow={e => showModal(IndexEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, driver })}
columns={[
{
fieldName: 'columns',
header: _t('tableEditor.columns', { defaultMessage: 'Columns' }),
slot: 0,
sortable: true,
},
{
fieldName: 'indexType',
header: _t('tableEditor.indexType', { defaultMessage: 'Type' }),
slot: 1,
},
isWritable
? {
fieldName: 'actions',
slot: 2,
}
: null,
]}
>
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
<svelte:fragment slot="1" let:row>{getIndexTypeLabel(row)}</svelte:fragment>
<svelte:fragment slot="2" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteConstraint(tbl, row));
}}>{_t('common.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
</ObjectListControl>
{/if}
{#if !driver?.dialect?.omitUniqueConstraints}
<ObjectListControl
collection={uniques}
onAddNew={isWritable && columns?.length > 0 ? addUnique : null}
title={_t('tableEditor.uniqueConstraints', {
defaultMessage: 'Unique constraints ({constraintCount})',
values: { constraintCount: uniques?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.nouniquedefined', { defaultMessage: 'No unique defined' }) : null}
clickable
on:clickrow={e => showModal(UniqueEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })}
columns={[
{
fieldName: 'columns',
header: _t('tableEditor.columns', { defaultMessage: 'Columns' }),
slot: 0,
sortable: true,
},
isWritable
? {
fieldName: 'actions',
sortable: true,
slot: 1,
}
: null,
]}
>
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
<svelte:fragment slot="1" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteConstraint(tbl, row));
}}>{_t('common.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
</ObjectListControl>
{/if}
{#if !driver?.dialect?.omitForeignKeys}
<ForeignKeyObjectListControl
collection={foreignKeys}
onAddNew={isWritable && columns?.length > 0 ? addForeignKey : null}
title={_t('tableEditor.foreignKeys', {
defaultMessage: 'Foreign keys ({foreignKeyCount})',
values: { foreignKeyCount: foreignKeys?.length || 0 },
})}
emptyMessage={isWritable
? _t('tableEditor.noforeignkeydefined', { defaultMessage: 'No foreign key defined' })
: null}
clickable
onRemove={row => setTableInfo(tbl => editorDeleteConstraint(tbl, row))}
on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })}
/>
<ForeignKeyObjectListControl
collection={dependencies}
title={_t('tableEditor.dependencies', { defaultMessage: 'Dependencies' })}
/>
{/if}
</div>
<style>
.wrapper {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: var(--theme-content-background);
overflow: auto;
}
</style>
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('TableEditor');
registerCommand({
id: 'tableEditor.addColumn',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addColumn', { defaultMessage: 'Add column' }),
icon: 'icon add-column',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable(),
onClick: () => getCurrentEditor().addColumn(),
});
registerCommand({
id: 'tableEditor.addPrimaryKey',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addPrimaryKey', { defaultMessage: 'Add primary key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.allowAddPrimaryKey(),
onClick: () => getCurrentEditor().addPrimaryKey(),
});
registerCommand({
id: 'tableEditor.addForeignKey',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addForeignKey', { defaultMessage: 'Add foreign key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitForeignKeys,
onClick: () => getCurrentEditor().addForeignKey(),
});
registerCommand({
id: 'tableEditor.addIndex',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addIndex', { defaultMessage: 'Add index' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitIndexes,
onClick: () => getCurrentEditor().addIndex(),
});
registerCommand({
id: 'tableEditor.addUnique',
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addUnique', { defaultMessage: 'Add unique' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.getIsWritable() && !getCurrentEditor()?.getDialect()?.omitUniqueConstraints,
onClick: () => getCurrentEditor().addUnique(),
});
</script>
<script lang="ts">
import { editorDeleteColumn, editorDeleteConstraint } from 'dbgate-tools';
import _ from 'lodash';
import { onMount, tick } from 'svelte';
import invalidateCommands from '../commands/invalidateCommands';
import ColumnLabel from '../elements/ColumnLabel.svelte';
import ConstraintLabel from '../elements/ConstraintLabel.svelte';
import ForeignKeyObjectListControl from '../elements/ForeignKeyObjectListControl.svelte';
import Link from '../elements/Link.svelte';
import ObjectListControl from '../elements/ObjectListControl.svelte';
import { showModal } from '../modals/modalTools';
import useEditorData from '../query/useEditorData';
import createActivator from '../utility/createActivator';
import { useDbCore } from '../utility/metadataLoaders';
import ColumnEditorModal from './ColumnEditorModal.svelte';
import ForeignKeyEditorModal from './ForeignKeyEditorModal.svelte';
import IndexEditorModal from './IndexEditorModal.svelte';
import PrimaryKeyEditorModal from './PrimaryKeyEditorModal.svelte';
import UniqueEditorModal from './UniqueEditorModal.svelte';
import ObjectFieldsEditor from '../elements/ObjectFieldsEditor.svelte';
import PrimaryKeyLikeListControl from './PrimaryKeyLikeListControl.svelte';
import { _t } from '../translations';
export const activator = createActivator('TableEditor', true);
export let tableInfo;
export let setTableInfo;
export let dbInfo;
export let driver;
export let resetCounter;
export let isCreateTable;
export let schemaList;
$: isWritable = !!setTableInfo;
export function getIsWritable() {
return isWritable;
}
export function getDialect() {
return driver?.dialect;
}
export function addColumn() {
showModal(ColumnEditorModal, {
setTableInfo,
tableInfo,
driver,
onAddNext: async () => {
await tick();
addColumn();
},
});
}
export function allowAddPrimaryKey() {
return isWritable && !tableInfo?.primaryKey;
}
export function addPrimaryKey() {
showModal(PrimaryKeyEditorModal, {
setTableInfo,
tableInfo,
driver,
});
}
export function addForeignKey() {
showModal(ForeignKeyEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
});
}
export function addIndex() {
showModal(IndexEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
driver,
});
}
export function addUnique() {
showModal(UniqueEditorModal, {
setTableInfo,
tableInfo,
dbInfo,
});
}
function getIndexTypeLabel(row) {
const indexType = row?.indexType?.toString()?.toUpperCase();
if (indexType === 'FULLTEXT') return 'FULLTEXT';
if (row?.isUnique) return 'UNIQUE';
if (indexType) return indexType;
return 'INDEX';
}
$: columns = tableInfo?.columns;
$: foreignKeys = tableInfo?.foreignKeys;
$: dependencies = tableInfo?.dependencies;
$: indexes = tableInfo?.indexes;
$: uniques = tableInfo?.uniques;
$: {
tableInfo;
invalidateCommands();
}
$: tableFormOptions = driver?.dialect?.getTableFormOptions?.(tableInfo?.objectId ? 'editTableForm' : 'newTableForm');
</script>
<div class="wrapper">
{#if tableInfo && (tableFormOptions || isCreateTable)}
{#key resetCounter}
<ObjectFieldsEditor
title={_t('tableEditor.tableproperties', { defaultMessage: 'Table properties' })}
fieldDefinitions={tableFormOptions ?? []}
pureNameTitle={isCreateTable ? _t('tableEditor.tablename', { defaultMessage: 'Table name' }) : null}
schemaList={isCreateTable && schemaList?.length >= 0 ? schemaList : null}
values={_.pick(tableInfo, ['schemaName', 'pureName', ...(tableFormOptions ?? []).map(x => x.name)])}
onChangeValues={vals => {
if (!_.isEmpty(vals) && setTableInfo) {
setTableInfo(tbl => ({ ...tbl, ...vals }));
}
}}
/>
{/key}
{/if}
<ObjectListControl
collection={columns?.map((x, index) => ({ ...x, ordinal: index + 1 }))}
title={_t('tableEditor.columnsCount', {
defaultMessage: 'Columns ({columnCount})',
values: { columnCount: columns?.length || 0 },
})}
emptyMessage={_t('tableEditor.nocolumnsdefined', { defaultMessage: 'No columns defined' })}
clickable
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })}
onAddNew={isWritable ? addColumn : null}
displayNameFieldName="columnName"
multipleItemsActions={[
{
text: _t('tableEditor.remove', { defaultMessage: 'Remove' }),
icon: 'icon delete',
onClick: selected => {
setTableInfo(tbl => {
const newColumns = tbl.columns.filter(x => !selected.find(y => y.columnName === x.columnName));
return { ...tbl, columns: newColumns };
});
},
},
{
text: _t('tableEditor.copynames', { defaultMessage: 'Copy names' }),
icon: 'icon copy',
onClick: selected => {
const names = selected.map(x => x.columnName).join('\n');
navigator.clipboard.writeText(names);
},
},
{
text: _t('tableEditor.copydefinitions', { defaultMessage: 'Copy definitions' }),
icon: 'icon copy',
onClick: selected => {
const names = selected.map(x => `${x.columnName} ${x.dataType}${x.notNull ? ' NOT NULL' : ''}`).join(',\n');
navigator.clipboard.writeText(names);
},
},
]}
columns={[
!driver?.dialect?.specificNullabilityImplementation && {
fieldName: 'notNull',
header: _t('tableEditor.nullability', { defaultMessage: 'Nullability' }),
sortable: true,
slot: 0,
},
{
fieldName: 'dataType',
header: _t('tableEditor.dataType', { defaultMessage: 'Data type' }),
sortable: true,
filterable: true,
},
{
fieldName: 'defaultValue',
header: _t('tableEditor.defaultValue', { defaultMessage: 'Default value' }),
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isSparse && {
fieldName: 'isSparse',
header: _t('tableEditor.isSparse', { defaultMessage: 'Is Sparse' }),
sortable: true,
slot: 1,
},
{
fieldName: 'computedExpression',
header: _t('tableEditor.computedExpression', { defaultMessage: 'Computed Expression' }),
sortable: true,
filterable: true,
},
driver?.dialect?.columnProperties?.isPersisted && {
fieldName: 'isPersisted',
header: _t('tableEditor.isPersisted', { defaultMessage: 'Is Persisted' }),
sortable: true,
slot: 2,
},
driver?.dialect?.columnProperties?.isUnsigned && {
fieldName: 'isUnsigned',
header: _t('tableEditor.isUnsigned', { defaultMessage: 'Unsigned' }),
sortable: true,
slot: 4,
},
driver?.dialect?.columnProperties?.isZerofill && {
fieldName: 'isZerofill',
header: _t('tableEditor.isZeroFill', { defaultMessage: 'Zero fill' }),
sortable: true,
slot: 5,
},
driver?.dialect?.columnProperties?.columnComment && {
fieldName: 'columnComment',
header: _t('tableEditor.columnComment', { defaultMessage: 'Comment' }),
sortable: true,
filterable: true,
},
isWritable
? {
fieldName: 'actions',
filterable: false,
slot: 3,
}
: null,
]}
>
<svelte:fragment slot="0" let:row
>{row?.notNull
? _t('tableEditor.notnull', { defaultMessage: 'NOT NULL' })
: _t('tableEditor.null', { defaultMessage: 'NULL' })}</svelte:fragment
>
<svelte:fragment slot="1" let:row
>{row?.isSparse
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="2" let:row
>{row?.isPersisted
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="3" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteColumn(tbl, row));
}}>{_t('tableEditor.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
<svelte:fragment slot="4" let:row
>{row?.isUnsigned
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="5" let:row
>{row?.isZerofill
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="name" let:row><ColumnLabel {...row} forceIcon /></svelte:fragment>
</ObjectListControl>
<PrimaryKeyLikeListControl {tableInfo} {setTableInfo} {isWritable} {driver} />
{#if driver?.dialect?.sortingKeys}
<PrimaryKeyLikeListControl
{tableInfo}
{setTableInfo}
{isWritable}
{driver}
constraintLabel="sorting key"
constraintType="sortingKey"
/>
{/if}
{#if !driver?.dialect?.omitIndexes}
<ObjectListControl
collection={indexes}
onAddNew={isWritable && columns?.length > 0 ? addIndex : null}
title={_t('tableEditor.indexes', {
defaultMessage: 'Indexes ({indexCount})',
values: { indexCount: indexes?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.noindexdefined', { defaultMessage: 'No index defined' }) : null}
clickable
on:clickrow={e => showModal(IndexEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, driver })}
columns={[
{
fieldName: 'columns',
header: _t('tableEditor.columns', { defaultMessage: 'Columns' }),
slot: 0,
sortable: true,
},
{
fieldName: 'indexType',
header: _t('tableEditor.indexType', { defaultMessage: 'Type' }),
slot: 1,
},
isWritable
? {
fieldName: 'actions',
slot: 2,
}
: null,
]}
>
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
<svelte:fragment slot="1" let:row>{getIndexTypeLabel(row)}</svelte:fragment>
<svelte:fragment slot="2" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteConstraint(tbl, row));
}}>{_t('common.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
</ObjectListControl>
{/if}
{#if !driver?.dialect?.omitUniqueConstraints}
<ObjectListControl
collection={uniques}
onAddNew={isWritable && columns?.length > 0 ? addUnique : null}
title={_t('tableEditor.uniqueConstraints', {
defaultMessage: 'Unique constraints ({constraintCount})',
values: { constraintCount: uniques?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.nouniquedefined', { defaultMessage: 'No unique defined' }) : null}
clickable
on:clickrow={e => showModal(UniqueEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })}
columns={[
{
fieldName: 'columns',
header: _t('tableEditor.columns', { defaultMessage: 'Columns' }),
slot: 0,
sortable: true,
},
isWritable
? {
fieldName: 'actions',
sortable: true,
slot: 1,
}
: null,
]}
>
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
<svelte:fragment slot="1" let:row
><Link
onClick={e => {
e.stopPropagation();
setTableInfo(tbl => editorDeleteConstraint(tbl, row));
}}>{_t('common.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
</ObjectListControl>
{/if}
{#if !driver?.dialect?.omitForeignKeys}
<ForeignKeyObjectListControl
collection={foreignKeys}
onAddNew={isWritable && columns?.length > 0 ? addForeignKey : null}
title={_t('tableEditor.foreignKeys', {
defaultMessage: 'Foreign keys ({foreignKeyCount})',
values: { foreignKeyCount: foreignKeys?.length || 0 },
})}
emptyMessage={isWritable
? _t('tableEditor.noforeignkeydefined', { defaultMessage: 'No foreign key defined' })
: null}
clickable
onRemove={row => setTableInfo(tbl => editorDeleteConstraint(tbl, row))}
on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })}
/>
<ForeignKeyObjectListControl
collection={dependencies}
title={_t('tableEditor.dependencies', { defaultMessage: 'Dependencies' })}
/>
{/if}
</div>
<style>
.wrapper {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: var(--theme-content-background);
overflow: auto;
}

View File

@@ -2,7 +2,7 @@
import TabRegister from './TabRegister.svelte';
import TabsPanel from './TabsPanel.svelte';
import _ from 'lodash';
import { currentDatabase, lockedDatabaseMode, openedTabs, TabDefinition } from '../stores';
import { currentDatabase, lockedDatabaseMode, openedTabs, type TabDefinition } from '../stores';
import { shouldShowTab } from './TabsPanel.svelte';
export let multiTabIndex;

View File

@@ -1,4 +1,14 @@
<script lang="ts" context="module">
import { getLockedDatabaseMode, getCurrentDatabase, getOpenedTabs, openedTabs, getActiveTabId, cloudConnectionsStore, getOpenedModals, getActiveTab } from '../stores';
import { showModal } from '../modals/modalTools';
import CloseTabModal from '../modals/CloseTabModal.svelte';
import _ from 'lodash';
import { getConnectionLabel } from 'dbgate-tools';
import { setSelectedTab } from '../utility/common';
import registerCommand from '../commands/registerCommand';
import { isElectronAvailable } from '../utility/getElectron';
import FavoriteModal from '../modals/FavoriteModal.svelte';
import { __t, _t } from '../translations';
const getCurrentValueMarker: any = {};
export function shouldShowTab(tab, lockedDbModeArg = getCurrentValueMarker, currentDbArg = getCurrentValueMarker) {
@@ -319,50 +329,25 @@
</script>
<script lang="ts">
import _ from 'lodash';
import { tick } from 'svelte';
import registerCommand from '../commands/registerCommand';
import FontIcon from '../icons/FontIcon.svelte';
import FavoriteModal from '../modals/FavoriteModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import appObjectTypes from '../appobj';
import {
currentDatabase,
getActiveTab,
getOpenedTabs,
openedTabs,
activeTabId,
getActiveTabId,
getCurrentDatabase,
lockedDatabaseMode,
getLockedDatabaseMode,
draggingDbGroup,
draggingDbGroupTarget,
draggingTab,
draggingTabTarget,
getOpenedModals,
cloudConnectionsStore,
} from '../stores';
import { currentDatabase, activeTabId, lockedDatabaseMode, draggingDbGroup, draggingDbGroupTarget, draggingTab, draggingTabTarget } from '../stores';
import tabs from '../tabs';
import { setSelectedTab, switchCurrentDatabase } from '../utility/common';
import { switchCurrentDatabase } from '../utility/common';
import contextMenu from '../utility/contextMenu';
import { isElectronAvailable } from '../utility/getElectron';
import { getConnectionInfo, useConnectionList, useSettings } from '../utility/metadataLoaders';
import { duplicateTab, getTabDbKey, sortTabs, groupTabs } from '../utility/openNewTab';
import { useConnectionColorFactory } from '../utility/useConnectionColor';
import TabCloseButton from '../elements/TabCloseButton.svelte';
import CloseTabModal from '../modals/CloseTabModal.svelte';
import SwitchDatabaseModal from '../modals/SwitchDatabaseModal.svelte';
import { getConnectionLabel } from 'dbgate-tools';
import { handleAfterTabClick } from '../utility/changeCurrentDbByTab';
import { getBoolSettingsValue } from '../settings/settingsTools';
import NewObjectModal from '../modals/NewObjectModal.svelte';
import { isProApp } from '../utility/proTools';
import { openWebLink } from '../utility/simpleTools';
import { __t, _t } from '../translations';
export let multiTabIndex;
export let shownTab;

View File

@@ -1,200 +1,198 @@
<script lang="ts" context="module">
export const matchingProps = ['archiveFile', 'archiveFolder', 'jslid'];
const getCurrentEditor = () => getActiveComponent('ArchiveFileTab');
registerCommand({
id: 'archiveFile.save',
group: 'save',
category: __t('command.archiveFile', { defaultMessage: 'Archive file' }),
name: __t('command.archiveFile.save', { defaultMessage: 'Save' }),
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),
});
registerCommand({
id: 'archiveFile.saveAs',
category: __t('command.archiveFile', { defaultMessage: 'Archive file' }),
name: __t('command.archiveFile.saveAs', { defaultMessage: 'Save as' }),
icon: 'icon save',
isRelatedToTab: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().saveAs(),
});
</script>
<script lang="ts">
import { changeSetContainsChanges, createChangeSet } from 'dbgate-datalib';
import localforage from 'localforage';
import { onMount, tick } from 'svelte';
import _ from 'lodash';
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
import invalidateCommands from '../commands/invalidateCommands';
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import runCommand from '../commands/runCommand';
import JslDataGrid from '../datagrid/JslDataGrid.svelte';
import { showModal } from '../modals/modalTools';
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
import useEditorData from '../query/useEditorData';
import { apiCall } from '../utility/api';
import { changeTab, markTabSaved, markTabUnsaved, sleep } from '../utility/common';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createUndoReducer from '../utility/createUndoReducer';
import { __t } from '../translations';
export const activator = createActivator('ArchiveFileTab', true);
export let archiveFolder = undefined;
export let archiveFile = undefined;
export let jslid = undefined;
export let tabid;
let infoLoadCounter = 0;
let jslidChecked = false;
let extractedJslId = null;
const quickExportHandlerRef = createQuickExportHandlerRef();
const { editorState, editorValue, setEditorData } = useEditorData({
tabid,
onInitialData: value => {
dispatchChangeSet({ type: 'reset', value });
invalidateCommands();
if (changeSetContainsChanges(value)) {
markTabUnsaved(tabid);
}
},
});
const [changeSetStore, dispatchChangeSet] = createUndoReducer(createChangeSet());
$: {
setEditorData($changeSetStore.value);
if (changeSetContainsChanges($changeSetStore?.value)) {
markTabUnsaved(tabid);
} else {
markTabSaved(tabid);
}
}
export function saveAs() {
showModal(SaveArchiveModal, {
folder: archiveFolder,
file: archiveFile,
onSave: doSaveAs,
});
}
const doSaveAs = async (folder, file) => {
await apiCall('archive/save-jsl-data', {
folder,
file,
jslid: jslid || `archive://${archiveFolder}/${archiveFile}`,
changeSet: changeSetContainsChanges($changeSetStore?.value) ? $changeSetStore.value : null,
});
changeTab(tabid, tab => ({
...tab,
title: file,
props: { archiveFile: file, archiveFolder: folder },
archiveFile: file,
archiveFolder: folder,
}));
if (changeSetContainsChanges($changeSetStore?.value)) {
await sleep(100);
afterSaveChangeSet();
}
};
async function afterSaveChangeSet() {
const structureChanged = !!$changeSetStore.value?.structure;
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
if (structureChanged) {
infoLoadCounter += 1;
}
await tick();
runCommand('dataGrid.refresh');
}
export async function save() {
if (jslid) {
saveAs();
} else {
await apiCall('archive/modify-file', {
folder: archiveFolder,
file: archiveFile,
changeSet: {
...$changeSetStore.value,
updates: $changeSetStore.value.updates.map(update => ({
...update,
fields: _.mapValues(update.fields, (v, k) => (v === undefined ? { $$undefined$$: true } : v)),
})),
},
});
await afterSaveChangeSet();
}
}
export function canSave() {
return jslid || changeSetContainsChanges($changeSetStore?.value);
}
async function checkJslid() {
if (jslid) {
if (!(await apiCall('jsldata/exists', { jslid }))) {
const rows = await localforage.getItem(`tabdata_rows_${tabid}`);
if (rows) {
await apiCall('jsldata/save-rows', { jslid, rows });
}
}
}
if (archiveFolder?.endsWith('.zip')) {
const resp = await apiCall('jsldata/download-jsl-data', {
uri: `zip://archive:${archiveFolder}//${archiveFile}.jsonl`,
});
extractedJslId = resp.jslid;
}
jslidChecked = true;
}
onMount(() => {
checkJslid();
});
</script>
<ToolStripContainer>
{#if jslidChecked || !jslid}
<JslDataGrid
jslid={extractedJslId || jslid || `archive://${archiveFolder}/${archiveFile}`}
supportsReload
allowChangeChangeSetStructure
changeSetState={$changeSetStore}
focusOnVisible
{changeSetStore}
{dispatchChangeSet}
{infoLoadCounter}
/>
{/if}
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="dataGrid.refresh" />
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
<ToolStripCommandButton command="dataGrid.revertAllChanges" hideDisabled />
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled />
<ToolStripCommandButton command="dataGrid.deleteSelectedRows" hideDisabled />
<ToolStripCommandButton command="dataGrid.addNewColumn" hideDisabled />
<ToolStripCommandButton command="archiveFile.save" />
<ToolStripCommandButton command="archiveFile.saveAs" />
</svelte:fragment>
</ToolStripContainer>
export const matchingProps = ['archiveFile', 'archiveFolder', 'jslid'];
const getCurrentEditor = () => getActiveComponent('ArchiveFileTab');
registerCommand({
id: 'archiveFile.save',
group: 'save',
category: __t('command.archiveFile', { defaultMessage: 'Archive file' }),
name: __t('command.archiveFile.save', { defaultMessage: 'Save' }),
toolbar: true,
isRelatedToTab: true,
icon: 'icon save',
testEnabled: () => getCurrentEditor()?.canSave(),
onClick: () => getCurrentEditor().save(),
});
registerCommand({
id: 'archiveFile.saveAs',
category: __t('command.archiveFile', { defaultMessage: 'Archive file' }),
name: __t('command.archiveFile.saveAs', { defaultMessage: 'Save as' }),
icon: 'icon save',
isRelatedToTab: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().saveAs(),
});
</script>
<script lang="ts">
import { changeSetContainsChanges, createChangeSet } from 'dbgate-datalib';
import localforage from 'localforage';
import { onMount, tick } from 'svelte';
import _ from 'lodash';
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
import invalidateCommands from '../commands/invalidateCommands';
import runCommand from '../commands/runCommand';
import JslDataGrid from '../datagrid/JslDataGrid.svelte';
import { showModal } from '../modals/modalTools';
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
import useEditorData from '../query/useEditorData';
import { apiCall } from '../utility/api';
import { changeTab, markTabSaved, markTabUnsaved, sleep } from '../utility/common';
import createActivator from '../utility/createActivator';
import createUndoReducer from '../utility/createUndoReducer';
export const activator = createActivator('ArchiveFileTab', true);
export let archiveFolder = undefined;
export let archiveFile = undefined;
export let jslid = undefined;
export let tabid;
let infoLoadCounter = 0;
let jslidChecked = false;
let extractedJslId = null;
const quickExportHandlerRef = createQuickExportHandlerRef();
const { editorState, editorValue, setEditorData } = useEditorData({
tabid,
onInitialData: value => {
dispatchChangeSet({ type: 'reset', value });
invalidateCommands();
if (changeSetContainsChanges(value)) {
markTabUnsaved(tabid);
}
},
});
const [changeSetStore, dispatchChangeSet] = createUndoReducer(createChangeSet());
$: {
setEditorData($changeSetStore.value);
if (changeSetContainsChanges($changeSetStore?.value)) {
markTabUnsaved(tabid);
} else {
markTabSaved(tabid);
}
}
export function saveAs() {
showModal(SaveArchiveModal, {
folder: archiveFolder,
file: archiveFile,
onSave: doSaveAs,
});
}
const doSaveAs = async (folder, file) => {
await apiCall('archive/save-jsl-data', {
folder,
file,
jslid: jslid || `archive://${archiveFolder}/${archiveFile}`,
changeSet: changeSetContainsChanges($changeSetStore?.value) ? $changeSetStore.value : null,
});
changeTab(tabid, tab => ({
...tab,
title: file,
props: { archiveFile: file, archiveFolder: folder },
archiveFile: file,
archiveFolder: folder,
}));
if (changeSetContainsChanges($changeSetStore?.value)) {
await sleep(100);
afterSaveChangeSet();
}
};
async function afterSaveChangeSet() {
const structureChanged = !!$changeSetStore.value?.structure;
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
if (structureChanged) {
infoLoadCounter += 1;
}
await tick();
runCommand('dataGrid.refresh');
}
export async function save() {
if (jslid) {
saveAs();
} else {
await apiCall('archive/modify-file', {
folder: archiveFolder,
file: archiveFile,
changeSet: {
...$changeSetStore.value,
updates: $changeSetStore.value.updates.map(update => ({
...update,
fields: _.mapValues(update.fields, (v, k) => (v === undefined ? { $$undefined$$: true } : v)),
})),
},
});
await afterSaveChangeSet();
}
}
export function canSave() {
return jslid || changeSetContainsChanges($changeSetStore?.value);
}
async function checkJslid() {
if (jslid) {
if (!(await apiCall('jsldata/exists', { jslid }))) {
const rows = await localforage.getItem(`tabdata_rows_${tabid}`);
if (rows) {
await apiCall('jsldata/save-rows', { jslid, rows });
}
}
}
if (archiveFolder?.endsWith('.zip')) {
const resp = await apiCall('jsldata/download-jsl-data', {
uri: `zip://archive:${archiveFolder}//${archiveFile}.jsonl`,
});
extractedJslId = resp.jslid;
}
jslidChecked = true;
}
onMount(() => {
checkJslid();
});
</script>
<ToolStripContainer>
{#if jslidChecked || !jslid}
<JslDataGrid
jslid={extractedJslId || jslid || `archive://${archiveFolder}/${archiveFile}`}
supportsReload
allowChangeChangeSetStructure
changeSetState={$changeSetStore}
focusOnVisible
{changeSetStore}
{dispatchChangeSet}
{infoLoadCounter}
/>
{/if}
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="dataGrid.refresh" />
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
<ToolStripCommandButton command="dataGrid.revertAllChanges" hideDisabled />
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled />
<ToolStripCommandButton command="dataGrid.deleteSelectedRows" hideDisabled />
<ToolStripCommandButton command="dataGrid.addNewColumn" hideDisabled />
<ToolStripCommandButton command="archiveFile.save" />
<ToolStripCommandButton command="archiveFile.saveAs" />

View File

@@ -1,4 +1,7 @@
<script lang="ts" context="module">
import { getActiveComponent } from '../utility/createActivator';
import registerCommand from '../commands/registerCommand';
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('CollectionDataTab');
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName'];
@@ -39,11 +42,10 @@
import { useCollectionInfo, useConnectionInfo, useSettings } from '../utility/metadataLoaders';
import { extensions } from '../stores';
import CollectionJsonView from '../formview/CollectionJsonView.svelte';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import { showModal } from '../modals/modalTools';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
import registerCommand from '../commands/registerCommand';
import { registerMenu } from '../utility/contextMenu';
import { setContext } from 'svelte';
import _ from 'lodash';
@@ -56,8 +58,6 @@
import useEditorData from '../query/useEditorData';
import { markTabSaved, markTabUnsaved } from '../utility/common';
import { getNumberIcon } from '../icons/FontIcon.svelte';
import { __t } from '../translations';
export let tabid;
export let conid;
export let database;
@@ -219,8 +219,13 @@
<ToolStripCommandButton command="dataGrid.switchToJson" hideDisabled />
<ToolStripCommandButton command="dataGrid.switchToTable" hideDisabled />
<ToolStripExportButton {quickExportHandlerRef} command="collectionDataGrid.export" />
<ToolStripCommandButton command="dataGrid.fetchAll" hideDisabled />
<ToolStripCommandButton command="collectionJsonView.expandAll" hideDisabled />
<ToolStripCommandButton command="collectionJsonView.collapseAll" hideDisabled />
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled data-testid="CollectionDataTab_toggleCellDataView" />
<ToolStripCommandButton
command="dataGrid.toggleCellDataView"
hideDisabled
data-testid="CollectionDataTab_toggleCellDataView"
/>
</svelte:fragment>
</ToolStripContainer>

View File

@@ -68,6 +68,12 @@
$: driver = $extensions.drivers.find(x => x.engine == engine);
$: config = useConfig();
$: showConnectionFieldArgs = { config: $config };
$: showAdvancedTab =
driver?.showConnectionField?.('allowedDatabases', $values, showConnectionFieldArgs) === true ||
driver?.showConnectionField?.('httpProxyUrl', $values, showConnectionFieldArgs) === true ||
!!driver?.getAdvancedConnectionFields?.();
const testIdRef = createRef(0);
function handleTest(requestDbList = false) {
@@ -97,7 +103,7 @@
if (resp?.missingCredentials && resp?.detail?.redirectToDbLogin) {
// Keep isTesting = true, wait for the event
const eventName = `connection-test-result-${connection._id}`;
const handleTestResult = (result) => {
const handleTestResult = result => {
if (testIdRef.get() != testid) {
apiOff(eventName, handleTestResult);
return;
@@ -336,7 +342,7 @@
props: { isFormReadOnly },
testid: 'ConnectionTab_tabSsl',
},
{
showAdvancedTab && {
label: _t('common.advanced', { defaultMessage: 'Advanced' }),
component: ConnectionAdvancedDriverFields,
props: { isFormReadOnly },

View File

@@ -1,4 +1,7 @@
<script lang="ts" context="module">
import { getActiveComponent } from '../utility/createActivator';
import { registerFileCommands } from '../commands/stdCommands';
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('DiagramTab');
registerFileCommands({
@@ -16,10 +19,9 @@
<script lang="ts">
import useEditorData from '../query/useEditorData';
import { registerFileCommands } from '../commands/stdCommands';
import createUndoReducer from '../utility/createUndoReducer';
import _ from 'lodash';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import DiagramDesigner from '../designer/DiagramDesigner.svelte';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
@@ -30,11 +32,11 @@
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
import ToolStripButton from '../buttons/ToolStripButton.svelte';
import ToolStripDropDownButton from '../buttons/ToolStripDropDownButton.svelte';
import DiagramSettings from '../designer/DiagramSettings.svelte';
import { derived } from 'svelte/store';
import { isProApp } from '../utility/proTools';
import { __t } from '../translations';
import { _t } from '../translations';
export let tabid;
export let conid;
export let database;
@@ -160,7 +162,11 @@
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="designer.arrange" />
<ToolStripSaveButton idPrefix="diagram" />
<ToolStripCommandButton command="diagram.export" />
<ToolStripDropDownButton
icon="icon report"
label={_t('common.export', { defaultMessage: 'Export' })}
menu={[{ command: 'diagram.export' }, { command: 'diagram.exportPng' }]}
/>
<ToolStripCommandButton command="diagram.undo" />
<ToolStripCommandButton command="diagram.redo" />
<ToolStripCommandButton command="diagram.deleteSelectedTables" />

View File

@@ -1,4 +1,8 @@
<script lang="ts" context="module">
import { getActiveComponent } from '../utility/createActivator';
import { registerFileCommands } from '../commands/stdCommands';
import registerCommand from '../commands/registerCommand';
import { __t } from '../translations';
const getCurrentEditor = () => getActiveComponent('FavoriteEditorTab');
registerFileCommands({
@@ -32,19 +36,14 @@
<script lang="ts">
import { getContext } from 'svelte';
import registerCommand from '../commands/registerCommand';
import { registerFileCommands } from '../commands/stdCommands';
import AceEditor from '../query/AceEditor.svelte';
import useEditorData from '../query/useEditorData';
import invalidateCommands from '../commands/invalidateCommands';
import { showModal } from '../modals/modalTools';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { openFavorite } from '../appobj/FavoriteFileAppObject.svelte';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import createActivator from '../utility/createActivator';
import { apiCall } from '../utility/api';
import { __t } from '../translations';
export let tabid;
export let savedFile;

Some files were not shown because too many files have changed in this diff Show More