Compare commits
154 Commits
v7.1.0
...
feature/co
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137fc6b928 | ||
|
|
9d1d7b7e34 | ||
|
|
588cd39d7c | ||
|
|
79ebfa9b7a | ||
|
|
0c6b2746d1 | ||
|
|
978972c55c | ||
|
|
37854fc577 | ||
|
|
5537e193a6 | ||
|
|
0d42b2b133 | ||
|
|
44bd7972d4 | ||
|
|
5143eb39f7 | ||
|
|
cf51883b3e | ||
|
|
484ca0c78a | ||
|
|
8f5cad0e2c | ||
|
|
988512a571 | ||
|
|
f8bd380051 | ||
|
|
281131dbba | ||
|
|
ea3a61077a | ||
|
|
d1a898b40d | ||
|
|
a521a81ef0 | ||
|
|
2505c61975 | ||
|
|
ab5a54dbb6 | ||
|
|
44ad8fa60a | ||
|
|
5b27a241d7 | ||
|
|
084019ca65 | ||
|
|
ba147af8fe | ||
|
|
1b3f4db07d | ||
|
|
c36705d458 | ||
|
|
0e126cb8cf | ||
|
|
c48183a539 | ||
|
|
50f380dbbe | ||
|
|
66023a9a68 | ||
|
|
c3fbc3354c | ||
|
|
a7d2ed11f3 | ||
|
|
6a3dc92572 | ||
|
|
e3a4667422 | ||
|
|
c4dd99bba9 | ||
|
|
588b6f9882 | ||
|
|
375f69ca1e | ||
|
|
a32e5cc139 | ||
|
|
8e00137751 | ||
|
|
003db50833 | ||
|
|
bc519c2c20 | ||
|
|
3b41fa8cfa | ||
|
|
39ed0f6d2d | ||
|
|
710f796832 | ||
|
|
9ec5fb7263 | ||
|
|
407db457d5 | ||
|
|
0c5d2cfcd1 | ||
|
|
87ace375bb | ||
|
|
d010020f3b | ||
|
|
c60227a98f | ||
|
|
2824681bff | ||
|
|
073a3e3946 | ||
|
|
93e91127a0 | ||
|
|
b60a6cff56 | ||
|
|
1f3b1963d9 | ||
|
|
4915f57abb | ||
|
|
97c6fc97d5 | ||
|
|
b68421bbc3 | ||
|
|
2d10559754 | ||
|
|
b398a7b546 | ||
|
|
1711d2102d | ||
|
|
97cea230f3 | ||
|
|
b6a0fe9465 | ||
|
|
06c50659bb | ||
|
|
244b47f548 | ||
|
|
b72a244d93 | ||
|
|
c1e069d4dc | ||
|
|
f99994085a | ||
|
|
32fd0dd78c | ||
|
|
a557b6b2b4 | ||
|
|
e84583c776 | ||
|
|
a548b0d543 | ||
|
|
de94f15383 | ||
|
|
7045d986ef | ||
|
|
de7ae9cf09 | ||
|
|
ab3d6888dc | ||
|
|
98a70891f3 | ||
|
|
52e7326a2c | ||
|
|
bfd2e3b07a | ||
|
|
799f5e30d3 | ||
|
|
d3e544c3c0 | ||
|
|
866fd55834 | ||
|
|
74ce1fba32 | ||
|
|
a11b93b4cc | ||
|
|
066f2baa03 | ||
|
|
e02396280f | ||
|
|
a654c80746 | ||
|
|
3b50f4bd7c | ||
|
|
cc1f77f5bc | ||
|
|
381fce4a82 | ||
|
|
bc3be97cee | ||
|
|
1c389208a7 | ||
|
|
cbeed2d3d0 | ||
|
|
3d974ad144 | ||
|
|
749042a05d | ||
|
|
52413b82ee | ||
|
|
212a7ec083 | ||
|
|
cee94fe113 | ||
|
|
e1ead2519a | ||
|
|
80330a25ac | ||
|
|
508470e970 | ||
|
|
bc64b4b5c7 | ||
|
|
48d8494ead | ||
|
|
2a51d2ed96 | ||
|
|
cfabcc7bf6 | ||
|
|
90fc8fd0fc | ||
|
|
ff54533e33 | ||
|
|
2072f0b5ba | ||
|
|
6efc720a45 | ||
|
|
c7cb1efe9c | ||
|
|
e193531246 | ||
|
|
2aa53f414e | ||
|
|
843c15d754 | ||
|
|
fb19582088 | ||
|
|
8040466cbe | ||
|
|
302b4d7acd | ||
|
|
a8ccc24d46 | ||
|
|
b2fb071a7b | ||
|
|
204d7b97d5 | ||
|
|
f3da709aac | ||
|
|
0ab8afb838 | ||
|
|
d50999547f | ||
|
|
04741b0eba | ||
|
|
ba86fe32e7 | ||
|
|
9deb7d7fdc | ||
|
|
55eb64e5ca | ||
|
|
a5f50f3f2b | ||
|
|
47214eb5b3 | ||
|
|
599509d417 | ||
|
|
9d366fc359 | ||
|
|
0e1ed0bde6 | ||
|
|
6ad7824bf2 | ||
|
|
1174f51c07 | ||
|
|
1950dda1ab | ||
|
|
8231b6d5be | ||
|
|
0feacbe6eb | ||
|
|
80b5f5adca | ||
|
|
13650f36e6 | ||
|
|
3f58d99069 | ||
|
|
0c8a025cf6 | ||
|
|
5014df4859 | ||
|
|
34a491e2ef | ||
|
|
884e4ca88e | ||
|
|
a670c5e86c | ||
|
|
af1fba79be | ||
|
|
ac44de0bf4 | ||
|
|
f013a241ce | ||
|
|
0e29a7206d | ||
|
|
689b3f299c | ||
|
|
02ccb990bd | ||
|
|
61fe4f0d57 | ||
|
|
0a920195d5 |
2
.github/workflows/build-app-pro-beta.yaml
vendored
2
.github/workflows/build-app-pro-beta.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-app-pro.yaml
vendored
2
.github/workflows/build-app-pro.yaml
vendored
@@ -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
|
||||
|
||||
20
.github/workflows/build-cloud-pro.yaml
vendored
20
.github/workflows/build-cloud-pro.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-docker-pro.yaml
vendored
2
.github/workflows/build-docker-pro.yaml
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/build-npm-pro.yaml
vendored
2
.github/workflows/build-npm-pro.yaml
vendored
@@ -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
|
||||
|
||||
5
.github/workflows/build-npm.yaml
vendored
5
.github/workflows/build-npm.yaml
vendored
@@ -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: |
|
||||
|
||||
6
.github/workflows/e2e-pro.yaml
vendored
6
.github/workflows/e2e-pro.yaml
vendored
@@ -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:
|
||||
|
||||
252
CHANGELOG.md
252
CHANGELOG.md
@@ -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
119
CLAUDE.md
Normal 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
|
||||
```
|
||||
@@ -4,5 +4,6 @@ module.exports = {
|
||||
mssql: true,
|
||||
oracle: true,
|
||||
sqlite: true,
|
||||
mongo: true
|
||||
mongo: true,
|
||||
dynamo: true,
|
||||
};
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -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'] });
|
||||
|
||||
105
e2e-tests/cypress/e2e/ai-chat.cy.js
Normal file
105
e2e-tests/cypress/e2e/ai-chat.cy.js
Normal 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');
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
14
e2e-tests/env/ai-chat/.env
vendored
Normal 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
|
||||
8
e2e-tests/env/browse-data/.env
vendored
8
e2e-tests/env/browse-data/.env
vendored
@@ -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
|
||||
|
||||
9
e2e-tests/env/multi-sql/.env
vendored
9
e2e-tests/env/multi-sql/.env
vendored
@@ -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
168
e2e-tests/init/ai-chat.js
Normal 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);
|
||||
});
|
||||
@@ -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')
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
3
e2e-tests/tmpdata/.gitignore
vendored
3
e2e-tests/tmpdata/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
test-api.pid
|
||||
test-api.pid
|
||||
aigwmock.pid
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "7.1.0",
|
||||
"version": "7.1.6",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
14
packages/aigwmock/package.json
Normal file
14
packages/aigwmock/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
202
packages/aigwmock/src/index.js
Normal file
202
packages/aigwmock/src/index.js
Normal 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}`);
|
||||
});
|
||||
193
packages/aigwmock/src/mockResponses.json
Normal file
193
packages/aigwmock/src/mockResponses.json
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -502,6 +502,9 @@ module.exports = {
|
||||
_id: '__model',
|
||||
};
|
||||
}
|
||||
if (!conid) {
|
||||
return null;
|
||||
}
|
||||
await testConnectionPermission(conid, req);
|
||||
return this.getCore({ conid, mask: true });
|
||||
},
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@@ -4,6 +4,7 @@ export interface SqlDialect {
|
||||
rangeSelect?: boolean;
|
||||
limitSelect?: boolean;
|
||||
ilike?: boolean;
|
||||
likeAsFunction?: boolean;
|
||||
rowNumberOverPaging?: boolean;
|
||||
topRecords?: boolean;
|
||||
stringEscapeChar: string;
|
||||
|
||||
5
packages/types/engines.d.ts
vendored
5
packages/types/engines.d.ts
vendored
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() } });
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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(' ')}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 = {};
|
||||
|
||||
@@ -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;
|
||||
|
||||
74
packages/web/src/modals/FetchAllConfirmModal.svelte
Normal file
74
packages/web/src/modals/FetchAllConfirmModal.svelte
Normal 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>
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user