Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 02e3bfaa8a | |||
| c7259e4663 | |||
| 42d1ca8fd4 | |||
| 1cf52d8b39 | |||
| a7cf51bdf7 | |||
| dfdb31e2f8 | |||
| 3508ddc3ca | |||
| 137fc6b928 | |||
| e6f5295420 | |||
| 2bb08921c3 | |||
| ee2d0e4c30 | |||
| c43a838572 | |||
| 17ff6a8013 | |||
| 62ad6a0d08 | |||
| 5c049fa867 | |||
| 619f17114a | |||
| 1c1431014c | |||
| 9d1d7b7e34 | |||
| f68ca1e786 | |||
| 8d16a30064 | |||
| cf601c33c0 | |||
| 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 | |||
| 48d8494ead | |||
| 2a51d2ed96 |
@@ -47,7 +47,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -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: |
|
||||
|
||||
@@ -30,7 +30,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: c33e71a6ddc30d8ce59cf0351e04e08f6be272a3
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- 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:
|
||||
|
||||
@@ -23,26 +23,49 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Checkout dbgate/dbgate-pro
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 6b5e2ff831db9baedb2a43862daa4247810b15de
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
mv dbgate-pro/* ../dbgate-pro/
|
||||
cd ..
|
||||
mkdir dbgate-merged
|
||||
cd dbgate-pro
|
||||
cd sync
|
||||
yarn
|
||||
node sync.js --nowatch
|
||||
cd ..
|
||||
- name: yarn install
|
||||
run: |
|
||||
cd ../dbgate-merged
|
||||
yarn install
|
||||
- name: Integration tests
|
||||
run: |
|
||||
cd ../dbgate-merged
|
||||
cd integration-tests
|
||||
yarn test:ci
|
||||
- name: Filter parser tests
|
||||
if: always()
|
||||
run: |
|
||||
cd ../dbgate-merged
|
||||
cd packages/filterparser
|
||||
yarn test:ci
|
||||
- name: Datalib (perspective) tests
|
||||
if: always()
|
||||
run: |
|
||||
cd ../dbgate-merged
|
||||
cd packages/datalib
|
||||
yarn test:ci
|
||||
- name: Tools tests
|
||||
if: always()
|
||||
run: |
|
||||
cd ../dbgate-merged
|
||||
cd packages/tools
|
||||
yarn test:ci
|
||||
services:
|
||||
@@ -98,3 +121,14 @@ jobs:
|
||||
FIREBIRD_USE_LEGACY_AUTH: true
|
||||
ports:
|
||||
- '3050:3050'
|
||||
mongodb:
|
||||
image: mongo:4.0.12
|
||||
ports:
|
||||
- '27017:27017'
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
- mongo-config:/data/configdb
|
||||
dynamodb:
|
||||
image: amazon/dynamodb-local
|
||||
ports:
|
||||
- '8000:8000'
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# --------------------------------------------------------------------------------------------
|
||||
# This file is generated. Do not edit manually
|
||||
# --------------------------------------------------------------------------------------------
|
||||
name: SAST
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
jobs:
|
||||
sast:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: semgrep/semgrep
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- name: Clone Apiiro malicious code ruleset
|
||||
run: git clone https://github.com/apiiro/malicious-code-ruleset.git /apiiro-ruleset
|
||||
- name: Run Semgrep SAST scan
|
||||
run: semgrep ci --code --config=auto --config /apiiro-ruleset --json > sast-report.json || true
|
||||
- name: Upload SAST report
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sast-report
|
||||
path: sast-report.json
|
||||
+196
-41
@@ -1,14 +1,38 @@
|
||||
# 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)
|
||||
@@ -16,6 +40,7 @@ Builds:
|
||||
- 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
|
||||
@@ -24,10 +49,10 @@ Builds:
|
||||
- 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
|
||||
@@ -37,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
|
||||
@@ -53,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
|
||||
@@ -64,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
|
||||
@@ -74,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
|
||||
@@ -89,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
|
||||
@@ -113,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
|
||||
@@ -127,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
|
||||
@@ -140,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
|
||||
@@ -153,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
|
||||
@@ -167,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)
|
||||
@@ -188,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)
|
||||
@@ -201,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
|
||||
@@ -215,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
|
||||
@@ -224,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
|
||||
@@ -238,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)
|
||||
@@ -247,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)
|
||||
@@ -286,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
|
||||
@@ -296,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
|
||||
@@ -317,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
|
||||
@@ -326,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
|
||||
@@ -338,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)
|
||||
@@ -349,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
|
||||
@@ -361,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
|
||||
@@ -373,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)
|
||||
@@ -394,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
|
||||
@@ -406,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
|
||||
@@ -413,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
|
||||
@@ -429,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)
|
||||
@@ -461,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)
|
||||
@@ -468,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
|
||||
@@ -478,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
|
||||
@@ -489,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
|
||||
@@ -498,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)
|
||||
@@ -528,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)
|
||||
@@ -536,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
|
||||
@@ -560,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
|
||||
@@ -571,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
|
||||
@@ -585,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)
|
||||
@@ -613,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
|
||||
@@ -625,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
|
||||
@@ -632,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
|
||||
@@ -640,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
|
||||
@@ -674,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
|
||||
@@ -686,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
|
||||
@@ -695,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
|
||||
@@ -716,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
|
||||
@@ -725,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
|
||||
@@ -738,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
|
||||
@@ -748,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
|
||||
@@ -756,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
|
||||
@@ -764,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
|
||||
@@ -777,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
|
||||
@@ -786,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
|
||||
@@ -819,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
|
||||
@@ -828,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
|
||||
@@ -835,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
|
||||
@@ -848,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
|
||||
@@ -867,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
|
||||
@@ -875,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
|
||||
@@ -883,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
|
||||
@@ -894,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
|
||||
@@ -903,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
|
||||
@@ -923,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
|
||||
@@ -944,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
|
||||
@@ -952,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
|
||||
@@ -962,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
|
||||
@@ -981,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-)
|
||||
@@ -997,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
|
||||
@@ -1012,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
|
||||
@@ -1043,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
|
||||
@@ -1057,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
|
||||
@@ -1074,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
|
||||
@@ -1083,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
|
||||
@@ -1096,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
|
||||
@@ -1112,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)
|
||||
@@ -1134,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
|
||||
@@ -1163,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
|
||||
@@ -1188,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
|
||||
@@ -1203,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
|
||||
@@ -1213,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
|
||||
@@ -1240,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
|
||||
@@ -1249,4 +1403,5 @@ Builds:
|
||||
- FIX: #62 - import, export executed from SNAP installs didn't work
|
||||
|
||||
### 3.9.5
|
||||
|
||||
- Start point of changelog
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -400,6 +400,14 @@ function createWindow() {
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
|
||||
{ urls: ['https://*.tile.openstreetmap.org/*'] },
|
||||
(details, callback) => {
|
||||
details.requestHeaders['Referer'] = 'https://www.dbgate.io';
|
||||
callback({ requestHeaders: details.requestHeaders });
|
||||
}
|
||||
);
|
||||
|
||||
if (initialConfig['winIsMaximized']) {
|
||||
mainWindow.maximize();
|
||||
}
|
||||
|
||||
@@ -4,5 +4,6 @@ module.exports = {
|
||||
mssql: true,
|
||||
oracle: true,
|
||||
sqlite: true,
|
||||
mongo: true
|
||||
mongo: true,
|
||||
dynamo: true,
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+7
-1
@@ -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
|
||||
|
||||
Vendored
+8
-1
@@ -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
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ function ensureDependencies(dir, checkFile) {
|
||||
env: process.env,
|
||||
});
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`DBGM-00000 Failed to install dependencies in ${dir}`);
|
||||
throw new Error(`DBGM-00297 Failed to install dependencies in ${dir}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ async function waitForReady(url, timeoutMs = 30000) {
|
||||
}
|
||||
await delay(500);
|
||||
}
|
||||
throw new Error(`DBGM-00000 Server at ${url} did not start in time`);
|
||||
throw new Error(`DBGM-00305 Server at ${url} did not start in time`);
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
|
||||
@@ -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,7 @@ 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) {
|
||||
@@ -116,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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,536 @@
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const crypto = require('crypto');
|
||||
const stream = require('stream');
|
||||
const { mongoDbEngine, dynamoDbEngine } = require('../engines');
|
||||
const tableWriter = require('dbgate-api/src/shell/tableWriter');
|
||||
const tableReader = require('dbgate-api/src/shell/tableReader');
|
||||
const copyStream = require('dbgate-api/src/shell/copyStream');
|
||||
|
||||
function randomCollectionName() {
|
||||
return 'test_' + crypto.randomBytes(6).toString('hex');
|
||||
}
|
||||
|
||||
const documentEngines = [
|
||||
{ label: 'MongoDB', engine: mongoDbEngine },
|
||||
{ label: 'DynamoDB', engine: dynamoDbEngine },
|
||||
];
|
||||
|
||||
async function connectEngine(engine) {
|
||||
const driver = requireEngineDriver(engine.connection);
|
||||
const conn = await driver.connect(engine.connection);
|
||||
return { driver, conn };
|
||||
}
|
||||
|
||||
async function createCollection(driver, conn, collectionName, engine) {
|
||||
if (engine.connection.engine.startsWith('dynamodb')) {
|
||||
await driver.operation(conn, {
|
||||
type: 'createCollection',
|
||||
collection: {
|
||||
name: collectionName,
|
||||
partitionKey: '_id',
|
||||
partitionKeyType: 'S',
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await driver.operation(conn, {
|
||||
type: 'createCollection',
|
||||
collection: { name: collectionName },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function dropCollection(driver, conn, collectionName) {
|
||||
try {
|
||||
await driver.operation(conn, {
|
||||
type: 'dropCollection',
|
||||
collection: collectionName,
|
||||
});
|
||||
} catch (e) {
|
||||
// Ignore errors when dropping (collection may not exist)
|
||||
}
|
||||
}
|
||||
|
||||
async function insertDocument(driver, conn, collectionName, doc) {
|
||||
return driver.updateCollection(conn, {
|
||||
inserts: [{ pureName: collectionName, document: {}, fields: doc }],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
});
|
||||
}
|
||||
|
||||
async function readAll(driver, conn, collectionName) {
|
||||
return driver.readCollection(conn, { pureName: collectionName, limit: 1000 });
|
||||
}
|
||||
|
||||
async function updateDocument(driver, conn, collectionName, condition, fields) {
|
||||
return driver.updateCollection(conn, {
|
||||
inserts: [],
|
||||
updates: [{ pureName: collectionName, condition, fields }],
|
||||
deletes: [],
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteDocument(driver, conn, collectionName, condition) {
|
||||
return driver.updateCollection(conn, {
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: [{ pureName: collectionName, condition }],
|
||||
});
|
||||
}
|
||||
|
||||
describe('Collection CRUD', () => {
|
||||
describe.each(documentEngines.map(e => [e.label, e.engine]))('%s', (label, engine) => {
|
||||
let driver;
|
||||
let conn;
|
||||
let collectionName;
|
||||
|
||||
beforeAll(async () => {
|
||||
const result = await connectEngine(engine);
|
||||
driver = result.driver;
|
||||
conn = result.conn;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (conn) {
|
||||
await driver.close(conn);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
collectionName = randomCollectionName();
|
||||
await createCollection(driver, conn, collectionName, engine);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dropCollection(driver, conn, collectionName);
|
||||
});
|
||||
|
||||
// ---- INSERT ----
|
||||
|
||||
test('insert a single document', async () => {
|
||||
const res = await insertDocument(driver, conn, collectionName, {
|
||||
_id: 'doc1',
|
||||
name: 'Alice',
|
||||
age: 30,
|
||||
});
|
||||
expect(res.inserted.length).toBe(1);
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Alice');
|
||||
expect(all.rows[0].age).toBe(30);
|
||||
});
|
||||
|
||||
test('insert multiple documents', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'a1', name: 'Alice' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'a2', name: 'Bob' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'a3', name: 'Charlie' });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(3);
|
||||
const names = all.rows.map(r => r.name).sort();
|
||||
expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
|
||||
});
|
||||
|
||||
test('insert document with nested object', async () => {
|
||||
await insertDocument(driver, conn, collectionName, {
|
||||
_id: 'nested1',
|
||||
name: 'Alice',
|
||||
address: { city: 'Prague', zip: '11000' },
|
||||
});
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].address.city).toBe('Prague');
|
||||
expect(all.rows[0].address.zip).toBe('11000');
|
||||
});
|
||||
|
||||
// ---- READ ----
|
||||
|
||||
test('read from empty collection returns no rows', async () => {
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(0);
|
||||
});
|
||||
|
||||
test('read with limit', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'l1', name: 'A' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'l2', name: 'B' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'l3', name: 'C' });
|
||||
|
||||
const limited = await driver.readCollection(conn, {
|
||||
pureName: collectionName,
|
||||
limit: 2,
|
||||
});
|
||||
expect(limited.rows.length).toBe(2);
|
||||
});
|
||||
|
||||
test('count documents', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'c1', name: 'A' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'c2', name: 'B' });
|
||||
|
||||
const result = await driver.readCollection(conn, {
|
||||
pureName: collectionName,
|
||||
countDocuments: true,
|
||||
});
|
||||
expect(result.count).toBe(2);
|
||||
});
|
||||
|
||||
test('count documents on empty collection returns zero', async () => {
|
||||
const result = await driver.readCollection(conn, {
|
||||
pureName: collectionName,
|
||||
countDocuments: true,
|
||||
});
|
||||
expect(result.count).toBe(0);
|
||||
});
|
||||
|
||||
// ---- UPDATE ----
|
||||
|
||||
test('update an existing document', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'u1', name: 'Alice', age: 25 });
|
||||
|
||||
const res = await updateDocument(driver, conn, collectionName, { _id: 'u1' }, { name: 'Alice Updated' });
|
||||
expect(res.errorMessage).toBeUndefined();
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Alice Updated');
|
||||
});
|
||||
|
||||
test('update does not create new document', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'u2', name: 'Bob' });
|
||||
|
||||
await updateDocument(driver, conn, collectionName, { _id: 'nonexistent' }, { name: 'Ghost' });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Bob');
|
||||
});
|
||||
|
||||
test('update only specified fields', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'u3', name: 'Carol', age: 40, city: 'London' });
|
||||
|
||||
await updateDocument(driver, conn, collectionName, { _id: 'u3' }, { age: 41 });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Carol');
|
||||
expect(all.rows[0].age).toBe(41);
|
||||
expect(all.rows[0].city).toBe('London');
|
||||
});
|
||||
|
||||
// ---- DELETE ----
|
||||
|
||||
test('delete an existing document', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'd1', name: 'Alice' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'd2', name: 'Bob' });
|
||||
|
||||
const res = await deleteDocument(driver, conn, collectionName, { _id: 'd1' });
|
||||
expect(res.errorMessage).toBeUndefined();
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Bob');
|
||||
});
|
||||
|
||||
test('delete non-existing document does not affect collection', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'dx1', name: 'Alice' });
|
||||
|
||||
await deleteDocument(driver, conn, collectionName, { _id: 'nonexistent' });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Alice');
|
||||
});
|
||||
|
||||
test('delete all documents leaves empty collection', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'da1', name: 'A' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'da2', name: 'B' });
|
||||
|
||||
await deleteDocument(driver, conn, collectionName, { _id: 'da1' });
|
||||
await deleteDocument(driver, conn, collectionName, { _id: 'da2' });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(0);
|
||||
});
|
||||
|
||||
// ---- EDGE CASES ----
|
||||
|
||||
test('insert and read document with empty string field', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'e1', name: '', value: 'test' });
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('');
|
||||
expect(all.rows[0].value).toBe('test');
|
||||
});
|
||||
|
||||
test('insert and read document with numeric values', async () => {
|
||||
await insertDocument(driver, conn, collectionName, {
|
||||
_id: 'n1',
|
||||
intVal: 42,
|
||||
floatVal: 3.14,
|
||||
zero: 0,
|
||||
negative: -10,
|
||||
});
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].intVal).toBe(42);
|
||||
expect(all.rows[0].floatVal).toBeCloseTo(3.14);
|
||||
expect(all.rows[0].zero).toBe(0);
|
||||
expect(all.rows[0].negative).toBe(-10);
|
||||
});
|
||||
|
||||
test('insert and read document with boolean values', async () => {
|
||||
await insertDocument(driver, conn, collectionName, {
|
||||
_id: 'b1',
|
||||
active: true,
|
||||
deleted: false,
|
||||
});
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].active).toBe(true);
|
||||
expect(all.rows[0].deleted).toBe(false);
|
||||
});
|
||||
|
||||
test('reading non-existing collection returns error or empty', async () => {
|
||||
const result = await driver.readCollection(conn, {
|
||||
pureName: 'nonexistent_collection_' + crypto.randomBytes(4).toString('hex'),
|
||||
limit: 10,
|
||||
});
|
||||
// Depending on the driver, this may return an error or empty rows
|
||||
if (result.errorMessage) {
|
||||
expect(typeof result.errorMessage).toBe('string');
|
||||
} else {
|
||||
expect(result.rows.length).toBe(0);
|
||||
}
|
||||
});
|
||||
|
||||
test('replace full document via update with document field', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'r1', name: 'Original', extra: 'data' });
|
||||
|
||||
await driver.updateCollection(conn, {
|
||||
inserts: [],
|
||||
updates: [
|
||||
{
|
||||
pureName: collectionName,
|
||||
condition: { _id: 'r1' },
|
||||
document: { _id: 'r1', name: 'Replaced' },
|
||||
fields: {},
|
||||
},
|
||||
],
|
||||
deletes: [],
|
||||
});
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].name).toBe('Replaced');
|
||||
});
|
||||
|
||||
test('insert then update then delete lifecycle', async () => {
|
||||
// Insert
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'life1', name: 'Lifecycle', status: 'created' });
|
||||
let all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(1);
|
||||
expect(all.rows[0].status).toBe('created');
|
||||
|
||||
// Update
|
||||
await updateDocument(driver, conn, collectionName, { _id: 'life1' }, { status: 'updated' });
|
||||
all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows[0].status).toBe('updated');
|
||||
|
||||
// Delete
|
||||
await deleteDocument(driver, conn, collectionName, { _id: 'life1' });
|
||||
all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createDocumentImportStream(documents) {
|
||||
const pass = new stream.PassThrough({ objectMode: true });
|
||||
pass.write({ __isStreamHeader: true, __isDynamicStructure: true });
|
||||
for (const doc of documents) {
|
||||
pass.write(doc);
|
||||
}
|
||||
pass.end();
|
||||
return pass;
|
||||
}
|
||||
|
||||
function createExportStream() {
|
||||
const writable = new stream.Writable({ objectMode: true });
|
||||
writable.resultArray = [];
|
||||
writable._write = (chunk, encoding, callback) => {
|
||||
writable.resultArray.push(chunk);
|
||||
callback();
|
||||
};
|
||||
return writable;
|
||||
}
|
||||
|
||||
describe('Collection Import/Export', () => {
|
||||
describe.each(documentEngines.map(e => [e.label, e.engine]))('%s', (label, engine) => {
|
||||
let driver;
|
||||
let conn;
|
||||
let collectionName;
|
||||
|
||||
beforeAll(async () => {
|
||||
const result = await connectEngine(engine);
|
||||
driver = result.driver;
|
||||
conn = result.conn;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (conn) {
|
||||
await driver.close(conn);
|
||||
}
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
collectionName = randomCollectionName();
|
||||
await createCollection(driver, conn, collectionName, engine);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await dropCollection(driver, conn, collectionName);
|
||||
});
|
||||
|
||||
test('import documents via stream', async () => {
|
||||
const documents = [
|
||||
{ _id: 'imp1', name: 'Alice', age: 30 },
|
||||
{ _id: 'imp2', name: 'Bob', age: 25 },
|
||||
{ _id: 'imp3', name: 'Charlie', age: 35 },
|
||||
];
|
||||
|
||||
const reader = createDocumentImportStream(documents);
|
||||
const writer = await tableWriter({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
createIfNotExists: true,
|
||||
});
|
||||
await copyStream(reader, writer);
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(3);
|
||||
const names = all.rows.map(r => r.name).sort();
|
||||
expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
|
||||
});
|
||||
|
||||
test('export documents via stream', async () => {
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'exp1', name: 'Alice', city: 'Prague' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'exp2', name: 'Bob', city: 'Vienna' });
|
||||
await insertDocument(driver, conn, collectionName, { _id: 'exp3', name: 'Charlie', city: 'Berlin' });
|
||||
|
||||
const reader = await tableReader({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
});
|
||||
const writer = createExportStream();
|
||||
await copyStream(reader, writer);
|
||||
|
||||
const rows = writer.resultArray.filter(x => !x.__isStreamHeader);
|
||||
expect(rows.length).toBe(3);
|
||||
const names = rows.map(r => r.name).sort();
|
||||
expect(names).toEqual(['Alice', 'Bob', 'Charlie']);
|
||||
});
|
||||
|
||||
test('import then export round-trip', async () => {
|
||||
const documents = [
|
||||
{ _id: 'rt1', name: 'Alice', value: 100 },
|
||||
{ _id: 'rt2', name: 'Bob', value: 200 },
|
||||
{ _id: 'rt3', name: 'Charlie', value: 300 },
|
||||
{ _id: 'rt4', name: 'Diana', value: 400 },
|
||||
];
|
||||
|
||||
// Import
|
||||
const importReader = createDocumentImportStream(documents);
|
||||
const importWriter = await tableWriter({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
createIfNotExists: true,
|
||||
});
|
||||
await copyStream(importReader, importWriter);
|
||||
|
||||
// Export
|
||||
const exportReader = await tableReader({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
});
|
||||
const exportWriter = createExportStream();
|
||||
await copyStream(exportReader, exportWriter);
|
||||
|
||||
const rows = exportWriter.resultArray.filter(x => !x.__isStreamHeader);
|
||||
expect(rows.length).toBe(4);
|
||||
|
||||
const sortedRows = rows.sort((a, b) => a._id.localeCompare(b._id));
|
||||
for (const doc of documents) {
|
||||
const found = sortedRows.find(r => r._id === doc._id);
|
||||
expect(found).toBeDefined();
|
||||
expect(found.name).toBe(doc.name);
|
||||
expect(found.value).toBe(doc.value);
|
||||
}
|
||||
});
|
||||
|
||||
test('import documents with nested objects', async () => {
|
||||
const documents = [
|
||||
{ _id: 'nest1', name: 'Alice', address: { city: 'Prague', zip: '11000' } },
|
||||
{ _id: 'nest2', name: 'Bob', address: { city: 'Vienna', zip: '1010' } },
|
||||
];
|
||||
|
||||
const reader = createDocumentImportStream(documents);
|
||||
const writer = await tableWriter({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
createIfNotExists: true,
|
||||
});
|
||||
await copyStream(reader, writer);
|
||||
|
||||
const all = await readAll(driver, conn, collectionName);
|
||||
expect(all.rows.length).toBe(2);
|
||||
|
||||
const alice = all.rows.find(r => r.name === 'Alice');
|
||||
expect(alice.address.city).toBe('Prague');
|
||||
expect(alice.address.zip).toBe('11000');
|
||||
});
|
||||
|
||||
test('import many documents', async () => {
|
||||
const documents = [];
|
||||
for (let i = 0; i < 150; i++) {
|
||||
documents.push({ _id: `many${i}`, name: `Name${i}`, index: i });
|
||||
}
|
||||
|
||||
const reader = createDocumentImportStream(documents);
|
||||
const writer = await tableWriter({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
createIfNotExists: true,
|
||||
});
|
||||
await copyStream(reader, writer);
|
||||
|
||||
const result = await driver.readCollection(conn, {
|
||||
pureName: collectionName,
|
||||
countDocuments: true,
|
||||
});
|
||||
expect(result.count).toBe(150);
|
||||
});
|
||||
|
||||
test('export empty collection returns no data rows', async () => {
|
||||
const reader = await tableReader({
|
||||
systemConnection: conn,
|
||||
driver,
|
||||
pureName: collectionName,
|
||||
});
|
||||
const writer = createExportStream();
|
||||
await copyStream(reader, writer);
|
||||
|
||||
const rows = writer.resultArray.filter(x => !x.__isStreamHeader);
|
||||
expect(rows.length).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -123,5 +123,22 @@ services:
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
mongodb:
|
||||
image: mongo:4.0.12
|
||||
restart: always
|
||||
volumes:
|
||||
- mongo-data:/data/db
|
||||
- mongo-config:/data/configdb
|
||||
ports:
|
||||
- 27017:27017
|
||||
|
||||
dynamodb:
|
||||
image: amazon/dynamodb-local
|
||||
restart: always
|
||||
ports:
|
||||
- 8000:8000
|
||||
|
||||
volumes:
|
||||
firebird-data:
|
||||
mongo-data:
|
||||
mongo-config:
|
||||
|
||||
@@ -738,6 +738,27 @@ const firebirdEngine = {
|
||||
skipDropReferences: true,
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').TestEngineInfo} */
|
||||
const mongoDbEngine = {
|
||||
label: 'MongoDB',
|
||||
connection: {
|
||||
engine: 'mongo@dbgate-plugin-mongo',
|
||||
server: 'localhost',
|
||||
port: 27017,
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').TestEngineInfo} */
|
||||
const dynamoDbEngine = {
|
||||
label: 'DynamoDB',
|
||||
connection: {
|
||||
engine: 'dynamodb@dbgate-plugin-dynamodb',
|
||||
server: 'localhost',
|
||||
port: 8000,
|
||||
authType: 'onpremise',
|
||||
},
|
||||
};
|
||||
|
||||
const enginesOnCi = [
|
||||
// all engines, which would be run on GitHub actions
|
||||
mysqlEngine,
|
||||
@@ -788,3 +809,5 @@ module.exports.libsqlFileEngine = libsqlFileEngine;
|
||||
module.exports.libsqlWsEngine = libsqlWsEngine;
|
||||
module.exports.duckdbEngine = duckdbEngine;
|
||||
module.exports.firebirdEngine = firebirdEngine;
|
||||
module.exports.mongoDbEngine = mongoDbEngine;
|
||||
module.exports.dynamoDbEngine = dynamoDbEngine;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
const requireEngineDriver = require('dbgate-api/src/utility/requireEngineDriver');
|
||||
const engines = require('./engines');
|
||||
const { mongoDbEngine, dynamoDbEngine } = require('./engines');
|
||||
global.DBGATE_PACKAGES = {
|
||||
'dbgate-tools': require('dbgate-tools'),
|
||||
'dbgate-sqltree': require('dbgate-sqltree'),
|
||||
@@ -9,7 +10,7 @@ global.DBGATE_PACKAGES = {
|
||||
async function connectEngine(engine) {
|
||||
const { connection } = engine;
|
||||
const driver = requireEngineDriver(connection);
|
||||
for (;;) {
|
||||
for (; ;) {
|
||||
try {
|
||||
const conn = await driver.connect(connection);
|
||||
await driver.getVersion(conn);
|
||||
@@ -26,7 +27,8 @@ async function connectEngine(engine) {
|
||||
|
||||
async function run() {
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
await Promise.all(engines.map(engine => connectEngine(engine)));
|
||||
const documentEngines = [mongoDbEngine, dynamoDbEngine];
|
||||
await Promise.all([...engines, ...documentEngines].map(engine => connectEngine(engine)));
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "7.1.2",
|
||||
"version": "7.1.6",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -95,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];
|
||||
},
|
||||
@@ -239,7 +241,7 @@ 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 {
|
||||
const serializedMessage = serializeJsTypesForJsonStringify({ msgid, ...message });
|
||||
conn.subprocess.send(serializedMessage);
|
||||
@@ -264,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
|
||||
@@ -344,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 },
|
||||
@@ -580,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);
|
||||
@@ -622,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,
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,6 +12,13 @@ import isPlainObject from 'lodash/isPlainObject';
|
||||
import md5 from 'blueimp-md5';
|
||||
|
||||
export const MAX_GRID_TEXT_LENGTH = 1000; // maximum length of text in grid cell, longer text is truncated
|
||||
export const MAX_GRID_BINARY_SIZE = 10000; // maximum binary size (base64 chars or byte count) before showing 'too large' in grid cell
|
||||
|
||||
function formatByteSize(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${bytes} B`;
|
||||
}
|
||||
|
||||
export type EditorDataType =
|
||||
| 'null'
|
||||
@@ -49,6 +56,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 +84,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 +109,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 +324,21 @@ 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' };
|
||||
}
|
||||
}
|
||||
if (intent === 'gridCellIntent' && value.$binary.base64.length > MAX_GRID_BINARY_SIZE) {
|
||||
return { value: `(Field too large, ${formatByteSize(Math.round(value.$binary.base64.length * 3 / 4))})`, gridStyle: 'nullCellStyle' };
|
||||
}
|
||||
return {
|
||||
value: base64ToHex(value.$binary.base64),
|
||||
gridStyle: 'valueCellStyle',
|
||||
@@ -354,6 +427,14 @@ export function stringifyCellValue(
|
||||
}
|
||||
}
|
||||
|
||||
if (value?.type === 'Buffer' && _isArray(value.data)) {
|
||||
if (intent === 'gridCellIntent') {
|
||||
return value.data.length > MAX_GRID_BINARY_SIZE
|
||||
? { value: `(Field too large, ${formatByteSize(value.data.length)})`, gridStyle: 'nullCellStyle' }
|
||||
: { value: '0x' + arrayToHexString(value.data), gridStyle: 'valueCellStyle' };
|
||||
}
|
||||
}
|
||||
|
||||
if (_isArray(value)) {
|
||||
switch (intent) {
|
||||
case 'gridCellIntent':
|
||||
@@ -482,7 +563,7 @@ export function shouldOpenMultilineDialog(value) {
|
||||
}
|
||||
|
||||
export function isJsonLikeLongString(value) {
|
||||
return _isString(value) && value.length > 100 && value.match(/^\s*\{.*\}\s*$|^\s*\[.*\]\s*$/m);
|
||||
return _isString(value) && value.length > 100 && value.length <= MAX_GRID_BINARY_SIZE && value.match(/^\s*\{.*\}\s*$|^\s*\[.*\]\s*$/m);
|
||||
}
|
||||
|
||||
export function getIconForRedisType(type) {
|
||||
|
||||
Vendored
+5
@@ -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 {
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"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",
|
||||
|
||||
@@ -1,286 +1,307 @@
|
||||
<script context="module" lang="ts">
|
||||
import { __t } from '../translations';
|
||||
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.openQuery',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.export',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
icon: 'icon export',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().exportGrid(),
|
||||
});
|
||||
|
||||
function buildConditionForGrid(props) {
|
||||
const filters = props?.display?.config?.filters;
|
||||
const filterBehaviour =
|
||||
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
|
||||
|
||||
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
|
||||
|
||||
const conditions = [];
|
||||
for (const uniqueName in filters || {}) {
|
||||
if (!filters[uniqueName]) continue;
|
||||
try {
|
||||
const ast = parseFilter(filters[uniqueName], filterBehaviour);
|
||||
// console.log('AST', ast);
|
||||
const cond = _.cloneDeepWith(ast, expr => {
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: uniqueName,
|
||||
};
|
||||
}
|
||||
|
||||
// if (expr.__placeholder__) {
|
||||
// return {
|
||||
// [uniqueName]: expr.__placeholder__,
|
||||
// };
|
||||
// }
|
||||
});
|
||||
conditions.push(cond);
|
||||
} catch (err) {
|
||||
// error in filter
|
||||
}
|
||||
}
|
||||
|
||||
return conditions.length > 0
|
||||
? {
|
||||
conditionType: 'and',
|
||||
conditions,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function buildSortForGrid(props) {
|
||||
const sort = props?.display?.config?.sort;
|
||||
|
||||
if (sort?.length > 0) {
|
||||
return sort.map(col => ({
|
||||
columnName: col.uniqueName,
|
||||
direction: col.order,
|
||||
}));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadCollectionDataPage(props, offset, limit) {
|
||||
const { conid, database } = props;
|
||||
|
||||
const response = await apiCall('database-connections/collection-data', {
|
||||
conid,
|
||||
database,
|
||||
options: {
|
||||
pureName: props.pureName,
|
||||
limit,
|
||||
skip: offset,
|
||||
condition: buildConditionForGrid(props),
|
||||
sort: buildSortForGrid(props),
|
||||
},
|
||||
auditLogSessionGroup: 'data-grid',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
return true;
|
||||
// const { display } = props;
|
||||
// const sql = display.getPageQuery(0, 1);
|
||||
// return !!sql;
|
||||
}
|
||||
|
||||
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),
|
||||
},
|
||||
});
|
||||
|
||||
return response.count;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import {
|
||||
extractShellConnection,
|
||||
extractShellConnectionHostable,
|
||||
extractShellHostConnection,
|
||||
} from '../impexp/createImpExpScript';
|
||||
import { apiCall } 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 { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import ChangeSetGrider from './ChangeSetGrider';
|
||||
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
|
||||
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
export let conid;
|
||||
export let display;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let config;
|
||||
export let changeSetState;
|
||||
export let dispatchChangeSet;
|
||||
|
||||
export let macroPreview;
|
||||
export let macroValues;
|
||||
export let setLoadedRows = null;
|
||||
export let onPublishedCellsChanged;
|
||||
|
||||
// export let onChangeGrider = undefined;
|
||||
|
||||
let loadedRows = [];
|
||||
let publishedCells = [];
|
||||
|
||||
export const activator = createActivator('CollectionDataGridCore', false);
|
||||
|
||||
// $: console.log('loadedRows BIND', loadedRows);
|
||||
$: grider = new ChangeSetGrider(
|
||||
loadedRows,
|
||||
changeSetState,
|
||||
dispatchChangeSet,
|
||||
display,
|
||||
macroPreview,
|
||||
macroValues,
|
||||
publishedCells
|
||||
);
|
||||
// $: console.log('GRIDER', grider);
|
||||
// $: if (onChangeGrider) onChangeGrider(grider);
|
||||
|
||||
function getExportQuery() {
|
||||
return display?.driver?.getCollectionExportQueryScript?.(
|
||||
pureName,
|
||||
buildConditionForGrid($$props),
|
||||
buildSortForGrid($$props)
|
||||
);
|
||||
// return `db.collection('${pureName}')
|
||||
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
|
||||
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
|
||||
}
|
||||
|
||||
function getExportQueryJson() {
|
||||
return display?.driver?.getCollectionExportQueryJson?.(
|
||||
pureName,
|
||||
buildConditionForGrid($$props),
|
||||
buildSortForGrid($$props)
|
||||
);
|
||||
// return {
|
||||
// collection: pureName,
|
||||
// condition: buildConditionForGrid($$props) || {},
|
||||
// sort: buildMongoSort($$props) || {},
|
||||
// };
|
||||
}
|
||||
|
||||
export async function exportGrid() {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
const initialValues: any = {};
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceQuery = coninfo.isReadOnly
|
||||
? JSON.stringify(getExportQueryJson(), undefined, 2)
|
||||
: getExportQuery();
|
||||
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
|
||||
initialValues.sourceList = [pureName];
|
||||
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
|
||||
openImportExportTab(initialValues);
|
||||
// showModal(ImportExportModal, { initialValues });
|
||||
}
|
||||
|
||||
export function openQuery() {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
focused: true,
|
||||
props: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: getExportQuery(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
exportQuickExportFile(
|
||||
pureName || 'Data',
|
||||
{
|
||||
functionName: 'queryReader',
|
||||
props: {
|
||||
...extractShellConnectionHostable(coninfo, database),
|
||||
queryType: coninfo.isReadOnly ? 'json' : 'native',
|
||||
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
|
||||
},
|
||||
hostConnection: extractShellHostConnection(coninfo, database),
|
||||
},
|
||||
fmt,
|
||||
display.getExportColumnMap()
|
||||
);
|
||||
};
|
||||
|
||||
registerQuickExportHandler(quickExportHandler);
|
||||
|
||||
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
|
||||
createQuickExportMenu(
|
||||
quickExportHandler,
|
||||
{
|
||||
command: 'collectionDataGrid.export',
|
||||
},
|
||||
{ tag: 'export' }
|
||||
)
|
||||
);
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
if (setLoadedRows) setLoadedRows(rows);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoadingDataGridCore
|
||||
{...$$props}
|
||||
loadDataPage={loadCollectionDataPage}
|
||||
{dataPageAvailable}
|
||||
{loadRowCount}
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
}}
|
||||
frameSelection={!!macroPreview}
|
||||
onOpenQuery={openQuery}
|
||||
{grider}
|
||||
const getCurrentEditor = () => getActiveComponent('CollectionDataGridCore');
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.openQuery',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.openQuery', { defaultMessage: 'Open query' }),
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionDataGrid.export',
|
||||
category: __t('command.dataGrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.dataGrid.export', { defaultMessage: 'Export' }),
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
icon: 'icon export',
|
||||
testEnabled: () => getCurrentEditor() != null,
|
||||
onClick: () => getCurrentEditor().exportGrid(),
|
||||
});
|
||||
|
||||
function buildConditionForGrid(props) {
|
||||
const filters = props?.display?.config?.filters;
|
||||
const filterBehaviour =
|
||||
props?.display?.driver?.getFilterBehaviour(null, standardFilterBehaviours) ?? mongoFilterBehaviour;
|
||||
|
||||
// console.log('USED FILTER BEHAVIOUR', filterBehaviour);
|
||||
|
||||
const conditions = [];
|
||||
for (const uniqueName in filters || {}) {
|
||||
if (!filters[uniqueName]) continue;
|
||||
try {
|
||||
const ast = parseFilter(filters[uniqueName], filterBehaviour);
|
||||
// console.log('AST', ast);
|
||||
const cond = _.cloneDeepWith(ast, expr => {
|
||||
if (expr.exprType == 'placeholder') {
|
||||
return {
|
||||
exprType: 'column',
|
||||
columnName: uniqueName,
|
||||
};
|
||||
}
|
||||
|
||||
// if (expr.__placeholder__) {
|
||||
// return {
|
||||
// [uniqueName]: expr.__placeholder__,
|
||||
// };
|
||||
// }
|
||||
});
|
||||
conditions.push(cond);
|
||||
} catch (err) {
|
||||
// error in filter
|
||||
}
|
||||
}
|
||||
|
||||
return conditions.length > 0
|
||||
? {
|
||||
conditionType: 'and',
|
||||
conditions,
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function buildSortForGrid(props) {
|
||||
const sort = props?.display?.config?.sort;
|
||||
|
||||
if (sort?.length > 0) {
|
||||
return sort.map(col => ({
|
||||
columnName: col.uniqueName,
|
||||
direction: col.order,
|
||||
}));
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadCollectionDataPage(props, offset, limit) {
|
||||
const { conid, database } = props;
|
||||
|
||||
const response = await apiCall('database-connections/collection-data', {
|
||||
conid,
|
||||
database,
|
||||
options: {
|
||||
pureName: props.pureName,
|
||||
limit,
|
||||
skip: offset,
|
||||
condition: buildConditionForGrid(props),
|
||||
sort: buildSortForGrid(props),
|
||||
},
|
||||
auditLogSessionGroup: 'data-grid',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
return true;
|
||||
// const { display } = props;
|
||||
// const sql = display.getPageQuery(0, 1);
|
||||
// return !!sql;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { conid, database } = props;
|
||||
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Row count query timed out')), 3000)
|
||||
);
|
||||
|
||||
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>
|
||||
|
||||
<script lang="ts">
|
||||
import { parseFilter } from 'dbgate-filterparser';
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import {
|
||||
extractShellConnection,
|
||||
extractShellConnectionHostable,
|
||||
extractShellHostConnection,
|
||||
} from '../impexp/createImpExpScript';
|
||||
import { apiCall } 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 { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import ChangeSetGrider from './ChangeSetGrider';
|
||||
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
|
||||
import { mongoFilterBehaviour, standardFilterBehaviours } from 'dbgate-tools';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
export let conid;
|
||||
export let display;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let config;
|
||||
export let changeSetState;
|
||||
export let dispatchChangeSet;
|
||||
|
||||
export let macroPreview;
|
||||
export let macroValues;
|
||||
export let setLoadedRows = null;
|
||||
export let onPublishedCellsChanged;
|
||||
|
||||
// export let onChangeGrider = undefined;
|
||||
|
||||
let loadedRows = [];
|
||||
let publishedCells = [];
|
||||
|
||||
export const activator = createActivator('CollectionDataGridCore', false);
|
||||
|
||||
// $: console.log('loadedRows BIND', loadedRows);
|
||||
$: grider = new ChangeSetGrider(
|
||||
loadedRows,
|
||||
changeSetState,
|
||||
dispatchChangeSet,
|
||||
display,
|
||||
macroPreview,
|
||||
macroValues,
|
||||
publishedCells
|
||||
);
|
||||
// $: console.log('GRIDER', grider);
|
||||
// $: if (onChangeGrider) onChangeGrider(grider);
|
||||
|
||||
function getExportQuery() {
|
||||
return display?.driver?.getCollectionExportQueryScript?.(
|
||||
pureName,
|
||||
buildConditionForGrid($$props),
|
||||
buildSortForGrid($$props)
|
||||
);
|
||||
// return `db.collection('${pureName}')
|
||||
// .find(${JSON.stringify(buildConditionForGrid($$props) || {})})
|
||||
// .sort(${JSON.stringify(buildMongoSort($$props) || {})})`;
|
||||
}
|
||||
|
||||
function getExportQueryJson() {
|
||||
return display?.driver?.getCollectionExportQueryJson?.(
|
||||
pureName,
|
||||
buildConditionForGrid($$props),
|
||||
buildSortForGrid($$props)
|
||||
);
|
||||
// return {
|
||||
// collection: pureName,
|
||||
// condition: buildConditionForGrid($$props) || {},
|
||||
// sort: buildMongoSort($$props) || {},
|
||||
// };
|
||||
}
|
||||
|
||||
export async function exportGrid() {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
const initialValues: any = {};
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceQuery = coninfo.isReadOnly
|
||||
? JSON.stringify(getExportQueryJson(), undefined, 2)
|
||||
: getExportQuery();
|
||||
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
|
||||
initialValues.sourceList = [pureName];
|
||||
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
|
||||
openImportExportTab(initialValues);
|
||||
// showModal(ImportExportModal, { initialValues });
|
||||
}
|
||||
|
||||
export function openQuery() {
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Query #',
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
focused: true,
|
||||
props: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: getExportQuery(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
exportQuickExportFile(
|
||||
pureName || 'Data',
|
||||
{
|
||||
functionName: 'queryReader',
|
||||
props: {
|
||||
...extractShellConnectionHostable(coninfo, database),
|
||||
queryType: coninfo.isReadOnly ? 'json' : 'native',
|
||||
query: coninfo.isReadOnly ? getExportQueryJson() : getExportQuery(),
|
||||
},
|
||||
hostConnection: extractShellHostConnection(coninfo, database),
|
||||
},
|
||||
fmt,
|
||||
display.getExportColumnMap()
|
||||
);
|
||||
};
|
||||
|
||||
registerQuickExportHandler(quickExportHandler);
|
||||
|
||||
registerMenu({ command: 'collectionDataGrid.openQuery', tag: 'export' }, () =>
|
||||
createQuickExportMenu(
|
||||
quickExportHandler,
|
||||
{
|
||||
command: 'collectionDataGrid.export',
|
||||
},
|
||||
{ tag: 'export' }
|
||||
)
|
||||
);
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
if (setLoadedRows) setLoadedRows(rows);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoadingDataGridCore
|
||||
{...$$props}
|
||||
loadDataPage={loadCollectionDataPage}
|
||||
{dataPageAvailable}
|
||||
{loadRowCount}
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
}}
|
||||
frameSelection={!!macroPreview}
|
||||
onOpenQuery={openQuery}
|
||||
{grider}
|
||||
/>
|
||||
|
||||
@@ -26,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' }),
|
||||
@@ -432,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';
|
||||
@@ -454,6 +467,7 @@
|
||||
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;
|
||||
@@ -461,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;
|
||||
@@ -471,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;
|
||||
@@ -645,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 });
|
||||
@@ -1975,6 +2009,7 @@
|
||||
|
||||
registerMenu(
|
||||
{ command: 'dataGrid.refresh' },
|
||||
{ command: 'dataGrid.fetchAll', hideDisabled: true },
|
||||
{ placeTag: 'copy' },
|
||||
{
|
||||
text: _t('datagrid.copyAdvanced', { defaultMessage: 'Copy advanced' }),
|
||||
@@ -2400,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}
|
||||
@@ -2472,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,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}
|
||||
|
||||
@@ -2,238 +2,268 @@
|
||||
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' }),
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'sqlDataGrid.export',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('common.export', { defaultMessage : 'Export' }),
|
||||
icon: 'icon export',
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
|
||||
onClick: () => getCurrentEditor().exportGrid(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
import {
|
||||
extractShellConnection,
|
||||
extractShellConnectionHostable,
|
||||
extractShellHostConnection,
|
||||
} from '../impexp/createImpExpScript';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import createActivator from '../utility/createActivator';
|
||||
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
||||
import { exportQuickExportFile } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import ChangeSetGrider from './ChangeSetGrider';
|
||||
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import OverlayDiffGrider from './OverlayDiffGrider';
|
||||
|
||||
export let conid;
|
||||
export let display;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let config;
|
||||
export let changeSetState;
|
||||
export let dispatchChangeSet;
|
||||
export let overlayDefinition = null;
|
||||
|
||||
export let macroPreview;
|
||||
export let macroValues;
|
||||
export let onPublishedCellsChanged;
|
||||
|
||||
let publishedCells = [];
|
||||
|
||||
// export let onChangeGrider = undefined;
|
||||
|
||||
export const activator = createActivator('SqlDataGridCore', false);
|
||||
|
||||
let loadedRows = [];
|
||||
|
||||
let grider;
|
||||
|
||||
// $: console.log('loadedRows BIND', loadedRows);
|
||||
|
||||
$: {
|
||||
if (!overlayDefinition && macroPreview) {
|
||||
grider = new ChangeSetGrider(
|
||||
loadedRows,
|
||||
changeSetState,
|
||||
dispatchChangeSet,
|
||||
display,
|
||||
macroPreview,
|
||||
macroValues,
|
||||
publishedCells
|
||||
);
|
||||
}
|
||||
}
|
||||
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
|
||||
$: {
|
||||
if (!overlayDefinition && !macroPreview) {
|
||||
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
}
|
||||
// $: console.log('GRIDER', grider);
|
||||
// $: if (onChangeGrider) onChangeGrider(grider);
|
||||
|
||||
$: {
|
||||
if (overlayDefinition) {
|
||||
grider = new OverlayDiffGrider(
|
||||
loadedRows,
|
||||
display,
|
||||
overlayDefinition.matchColumns,
|
||||
overlayDefinition.overlayData,
|
||||
overlayDefinition.matchedDbKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportGrid() {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
|
||||
const initialValues: any = {};
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceQuery = coninfo.isReadOnly
|
||||
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
|
||||
: display.getExportQuery();
|
||||
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
|
||||
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
|
||||
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
|
||||
openImportExportTab(initialValues);
|
||||
// showModal(ImportExportModal, { initialValues });
|
||||
}
|
||||
|
||||
export function openQuery(sql?) {
|
||||
openNewTab(
|
||||
{
|
||||
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
focused: true,
|
||||
props: {
|
||||
schemaName: display.baseTableOrSimilar?.schemaName,
|
||||
pureName: display.baseTableOrSimilar?.pureName,
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: sql ?? display.getExportQuery(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function openQueryOnError() {
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
exportQuickExportFile(
|
||||
pureName || 'Data',
|
||||
{
|
||||
functionName: 'queryReader',
|
||||
props: {
|
||||
...extractShellConnectionHostable(coninfo, database),
|
||||
queryType: coninfo.isReadOnly ? 'json' : 'native',
|
||||
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
|
||||
},
|
||||
hostConnection: extractShellHostConnection(coninfo, database),
|
||||
},
|
||||
fmt,
|
||||
display.getExportColumnMap()
|
||||
);
|
||||
};
|
||||
registerQuickExportHandler(quickExportHandler);
|
||||
|
||||
registerMenu(
|
||||
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
|
||||
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
|
||||
() =>
|
||||
createQuickExportMenu(
|
||||
quickExportHandler,
|
||||
{
|
||||
command: 'sqlDataGrid.export',
|
||||
},
|
||||
{ tag: 'export' }
|
||||
)
|
||||
);
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
}
|
||||
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const select = display.getPageQuery(offset, limit);
|
||||
|
||||
const response = await apiCall('database-connections/sql-select', {
|
||||
conid,
|
||||
database,
|
||||
select,
|
||||
auditLogSessionGroup: 'data-grid',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
const { display } = props;
|
||||
const select = display.getPageQuery(0, 1);
|
||||
return !!select;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const select = display.getCountQuery();
|
||||
|
||||
const response = await apiCall('database-connections/sql-select', {
|
||||
conid,
|
||||
database,
|
||||
select,
|
||||
});
|
||||
|
||||
return parseInt(response.rows[0].count);
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoadingDataGridCore
|
||||
{...$$props}
|
||||
{loadDataPage}
|
||||
{dataPageAvailable}
|
||||
{loadRowCount}
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
}}
|
||||
frameSelection={!!macroPreview}
|
||||
{grider}
|
||||
{display}
|
||||
onOpenQuery={openQuery}
|
||||
onOpenQueryOnError={openQueryOnError}
|
||||
/>
|
||||
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' }),
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/query'),
|
||||
onClick: () => getCurrentEditor().openQuery(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'sqlDataGrid.export',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('common.export', { defaultMessage: 'Export' }),
|
||||
icon: 'icon export',
|
||||
keyText: 'CtrlOrCommand+E',
|
||||
testEnabled: () => getCurrentEditor() != null && hasPermission('dbops/export'),
|
||||
onClick: () => getCurrentEditor().exportGrid(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { registerQuickExportHandler } from '../buttons/ToolStripExportButton.svelte';
|
||||
import {
|
||||
extractShellConnection,
|
||||
extractShellConnectionHostable,
|
||||
extractShellHostConnection,
|
||||
} from '../impexp/createImpExpScript';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import createActivator from '../utility/createActivator';
|
||||
import createQuickExportMenu from '../utility/createQuickExportMenu';
|
||||
import { exportQuickExportFile } from '../utility/exportFileTools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import ChangeSetGrider from './ChangeSetGrider';
|
||||
|
||||
import LoadingDataGridCore from './LoadingDataGridCore.svelte';
|
||||
import { openImportExportTab } from '../utility/importExportTools';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
import OverlayDiffGrider from './OverlayDiffGrider';
|
||||
|
||||
export let conid;
|
||||
export let display;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let config;
|
||||
export let changeSetState;
|
||||
export let dispatchChangeSet;
|
||||
export let overlayDefinition = null;
|
||||
|
||||
export let macroPreview;
|
||||
export let macroValues;
|
||||
export let onPublishedCellsChanged;
|
||||
|
||||
let publishedCells = [];
|
||||
|
||||
// export let onChangeGrider = undefined;
|
||||
|
||||
export const activator = createActivator('SqlDataGridCore', false);
|
||||
|
||||
let loadedRows = [];
|
||||
|
||||
let grider;
|
||||
|
||||
// $: console.log('loadedRows BIND', loadedRows);
|
||||
|
||||
$: {
|
||||
if (!overlayDefinition && macroPreview) {
|
||||
grider = new ChangeSetGrider(
|
||||
loadedRows,
|
||||
changeSetState,
|
||||
dispatchChangeSet,
|
||||
display,
|
||||
macroPreview,
|
||||
macroValues,
|
||||
publishedCells
|
||||
);
|
||||
}
|
||||
}
|
||||
// prevent recreate grider, if no macro is selected, so there is no need to selectedcells in macro
|
||||
$: {
|
||||
if (!overlayDefinition && !macroPreview) {
|
||||
grider = new ChangeSetGrider(loadedRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
}
|
||||
// $: console.log('GRIDER', grider);
|
||||
// $: if (onChangeGrider) onChangeGrider(grider);
|
||||
|
||||
$: {
|
||||
if (overlayDefinition) {
|
||||
grider = new OverlayDiffGrider(
|
||||
loadedRows,
|
||||
display,
|
||||
overlayDefinition.matchColumns,
|
||||
overlayDefinition.overlayData,
|
||||
overlayDefinition.matchedDbKeys
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function exportGrid() {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
|
||||
const initialValues: any = {};
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceQuery = coninfo.isReadOnly
|
||||
? JSON.stringify(display.getExportQueryJson(), undefined, 2)
|
||||
: display.getExportQuery();
|
||||
initialValues.sourceQueryType = coninfo.isReadOnly ? 'json' : 'native';
|
||||
initialValues.sourceList = display.baseTableOrSimilar ? [display.baseTableOrSimilar.pureName] : [];
|
||||
initialValues[`columns_${pureName}`] = display.getExportColumnMap();
|
||||
openImportExportTab(initialValues);
|
||||
// showModal(ImportExportModal, { initialValues });
|
||||
}
|
||||
|
||||
export function openQuery(sql?) {
|
||||
openNewTab(
|
||||
{
|
||||
title: _t('common.queryNumber', { defaultMessage: 'Query #' }),
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'QueryTab',
|
||||
focused: true,
|
||||
props: {
|
||||
schemaName: display.baseTableOrSimilar?.schemaName,
|
||||
pureName: display.baseTableOrSimilar?.pureName,
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: sql ?? display.getExportQuery(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function openQueryOnError() {
|
||||
openQuery(display.getPageQueryText(0, getIntSettingsValue('dataGrid.pageSize', 100, 5, 50000)));
|
||||
}
|
||||
|
||||
const quickExportHandler = fmt => async () => {
|
||||
const coninfo = await getConnectionInfo({ conid });
|
||||
exportQuickExportFile(
|
||||
pureName || 'Data',
|
||||
{
|
||||
functionName: 'queryReader',
|
||||
props: {
|
||||
...extractShellConnectionHostable(coninfo, database),
|
||||
queryType: coninfo.isReadOnly ? 'json' : 'native',
|
||||
query: coninfo.isReadOnly ? display.getExportQueryJson() : display.getExportQuery(),
|
||||
},
|
||||
hostConnection: extractShellHostConnection(coninfo, database),
|
||||
},
|
||||
fmt,
|
||||
display.getExportColumnMap()
|
||||
);
|
||||
};
|
||||
registerQuickExportHandler(quickExportHandler);
|
||||
|
||||
registerMenu(
|
||||
{ command: 'sqlDataGrid.openActiveChart', tag: 'chart' },
|
||||
{ command: 'sqlDataGrid.openQuery', tag: 'export' },
|
||||
() =>
|
||||
createQuickExportMenu(
|
||||
quickExportHandler,
|
||||
{
|
||||
command: 'sqlDataGrid.export',
|
||||
},
|
||||
{ tag: 'export' }
|
||||
)
|
||||
);
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
}
|
||||
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const select = display.getPageQuery(offset, limit);
|
||||
|
||||
const response = await apiCall('database-connections/sql-select', {
|
||||
conid,
|
||||
database,
|
||||
select,
|
||||
auditLogSessionGroup: 'data-grid',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
const { display } = props;
|
||||
const select = display.getPageQuery(0, 1);
|
||||
return !!select;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const select = display.getCountQuery();
|
||||
|
||||
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,
|
||||
sql,
|
||||
});
|
||||
if (!resp || resp.errorMessage) return null;
|
||||
return resp.jslid;
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoadingDataGridCore
|
||||
{...$$props}
|
||||
{loadDataPage}
|
||||
{dataPageAvailable}
|
||||
{loadRowCount}
|
||||
{startFetchAll}
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
onPublishedCellsChanged={value => {
|
||||
publishedCells = value;
|
||||
if (onPublishedCellsChanged) {
|
||||
onPublishedCellsChanged(value);
|
||||
}
|
||||
}}
|
||||
frameSelection={!!macroPreview}
|
||||
{grider}
|
||||
{display}
|
||||
onOpenQuery={openQuery}
|
||||
onOpenQueryOnError={openQueryOnError}
|
||||
/>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
{#if isNative}
|
||||
<select
|
||||
value={options.find(x => x.value == value) ? value : defaultValue}
|
||||
class="{selectClass}"
|
||||
class={selectClass}
|
||||
{...$$restProps}
|
||||
on:change={e => {
|
||||
dispatch('change', e.target['value']);
|
||||
@@ -47,7 +47,7 @@
|
||||
{...$$restProps}
|
||||
items={options ?? []}
|
||||
value={isMulti
|
||||
? _.compact((value && Array.isArray(value)) ? value.map(item => options?.find(x => x.value == item)) : [])
|
||||
? _.compact(value && Array.isArray(value) ? value.map(item => options?.find(x => x.value == item)) : [])
|
||||
: (options?.find(x => x.value == value) ?? null)}
|
||||
on:select={e => {
|
||||
if (isMulti) {
|
||||
@@ -69,7 +69,6 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
<style>
|
||||
.select {
|
||||
--border: var(--theme-input-border);
|
||||
@@ -78,10 +77,10 @@
|
||||
--background: var(--theme-input-background);
|
||||
--borderHoverColor: var(--theme-input-border-hover-color);
|
||||
--borderFocusColor: var(--theme-input-border-focus-color);
|
||||
--listBackground: var(--theme-input-list-background);
|
||||
--listBackground: var(--theme-input-background);
|
||||
--itemActiveBackground: var(--theme-input-item-active-background);
|
||||
--itemIsActiveBG: var(--theme-input-item-active-background);
|
||||
--itemHoverBG: var(--theme-input-item-hover-background);
|
||||
--itemHoverBG: var(--theme-input-multi-clear-hover);
|
||||
--itemColor: var(--theme-input-item-foreground);
|
||||
--listEmptyColor: var(--theme-input-background);
|
||||
--height: 40px;
|
||||
@@ -95,9 +94,8 @@
|
||||
--multiClearHoverFill: var(--theme-input-multi-clear-foreground);
|
||||
--multiItemActiveBG: var(--theme-input-multi-item-background);
|
||||
--multiItemActiveColor: var(--theme-input-multi-item-foreground);
|
||||
--multiItemBG: var(--theme-input-multi-item-background);
|
||||
--multiItemBG: var(--theme-input-multi-clear-background);
|
||||
--multiItemDisabledHoverBg: var(--theme-input-multi-item-background);
|
||||
--multiItemDisabledHoverColor: var(--theme-input-multi-item-foreground);
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,35 +1,47 @@
|
||||
<script lang="ts" context="module">
|
||||
import { apiCall } from '../utility/api';
|
||||
async function loadRow(props, select) {
|
||||
const { conid, database } = props;
|
||||
|
||||
if (!select) return null;
|
||||
|
||||
const response = await apiCall('database-connections/sql-select', {
|
||||
conid,
|
||||
database,
|
||||
select,
|
||||
auditLogSessionGroup: 'data-form',
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
import _ from 'lodash';
|
||||
import LoadingFormView from './LoadingFormView.svelte';
|
||||
|
||||
export let display;
|
||||
|
||||
async function handleLoadRow() {
|
||||
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
|
||||
}
|
||||
|
||||
async function handleLoadRowCount() {
|
||||
const countRow = await loadRow($$props, display.getCountQuery());
|
||||
return countRow ? parseInt(countRow.count) : null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
|
||||
async function loadRow(props, select, options = {}) {
|
||||
const { conid, database } = props;
|
||||
|
||||
if (!select) return null;
|
||||
|
||||
const response = await apiCall('database-connections/sql-select', {
|
||||
conid,
|
||||
database,
|
||||
select,
|
||||
auditLogSessionGroup: 'data-form',
|
||||
...options,
|
||||
});
|
||||
|
||||
if (response.errorMessage) return response;
|
||||
return response.rows[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import LoadingFormView from './LoadingFormView.svelte';
|
||||
|
||||
export let display;
|
||||
|
||||
async function handleLoadRow() {
|
||||
return await loadRow($$props, display.getPageQuery(display.config.formViewRecordNumber || 0, 1));
|
||||
}
|
||||
|
||||
async function handleLoadRowCount() {
|
||||
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>
|
||||
|
||||
<LoadingFormView {...$$props} loadRowFunc={handleLoadRow} loadRowCountFunc={handleLoadRowCount} />
|
||||
|
||||
@@ -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 = {
|
||||
@@ -379,7 +382,7 @@
|
||||
|
||||
{#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(' ')}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
import { addFilesToSourceList } from './ImportExportConfigurator.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
|
||||
let isLoading = false;
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
})),
|
||||
|
||||
@@ -2,223 +2,230 @@
|
||||
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'];
|
||||
export const allowAddToFavorites = props => true;
|
||||
export const allowSwitchDatabase = props => true;
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionTable.save',
|
||||
group: 'save',
|
||||
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
|
||||
name: __t('command.collectionData.save', { defaultMessage: 'Save' }),
|
||||
// keyText: 'CtrlOrCommand+S',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon save',
|
||||
testEnabled: () => getCurrentEditor()?.canSave(),
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import App from '../App.svelte';
|
||||
import DataGrid from '../datagrid/DataGrid.svelte';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import {
|
||||
createChangeSet,
|
||||
createGridCache,
|
||||
CollectionGridDisplay,
|
||||
changeSetContainsChanges,
|
||||
runMacroOnChangeSet,
|
||||
changeSetChangedCount,
|
||||
} from 'dbgate-datalib';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { writable } from 'svelte/store';
|
||||
import createUndoReducer from '../utility/createUndoReducer';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import CollectionDataGridCore from '../datagrid/CollectionDataGridCore.svelte';
|
||||
import { useCollectionInfo, useConnectionInfo, useSettings } from '../utility/metadataLoaders';
|
||||
import { extensions } from '../stores';
|
||||
import CollectionJsonView from '../formview/CollectionJsonView.svelte';
|
||||
import createActivator from '../utility/createActivator';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
import { registerMenu } from '../utility/contextMenu';
|
||||
import { setContext } from 'svelte';
|
||||
import _ from 'lodash';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { markTabSaved, markTabUnsaved } from '../utility/common';
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
export let tabid;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
|
||||
let loadedRows;
|
||||
|
||||
export const activator = createActivator('CollectionDataTab', true);
|
||||
|
||||
const config = useGridConfig(tabid);
|
||||
const cache = writable(createGridCache());
|
||||
const settingsValue = useSettings();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$changeSetStore;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: collectionInfo = useCollectionInfo({ conid, database, schemaName, pureName });
|
||||
|
||||
$: display =
|
||||
$collectionInfo && $connection
|
||||
? new CollectionGridDisplay(
|
||||
$collectionInfo,
|
||||
findEngineDriver($connection, $extensions),
|
||||
//@ts-ignore
|
||||
$config,
|
||||
config.update,
|
||||
$cache,
|
||||
cache.update,
|
||||
loadedRows,
|
||||
$changeSetStore?.value,
|
||||
$connection?.isReadOnly,
|
||||
$settingsValue
|
||||
)
|
||||
: null;
|
||||
// $: console.log('LOADED ROWS MONGO', loadedRows);
|
||||
|
||||
async function handleConfirmChange(changeSet) {
|
||||
const resp = await apiCall('database-connections/update-collection', {
|
||||
conid,
|
||||
database,
|
||||
changeSet: {
|
||||
...changeSet,
|
||||
updates: changeSet.updates.map(update => ({
|
||||
...update,
|
||||
fields: _.mapValues(update.fields, (v, k) => (v === undefined ? { $$undefined$$: true } : v)),
|
||||
})),
|
||||
},
|
||||
});
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, { title: 'Error when saving', message: errorMessage });
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
display?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function canSave() {
|
||||
return changeSetContainsChanges($changeSetStore?.value);
|
||||
}
|
||||
|
||||
export function save() {
|
||||
const json = $changeSetStore?.value;
|
||||
const driver = findEngineDriver($connection, $extensions);
|
||||
const script = driver.getCollectionUpdateScript ? driver.getCollectionUpdateScript(json, $collectionInfo) : null;
|
||||
if (script) {
|
||||
if (getBoolSettingsValue('skipConfirm.collectionDataSave', false)) {
|
||||
handleConfirmChange(json);
|
||||
} else {
|
||||
showModal(ConfirmNoSqlModal, {
|
||||
script,
|
||||
onConfirm: () => handleConfirmChange(json),
|
||||
engine: display.engine,
|
||||
skipConfirmSettingKey: 'skipConfirm.collectionDataSave',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
handleConfirmChange(json);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRunMacro(macro, params, cells) {
|
||||
const newChangeSet = runMacroOnChangeSet(macro, params, cells, $changeSetStore?.value, display, false);
|
||||
if (newChangeSet) {
|
||||
dispatchChangeSet({ type: 'set', value: newChangeSet });
|
||||
}
|
||||
}
|
||||
|
||||
registerMenu({ command: 'collectionTable.save', tag: 'save' });
|
||||
|
||||
const collapsedLeftColumnStore = writable(getLocalStorage('collection_collapsedLeftColumn', false));
|
||||
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
|
||||
$: setLocalStorage('collection_collapsedLeftColumn', $collapsedLeftColumnStore);
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<DataGrid
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
{...$$props}
|
||||
config={$config}
|
||||
setConfig={config.update}
|
||||
cache={$cache}
|
||||
setCache={cache.update}
|
||||
changeSetState={$changeSetStore}
|
||||
focusOnVisible
|
||||
{display}
|
||||
{changeSetStore}
|
||||
{dispatchChangeSet}
|
||||
gridCoreComponent={CollectionDataGridCore}
|
||||
jsonViewComponent={CollectionJsonView}
|
||||
isDynamicStructure
|
||||
showMacros
|
||||
macroCondition={macro => macro.type == 'transformValue'}
|
||||
onRunMacro={handleRunMacro}
|
||||
/>
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled />
|
||||
<ToolStripCommandButton
|
||||
command="collectionTable.save"
|
||||
iconAfter={getNumberIcon(changeSetChangedCount($changeSetStore?.value))}
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.revertAllChanges" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.deleteSelectedRows" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.addNewColumn" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.switchToJson" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.switchToTable" hideDisabled />
|
||||
<ToolStripExportButton {quickExportHandlerRef} command="collectionDataGrid.export" />
|
||||
<ToolStripCommandButton command="collectionJsonView.expandAll" hideDisabled />
|
||||
<ToolStripCommandButton command="collectionJsonView.collapseAll" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled data-testid="CollectionDataTab_toggleCellDataView" />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
const getCurrentEditor = () => getActiveComponent('CollectionDataTab');
|
||||
|
||||
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName'];
|
||||
export const allowAddToFavorites = props => true;
|
||||
export const allowSwitchDatabase = props => true;
|
||||
|
||||
registerCommand({
|
||||
id: 'collectionTable.save',
|
||||
group: 'save',
|
||||
category: __t('command.collectionData', { defaultMessage: 'Collection data' }),
|
||||
name: __t('command.collectionData.save', { defaultMessage: 'Save' }),
|
||||
// keyText: 'CtrlOrCommand+S',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon save',
|
||||
testEnabled: () => getCurrentEditor()?.canSave(),
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import App from '../App.svelte';
|
||||
import DataGrid from '../datagrid/DataGrid.svelte';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import {
|
||||
createChangeSet,
|
||||
createGridCache,
|
||||
CollectionGridDisplay,
|
||||
changeSetContainsChanges,
|
||||
runMacroOnChangeSet,
|
||||
changeSetChangedCount,
|
||||
} from 'dbgate-datalib';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { writable } from 'svelte/store';
|
||||
import createUndoReducer from '../utility/createUndoReducer';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import CollectionDataGridCore from '../datagrid/CollectionDataGridCore.svelte';
|
||||
import { useCollectionInfo, useConnectionInfo, useSettings } from '../utility/metadataLoaders';
|
||||
import { extensions } from '../stores';
|
||||
import CollectionJsonView from '../formview/CollectionJsonView.svelte';
|
||||
import createActivator from '../utility/createActivator';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmNoSqlModal from '../modals/ConfirmNoSqlModal.svelte';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { setContext } from 'svelte';
|
||||
import _ from 'lodash';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import { getBoolSettingsValue } from '../settings/settingsTools';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { markTabSaved, markTabUnsaved } from '../utility/common';
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
export let tabid;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
|
||||
let loadedRows;
|
||||
|
||||
export const activator = createActivator('CollectionDataTab', true);
|
||||
|
||||
const config = useGridConfig(tabid);
|
||||
const cache = writable(createGridCache());
|
||||
const settingsValue = useSettings();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$changeSetStore;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: collectionInfo = useCollectionInfo({ conid, database, schemaName, pureName });
|
||||
|
||||
$: display =
|
||||
$collectionInfo && $connection
|
||||
? new CollectionGridDisplay(
|
||||
$collectionInfo,
|
||||
findEngineDriver($connection, $extensions),
|
||||
//@ts-ignore
|
||||
$config,
|
||||
config.update,
|
||||
$cache,
|
||||
cache.update,
|
||||
loadedRows,
|
||||
$changeSetStore?.value,
|
||||
$connection?.isReadOnly,
|
||||
$settingsValue
|
||||
)
|
||||
: null;
|
||||
// $: console.log('LOADED ROWS MONGO', loadedRows);
|
||||
|
||||
async function handleConfirmChange(changeSet) {
|
||||
const resp = await apiCall('database-connections/update-collection', {
|
||||
conid,
|
||||
database,
|
||||
changeSet: {
|
||||
...changeSet,
|
||||
updates: changeSet.updates.map(update => ({
|
||||
...update,
|
||||
fields: _.mapValues(update.fields, (v, k) => (v === undefined ? { $$undefined$$: true } : v)),
|
||||
})),
|
||||
},
|
||||
});
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, { title: 'Error when saving', message: errorMessage });
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
display?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function canSave() {
|
||||
return changeSetContainsChanges($changeSetStore?.value);
|
||||
}
|
||||
|
||||
export function save() {
|
||||
const json = $changeSetStore?.value;
|
||||
const driver = findEngineDriver($connection, $extensions);
|
||||
const script = driver.getCollectionUpdateScript ? driver.getCollectionUpdateScript(json, $collectionInfo) : null;
|
||||
if (script) {
|
||||
if (getBoolSettingsValue('skipConfirm.collectionDataSave', false)) {
|
||||
handleConfirmChange(json);
|
||||
} else {
|
||||
showModal(ConfirmNoSqlModal, {
|
||||
script,
|
||||
onConfirm: () => handleConfirmChange(json),
|
||||
engine: display.engine,
|
||||
skipConfirmSettingKey: 'skipConfirm.collectionDataSave',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
handleConfirmChange(json);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRunMacro(macro, params, cells) {
|
||||
const newChangeSet = runMacroOnChangeSet(macro, params, cells, $changeSetStore?.value, display, false);
|
||||
if (newChangeSet) {
|
||||
dispatchChangeSet({ type: 'set', value: newChangeSet });
|
||||
}
|
||||
}
|
||||
|
||||
registerMenu({ command: 'collectionTable.save', tag: 'save' });
|
||||
|
||||
const collapsedLeftColumnStore = writable(getLocalStorage('collection_collapsedLeftColumn', false));
|
||||
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
|
||||
$: setLocalStorage('collection_collapsedLeftColumn', $collapsedLeftColumnStore);
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
function handleSetLoadedRows(rows) {
|
||||
loadedRows = rows;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<DataGrid
|
||||
setLoadedRows={handleSetLoadedRows}
|
||||
{...$$props}
|
||||
config={$config}
|
||||
setConfig={config.update}
|
||||
cache={$cache}
|
||||
setCache={cache.update}
|
||||
changeSetState={$changeSetStore}
|
||||
focusOnVisible
|
||||
{display}
|
||||
{changeSetStore}
|
||||
{dispatchChangeSet}
|
||||
gridCoreComponent={CollectionDataGridCore}
|
||||
jsonViewComponent={CollectionJsonView}
|
||||
isDynamicStructure
|
||||
showMacros
|
||||
macroCondition={macro => macro.type == 'transformValue'}
|
||||
onRunMacro={handleRunMacro}
|
||||
/>
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled />
|
||||
<ToolStripCommandButton
|
||||
command="collectionTable.save"
|
||||
iconAfter={getNumberIcon(changeSetChangedCount($changeSetStore?.value))}
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.revertAllChanges" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.deleteSelectedRows" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.addNewColumn" hideDisabled />
|
||||
<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"
|
||||
/>
|
||||
</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 },
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,434 +2,436 @@
|
||||
import { getActiveComponent } from '../utility/createActivator';
|
||||
import registerCommand from '../commands/registerCommand';
|
||||
import { __t } from '../translations';
|
||||
const getCurrentEditor = () => getActiveComponent('TableDataTab');
|
||||
const INTERVALS = [5, 10, 15, 30, 60];
|
||||
|
||||
const INTERVAL_COMMANDS = [
|
||||
{
|
||||
time: 5,
|
||||
name: __t('command.datagrid.setAutoRefresh.5', { defaultMessage: 'Refresh every 5 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 10,
|
||||
name: __t('command.datagrid.setAutoRefresh.10', { defaultMessage: 'Refresh every 10 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
name: __t('command.datagrid.setAutoRefresh.15', { defaultMessage: 'Refresh every 15 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 30,
|
||||
name: __t('command.datagrid.setAutoRefresh.30', { defaultMessage: 'Refresh every 30 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 60,
|
||||
name: __t('command.datagrid.setAutoRefresh.60', { defaultMessage: 'Refresh every 60 seconds' }),
|
||||
},
|
||||
];
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.save',
|
||||
group: 'save',
|
||||
category: __t('command.tableData', { defaultMessage: 'Table data' }),
|
||||
name: __t('command.tableData.save', { defaultMessage: 'Save' }),
|
||||
// keyText: 'CtrlOrCommand+S',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon save',
|
||||
testEnabled: () => getCurrentEditor()?.canSave(),
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.setAutoRefresh.1',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.setAutoRefresh.1', { defaultMessage: 'Refresh every 1 second' }),
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(1),
|
||||
});
|
||||
|
||||
for (const { time, name } of INTERVAL_COMMANDS) {
|
||||
registerCommand({
|
||||
id: `tableData.setAutoRefresh.${time}`,
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(time),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.stopAutoRefresh',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.stopAutoRefresh', { defaultMessage: 'Stop auto refresh' }),
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === true,
|
||||
onClick: () => getCurrentEditor().stopAutoRefresh(null),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.startAutoRefresh',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.startAutoRefresh', { defaultMessage: 'Start auto refresh' }),
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === false,
|
||||
onClick: () => getCurrentEditor().startAutoRefresh(),
|
||||
});
|
||||
|
||||
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName', 'isRawMode'];
|
||||
export const allowAddToFavorites = props => true;
|
||||
export const allowSwitchDatabase = props => true;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import App from '../App.svelte';
|
||||
import TableDataGrid from '../datagrid/TableDataGrid.svelte';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import {
|
||||
changeSetChangedCount,
|
||||
changeSetContainsChanges,
|
||||
changeSetToSql,
|
||||
createChangeSet,
|
||||
createGridCache,
|
||||
getDeleteCascades,
|
||||
} from 'dbgate-datalib';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { reloadDataCacheFunc } from 'dbgate-datalib';
|
||||
import { writable } from 'svelte/store';
|
||||
import createUndoReducer from '../utility/createUndoReducer';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import { getTableInfo, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { scriptToSql } from 'dbgate-sqltree';
|
||||
import { extensions, lastUsedDefaultActions } from '../stores';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||
import createActivator from '../utility/createActivator';
import { registerMenu } from '../utility/contextMenu';
|
||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { onDestroy, setContext } from 'svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
||||
import { getBoolSettingsValue, getIntSettingsValue } from '../settings/settingsTools';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { markTabSaved, markTabUnsaved } from '../utility/common';
|
||||
import ToolStripButton from '../buttons/ToolStripButton.svelte';
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let isRawMode = false;
|
||||
export let tabPreviewMode;
|
||||
|
||||
export const activator = createActivator('TableDataTab', true);
|
||||
|
||||
const config = useGridConfig(tabid);
|
||||
const cache = writable(createGridCache());
|
||||
const dbinfo = useDatabaseInfo({ conid, database });
|
||||
|
||||
let autoRefreshInterval = getIntSettingsValue('dataGrid.defaultAutoRefreshInterval', 10, 1, 3600);
|
||||
let autoRefreshStarted = false;
|
||||
let autoRefreshTimer = null;
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
const resp = await apiCall('database-connections/run-script', { conid, database, sql, useTransaction: true });
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, {
|
||||
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
|
||||
message: errorMessage,
|
||||
});
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
cache.update(reloadDataCacheFunc);
|
||||
showSnackbarSuccess(_t('tableData.savedToDatabase', { defaultMessage: 'Saved to database' }));
|
||||
}
|
||||
}
|
||||
|
||||
export async function save() {
|
||||
const driver = findEngineDriver($connection, $extensions);
|
||||
const tablePermissionRole = (await getTableInfo({ conid, database, schemaName, pureName }))?.tablePermissionRole;
|
||||
|
||||
if (tablePermissionRole == 'create_update_delete' || tablePermissionRole == 'update_only') {
|
||||
const resp = await apiCall('database-connections/save-table-data', {
|
||||
conid,
|
||||
database,
|
||||
changeSet: $changeSetStore?.value,
|
||||
});
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, {
|
||||
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
|
||||
message: errorMessage,
|
||||
});
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
cache.update(reloadDataCacheFunc);
|
||||
showSnackbarSuccess(_t('tableData.savedToDatabase', { defaultMessage: 'Saved to database' }));
|
||||
}
|
||||
} else {
|
||||
const script = driver.createSaveChangeSetScript($changeSetStore?.value, $dbinfo, () =>
|
||||
changeSetToSql($changeSetStore?.value, $dbinfo, driver.dialect)
|
||||
);
|
||||
|
||||
const deleteCascades = getDeleteCascades($changeSetStore?.value, $dbinfo);
|
||||
const sql = scriptToSql(driver, script);
|
||||
const deleteCascadesScripts = _.map(deleteCascades, ({ title, commands }) => ({
|
||||
title,
|
||||
script: scriptToSql(driver, commands),
|
||||
}));
|
||||
// console.log('deleteCascadesScripts', deleteCascadesScripts);
|
||||
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
|
||||
handleConfirmSql(sql);
|
||||
} else {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql,
|
||||
onConfirm: confirmedSql => handleConfirmSql(confirmedSql),
|
||||
engine: driver.engine,
|
||||
deleteCascadesScripts,
|
||||
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function canSave() {
|
||||
return changeSetContainsChanges($changeSetStore?.value);
|
||||
}
|
||||
|
||||
export function setAutoRefresh(interval) {
|
||||
autoRefreshInterval = interval;
|
||||
startAutoRefresh();
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function isAutoRefresh() {
|
||||
return autoRefreshStarted;
|
||||
}
|
||||
|
||||
export function startAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
cache.update(reloadDataCacheFunc);
|
||||
}, autoRefreshInterval * 1000);
|
||||
autoRefreshStarted = true;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function stopAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshStarted = false;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
function closeRefreshTimer() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$changeSetStore;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
registerMenu({ command: 'tableData.save', tag: 'save' });
|
||||
|
||||
const collapsedLeftColumnStore = writable(getLocalStorage('dataGrid_collapsedLeftColumn', false));
|
||||
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
|
||||
$: setLocalStorage('dataGrid_collapsedLeftColumn', $collapsedLeftColumnStore);
|
||||
|
||||
onDestroy(() => {
|
||||
closeRefreshTimer();
|
||||
});
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
function createAutoRefreshMenu() {
|
||||
return [
|
||||
{ divider: true },
|
||||
{ command: 'dataGrid.deepRefresh', hideDisabled: true },
|
||||
{ command: 'tableData.stopAutoRefresh', hideDisabled: true },
|
||||
{ command: 'tableData.startAutoRefresh', hideDisabled: true },
|
||||
'tableData.setAutoRefresh.1',
|
||||
...INTERVALS.map(seconds => ({
|
||||
command: `tableData.setAutoRefresh.${seconds}`,
|
||||
text: `...${seconds}` + ' ' + _t('command.datagrid.autoRefresh.seconds', { defaultMessage: 'seconds' }),
|
||||
})),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<TableDataGrid
|
||||
{...$$props}
|
||||
config={$config}
|
||||
setConfig={config.update}
|
||||
cache={$cache}
|
||||
setCache={cache.update}
|
||||
changeSetState={$changeSetStore}
|
||||
focusOnVisible
|
||||
{changeSetStore}
|
||||
{dispatchChangeSet}
|
||||
/>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripButton
|
||||
icon="icon structure"
|
||||
iconAfter="icon arrow-link"
|
||||
on:click={() => {
|
||||
if (tabPreviewMode && getBoolSettingsValue('defaultAction.useLastUsedAction', true)) {
|
||||
lastUsedDefaultActions.update(actions => ({
|
||||
...actions,
|
||||
tables: 'openStructure',
|
||||
}));
|
||||
}
|
||||
|
||||
openNewTab({
|
||||
title: pureName,
|
||||
icon: 'img table-structure',
|
||||
tabComponent: 'TableStructureTab',
|
||||
tabPreviewMode: true,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
defaultActionId: 'openStructure',
|
||||
},
|
||||
});
|
||||
}}>{_t('datagrid.structure', { defaultMessage: 'Structure' })}</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripButton
|
||||
icon="img sql-file"
|
||||
iconAfter="icon arrow-link"
|
||||
on:click={() => {
|
||||
if (tabPreviewMode && getBoolSettingsValue('defaultAction.useLastUsedAction', true)) {
|
||||
lastUsedDefaultActions.update(actions => ({
|
||||
...actions,
|
||||
tables: 'showSql',
|
||||
}));
|
||||
}
|
||||
|
||||
openNewTab({
|
||||
title: pureName,
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'SqlObjectTab',
|
||||
tabPreviewMode: true,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
defaultActionId: 'showSql',
|
||||
},
|
||||
});
|
||||
}}>SQL</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted
|
||||
? _t('tableData.refreshEvery', {
|
||||
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
|
||||
values: { autoRefreshInterval },
|
||||
})
|
||||
: null}
|
||||
commands={['dataGrid.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_refreshGrid"
|
||||
/>
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted
|
||||
? _t('tableData.refreshEvery', {
|
||||
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
|
||||
values: { autoRefreshInterval },
|
||||
})
|
||||
: null}
|
||||
commands={['dataForm.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_refreshForm"
|
||||
/>
|
||||
|
||||
<!-- <ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled /> -->
|
||||
|
||||
<ToolStripCommandButton command="dataForm.goToFirst" hideDisabled data-testid="TableDataTab_goToFirst" />
|
||||
<ToolStripCommandButton command="dataForm.goToPrevious" hideDisabled data-testid="TableDataTab_goToPrevious" />
|
||||
<ToolStripCommandButton command="dataForm.goToNext" hideDisabled data-testid="TableDataTab_goToNext" />
|
||||
<ToolStripCommandButton command="dataForm.goToLast" hideDisabled data-testid="TableDataTab_goToLast" />
|
||||
|
||||
<ToolStripCommandButton
|
||||
command="tableData.save"
|
||||
iconAfter={getNumberIcon(changeSetChangedCount($changeSetStore?.value))}
|
||||
data-testid="TableDataTab_save"
|
||||
/>
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.revertAllChanges"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_revertAllChanges"
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled data-testid="TableDataTab_insertNewRow" />
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.deleteSelectedRows"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_deleteSelectedRows"
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.switchToForm" hideDisabled data-testid="TableDataTab_switchToForm" />
|
||||
<ToolStripCommandButton command="dataGrid.switchToTable" hideDisabled data-testid="TableDataTab_switchToTable" />
|
||||
<ToolStripExportButton {quickExportHandlerRef} />
|
||||
|
||||
<ToolStripButton
|
||||
icon={$collapsedLeftColumnStore ? 'icon columns-outline' : 'icon columns'}
|
||||
on:click={() => collapsedLeftColumnStore.update(x => !x)}
|
||||
>{_t('tableData.viewColumns', { defaultMessage: 'View columns' })}</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.toggleCellDataView"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_toggleCellDataView"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
const getCurrentEditor = () => getActiveComponent('TableDataTab');
|
||||
const INTERVALS = [5, 10, 15, 30, 60];
|
||||
|
||||
const INTERVAL_COMMANDS = [
|
||||
{
|
||||
time: 5,
|
||||
name: __t('command.datagrid.setAutoRefresh.5', { defaultMessage: 'Refresh every 5 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 10,
|
||||
name: __t('command.datagrid.setAutoRefresh.10', { defaultMessage: 'Refresh every 10 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 15,
|
||||
name: __t('command.datagrid.setAutoRefresh.15', { defaultMessage: 'Refresh every 15 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 30,
|
||||
name: __t('command.datagrid.setAutoRefresh.30', { defaultMessage: 'Refresh every 30 seconds' }),
|
||||
},
|
||||
{
|
||||
time: 60,
|
||||
name: __t('command.datagrid.setAutoRefresh.60', { defaultMessage: 'Refresh every 60 seconds' }),
|
||||
},
|
||||
];
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.save',
|
||||
group: 'save',
|
||||
category: __t('command.tableData', { defaultMessage: 'Table data' }),
|
||||
name: __t('command.tableData.save', { defaultMessage: 'Save' }),
|
||||
// keyText: 'CtrlOrCommand+S',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon save',
|
||||
testEnabled: () => getCurrentEditor()?.canSave(),
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.setAutoRefresh.1',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.setAutoRefresh.1', { defaultMessage: 'Refresh every 1 second' }),
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(1),
|
||||
});
|
||||
|
||||
for (const { time, name } of INTERVAL_COMMANDS) {
|
||||
registerCommand({
|
||||
id: `tableData.setAutoRefresh.${time}`,
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(time),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.stopAutoRefresh',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.stopAutoRefresh', { defaultMessage: 'Stop auto refresh' }),
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === true,
|
||||
onClick: () => getCurrentEditor().stopAutoRefresh(null),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.startAutoRefresh',
|
||||
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
|
||||
name: __t('command.datagrid.startAutoRefresh', { defaultMessage: 'Start auto refresh' }),
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === false,
|
||||
onClick: () => getCurrentEditor().startAutoRefresh(),
|
||||
});
|
||||
|
||||
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName', 'isRawMode'];
|
||||
export const allowAddToFavorites = props => true;
|
||||
export const allowSwitchDatabase = props => true;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import App from '../App.svelte';
|
||||
import TableDataGrid from '../datagrid/TableDataGrid.svelte';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import {
|
||||
changeSetChangedCount,
|
||||
changeSetContainsChanges,
|
||||
changeSetToSql,
|
||||
createChangeSet,
|
||||
createGridCache,
|
||||
getDeleteCascades,
|
||||
} from 'dbgate-datalib';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { reloadDataCacheFunc } from 'dbgate-datalib';
|
||||
import { writable } from 'svelte/store';
|
||||
import createUndoReducer from '../utility/createUndoReducer';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import { getTableInfo, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { scriptToSql } from 'dbgate-sqltree';
|
||||
import { extensions, lastUsedDefaultActions } from '../stores';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||
import createActivator from '../utility/createActivator';
|
||||
import { registerMenu } from '../utility/contextMenu';
|
||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { onDestroy, setContext } from 'svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
||||
import { getBoolSettingsValue, getIntSettingsValue } from '../settings/settingsTools';
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { markTabSaved, markTabUnsaved } from '../utility/common';
|
||||
import ToolStripButton from '../buttons/ToolStripButton.svelte';
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let schemaName;
|
||||
export let pureName;
|
||||
export let isRawMode = false;
|
||||
export let tabPreviewMode;
|
||||
|
||||
export const activator = createActivator('TableDataTab', true);
|
||||
|
||||
const config = useGridConfig(tabid);
|
||||
const cache = writable(createGridCache());
|
||||
const dbinfo = useDatabaseInfo({ conid, database });
|
||||
|
||||
let autoRefreshInterval = getIntSettingsValue('dataGrid.defaultAutoRefreshInterval', 10, 1, 3600);
|
||||
let autoRefreshStarted = false;
|
||||
let autoRefreshTimer = null;
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleConfirmSql(sql) {
|
||||
const resp = await apiCall('database-connections/run-script', { conid, database, sql, useTransaction: true });
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, {
|
||||
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
|
||||
message: errorMessage,
|
||||
});
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
cache.update(reloadDataCacheFunc);
|
||||
showSnackbarSuccess(_t('tableData.savedToDatabase', { defaultMessage: 'Saved to database' }));
|
||||
}
|
||||
}
|
||||
|
||||
export async function save() {
|
||||
const driver = findEngineDriver($connection, $extensions);
|
||||
const tablePermissionRole = (await getTableInfo({ conid, database, schemaName, pureName }))?.tablePermissionRole;
|
||||
|
||||
if (tablePermissionRole == 'create_update_delete' || tablePermissionRole == 'update_only') {
|
||||
const resp = await apiCall('database-connections/save-table-data', {
|
||||
conid,
|
||||
database,
|
||||
changeSet: $changeSetStore?.value,
|
||||
});
|
||||
const { errorMessage } = resp || {};
|
||||
if (errorMessage) {
|
||||
showModal(ErrorMessageModal, {
|
||||
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
|
||||
message: errorMessage,
|
||||
});
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
cache.update(reloadDataCacheFunc);
|
||||
showSnackbarSuccess(_t('tableData.savedToDatabase', { defaultMessage: 'Saved to database' }));
|
||||
}
|
||||
} else {
|
||||
const script = driver.createSaveChangeSetScript($changeSetStore?.value, $dbinfo, () =>
|
||||
changeSetToSql($changeSetStore?.value, $dbinfo, driver.dialect)
|
||||
);
|
||||
|
||||
const deleteCascades = getDeleteCascades($changeSetStore?.value, $dbinfo);
|
||||
const sql = scriptToSql(driver, script);
|
||||
const deleteCascadesScripts = _.map(deleteCascades, ({ title, commands }) => ({
|
||||
title,
|
||||
script: scriptToSql(driver, commands),
|
||||
}));
|
||||
// console.log('deleteCascadesScripts', deleteCascadesScripts);
|
||||
if (getBoolSettingsValue('skipConfirm.tableDataSave', false) && !deleteCascadesScripts?.length) {
|
||||
handleConfirmSql(sql);
|
||||
} else {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql,
|
||||
onConfirm: confirmedSql => handleConfirmSql(confirmedSql),
|
||||
engine: driver.engine,
|
||||
deleteCascadesScripts,
|
||||
skipConfirmSettingKey: deleteCascadesScripts?.length ? null : 'skipConfirm.tableDataSave',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function canSave() {
|
||||
return changeSetContainsChanges($changeSetStore?.value);
|
||||
}
|
||||
|
||||
export function setAutoRefresh(interval) {
|
||||
autoRefreshInterval = interval;
|
||||
startAutoRefresh();
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function isAutoRefresh() {
|
||||
return autoRefreshStarted;
|
||||
}
|
||||
|
||||
export function startAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
cache.update(reloadDataCacheFunc);
|
||||
}, autoRefreshInterval * 1000);
|
||||
autoRefreshStarted = true;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function stopAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshStarted = false;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
function closeRefreshTimer() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$changeSetStore;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
registerMenu({ command: 'tableData.save', tag: 'save' });
|
||||
|
||||
const collapsedLeftColumnStore = writable(getLocalStorage('dataGrid_collapsedLeftColumn', false));
|
||||
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
|
||||
$: setLocalStorage('dataGrid_collapsedLeftColumn', $collapsedLeftColumnStore);
|
||||
|
||||
onDestroy(() => {
|
||||
closeRefreshTimer();
|
||||
});
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
function createAutoRefreshMenu() {
|
||||
return [
|
||||
{ divider: true },
|
||||
{ command: 'dataGrid.deepRefresh', hideDisabled: true },
|
||||
{ command: 'tableData.stopAutoRefresh', hideDisabled: true },
|
||||
{ command: 'tableData.startAutoRefresh', hideDisabled: true },
|
||||
'tableData.setAutoRefresh.1',
|
||||
...INTERVALS.map(seconds => ({
|
||||
command: `tableData.setAutoRefresh.${seconds}`,
|
||||
text: `...${seconds}` + ' ' + _t('command.datagrid.autoRefresh.seconds', { defaultMessage: 'seconds' }),
|
||||
})),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<TableDataGrid
|
||||
{...$$props}
|
||||
config={$config}
|
||||
setConfig={config.update}
|
||||
cache={$cache}
|
||||
setCache={cache.update}
|
||||
changeSetState={$changeSetStore}
|
||||
focusOnVisible
|
||||
{changeSetStore}
|
||||
{dispatchChangeSet}
|
||||
/>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripButton
|
||||
icon="icon structure"
|
||||
iconAfter="icon arrow-link"
|
||||
on:click={() => {
|
||||
if (tabPreviewMode && getBoolSettingsValue('defaultAction.useLastUsedAction', true)) {
|
||||
lastUsedDefaultActions.update(actions => ({
|
||||
...actions,
|
||||
tables: 'openStructure',
|
||||
}));
|
||||
}
|
||||
|
||||
openNewTab({
|
||||
title: pureName,
|
||||
icon: 'img table-structure',
|
||||
tabComponent: 'TableStructureTab',
|
||||
tabPreviewMode: true,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
defaultActionId: 'openStructure',
|
||||
},
|
||||
});
|
||||
}}>{_t('datagrid.structure', { defaultMessage: 'Structure' })}</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripButton
|
||||
icon="img sql-file"
|
||||
iconAfter="icon arrow-link"
|
||||
on:click={() => {
|
||||
if (tabPreviewMode && getBoolSettingsValue('defaultAction.useLastUsedAction', true)) {
|
||||
lastUsedDefaultActions.update(actions => ({
|
||||
...actions,
|
||||
tables: 'showSql',
|
||||
}));
|
||||
}
|
||||
|
||||
openNewTab({
|
||||
title: pureName,
|
||||
icon: 'img sql-file',
|
||||
tabComponent: 'SqlObjectTab',
|
||||
tabPreviewMode: true,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField: 'tables',
|
||||
defaultActionId: 'showSql',
|
||||
},
|
||||
});
|
||||
}}>SQL</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted
|
||||
? _t('tableData.refreshEvery', {
|
||||
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
|
||||
values: { autoRefreshInterval },
|
||||
})
|
||||
: null}
|
||||
commands={['dataGrid.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_refreshGrid"
|
||||
/>
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted
|
||||
? _t('tableData.refreshEvery', {
|
||||
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
|
||||
values: { autoRefreshInterval },
|
||||
})
|
||||
: null}
|
||||
commands={['dataForm.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_refreshForm"
|
||||
/>
|
||||
|
||||
<!-- <ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled /> -->
|
||||
|
||||
<ToolStripCommandButton command="dataForm.goToFirst" hideDisabled data-testid="TableDataTab_goToFirst" />
|
||||
<ToolStripCommandButton command="dataForm.goToPrevious" hideDisabled data-testid="TableDataTab_goToPrevious" />
|
||||
<ToolStripCommandButton command="dataForm.goToNext" hideDisabled data-testid="TableDataTab_goToNext" />
|
||||
<ToolStripCommandButton command="dataForm.goToLast" hideDisabled data-testid="TableDataTab_goToLast" />
|
||||
|
||||
<ToolStripCommandButton
|
||||
command="tableData.save"
|
||||
iconAfter={getNumberIcon(changeSetChangedCount($changeSetStore?.value))}
|
||||
data-testid="TableDataTab_save"
|
||||
/>
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.revertAllChanges"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_revertAllChanges"
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled data-testid="TableDataTab_insertNewRow" />
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.deleteSelectedRows"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_deleteSelectedRows"
|
||||
/>
|
||||
<ToolStripCommandButton command="dataGrid.switchToForm" hideDisabled data-testid="TableDataTab_switchToForm" />
|
||||
<ToolStripCommandButton command="dataGrid.switchToTable" hideDisabled data-testid="TableDataTab_switchToTable" />
|
||||
<ToolStripExportButton {quickExportHandlerRef} />
|
||||
<ToolStripCommandButton command="dataGrid.fetchAll" hideDisabled data-testid="TableDataTab_fetchAll" />
|
||||
|
||||
<ToolStripButton
|
||||
icon={$collapsedLeftColumnStore ? 'icon columns-outline' : 'icon columns'}
|
||||
on:click={() => collapsedLeftColumnStore.update(x => !x)}
|
||||
>{_t('tableData.viewColumns', { defaultMessage: 'View columns' })}</ToolStripButton
|
||||
>
|
||||
|
||||
<ToolStripCommandButton
|
||||
command="dataGrid.toggleCellDataView"
|
||||
hideDisabled
|
||||
data-testid="TableDataTab_toggleCellDataView"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
|
||||
<ToolStripCommandButton command="dataGrid.refresh" />
|
||||
<ToolStripExportButton {quickExportHandlerRef} />
|
||||
<ToolStripCommandButton command="dataGrid.fetchAll" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
@@ -98,29 +98,31 @@ const clipboardTextFormatter = (delimiter, headers) => (columns, rows, options)
|
||||
|
||||
const clipboardJsonFormatter = () => (columns, rows) => {
|
||||
return JSON.stringify(
|
||||
rows.map(row => _.pick(row, columns)),
|
||||
rows.map(row => _.omitBy(_.pick(row, columns), _.isUndefined)),
|
||||
undefined,
|
||||
2
|
||||
);
|
||||
};
|
||||
|
||||
const clipboardYamlFormatter = () => (columns, rows) => {
|
||||
return yaml.dump(rows.map(row => _.pick(row, columns)));
|
||||
return yaml.dump(rows.map(row => _.omitBy(_.pick(row, columns), _.isUndefined)));
|
||||
};
|
||||
|
||||
const clipboardJsonLinesFormatter = () => (columns, rows) => {
|
||||
return rows.map(row => JSON.stringify(_.pick(row, columns))).join('\r\n');
|
||||
return rows.map(row => JSON.stringify(_.omitBy(_.pick(row, columns), _.isUndefined))).join('\r\n');
|
||||
};
|
||||
|
||||
const clipboardInsertsFormatter = () => (columns, rows, options) => {
|
||||
const { schemaName, pureName, driver } = options;
|
||||
const dmp = driver.createDumper();
|
||||
for (const row of rows) {
|
||||
const definedColumns = columns.filter(col => row[col] !== undefined);
|
||||
if (definedColumns.length === 0) continue;
|
||||
dmp.putCmd(
|
||||
'^insert ^into %f (%,i) ^values (%,v)',
|
||||
{ schemaName, pureName },
|
||||
columns,
|
||||
columns.map(col => row[col])
|
||||
definedColumns,
|
||||
definedColumns.map(col => row[col])
|
||||
);
|
||||
}
|
||||
return dmp.s;
|
||||
@@ -130,8 +132,10 @@ const clipboardUpdatesFormatter = () => (columns, rows, options) => {
|
||||
const { schemaName, pureName, driver, keyColumns } = options;
|
||||
const dmp = driver.createDumper();
|
||||
for (const row of rows) {
|
||||
const definedColumns = columns.filter(col => row[col] !== undefined);
|
||||
if (definedColumns.length === 0) continue;
|
||||
dmp.put('^update %f ^set ', { schemaName, pureName });
|
||||
dmp.putCollection(', ', columns, col => dmp.put('%i=%v', col, row[col]));
|
||||
dmp.putCollection(', ', definedColumns, col => dmp.put('%i=%v', col, row[col]));
|
||||
dmp.put(' ^where ');
|
||||
dmp.putCollection(' ^and ', keyColumns, col => dmp.put('%i=%v', col, row[col]));
|
||||
dmp.endCommand();
|
||||
@@ -141,7 +145,7 @@ const clipboardUpdatesFormatter = () => (columns, rows, options) => {
|
||||
|
||||
const clipboardMongoInsertFormatter = () => (columns, rows, options) => {
|
||||
const { pureName } = options;
|
||||
return rows.map(row => `db.${pureName}.insert(${JSON.stringify(_.pick(row, columns), undefined, 2)});`).join('\n');
|
||||
return rows.map(row => `db.${pureName}.insert(${JSON.stringify(_.omitBy(_.pick(row, columns), _.isUndefined), undefined, 2)});`).join('\n');
|
||||
};
|
||||
|
||||
export function formatClipboardRows(format, columns, rows, options) {
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import _ from 'lodash';
|
||||
import { currentDatabase, openedConnectionsWithTemporary, getCurrentConfig, getOpenedConnections } from '../stores';
|
||||
import {
|
||||
currentDatabase,
|
||||
openedConnectionsWithTemporary,
|
||||
getCurrentConfig,
|
||||
getOpenedConnections,
|
||||
getOpenedTabs,
|
||||
} from '../stores';
|
||||
import { apiCall, getVolatileConnections, strmid } from './api';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { getConfig } from './metadataLoaders';
|
||||
@@ -37,9 +43,29 @@ const doDatabasePing = value => {
|
||||
}
|
||||
};
|
||||
|
||||
function pingAllOpenedDatabases() {
|
||||
const tabs = getOpenedTabs() || [];
|
||||
const allDbs = tabs
|
||||
.filter(tab => !tab.closedTime && tab.props?.conid && tab.props?.database)
|
||||
.map(tab => ({ conid: tab.props.conid as string, database: tab.props.database as string }));
|
||||
const seen = new Set<string>();
|
||||
const databases: { conid: string; database: string }[] = [];
|
||||
for (const db of allDbs) {
|
||||
const key = `${db.conid}/${db.database}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
databases.push(db);
|
||||
}
|
||||
}
|
||||
if (databases.length > 0) {
|
||||
apiCall('database-connections/ping-databases', { databases });
|
||||
}
|
||||
}
|
||||
|
||||
let openedConnectionsHandle = null;
|
||||
|
||||
let currentDatabaseHandle = null;
|
||||
let allDatabasesHandle = null;
|
||||
|
||||
export function subscribeConnectionPingers() {
|
||||
openedConnectionsWithTemporary.subscribe(value => {
|
||||
@@ -53,6 +79,11 @@ export function subscribeConnectionPingers() {
|
||||
if (currentDatabaseHandle) window.clearInterval(currentDatabaseHandle);
|
||||
currentDatabaseHandle = window.setInterval(() => doDatabasePing(value), 20 * 1000);
|
||||
});
|
||||
|
||||
// Ping all databases that have open (non-closed) tabs, not just the current one
|
||||
pingAllOpenedDatabases();
|
||||
if (allDatabasesHandle) window.clearInterval(allDatabasesHandle);
|
||||
allDatabasesHandle = window.setInterval(() => pingAllOpenedDatabases(), 20 * 1000);
|
||||
}
|
||||
|
||||
export function callServerPing() {
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import { isFileDragActive } from '../stores';
|
||||
import { fromEvent } from 'file-selector';
|
||||
import uploadFiles from './uploadFiles';
|
||||
import getElectron from './getElectron';
|
||||
|
||||
function isEvtWithFiles(event) {
|
||||
if (!event.dataTransfer) {
|
||||
@@ -65,8 +66,22 @@ export default function dragDropFileTarget(node, items) {
|
||||
isFileDragActive.set(false);
|
||||
|
||||
if (isEvtWithFiles(event)) {
|
||||
const files = await fromEvent(event);
|
||||
uploadFiles(files);
|
||||
const electron = getElectron();
|
||||
if (electron && event.dataTransfer?.files?.length) {
|
||||
// Electron 37+ removed File.path in favour of webUtils.getPathForFile().
|
||||
// Older Electron sets file.path automatically; newer requires webUtils.
|
||||
const electronModule = window['require']('electron');
|
||||
const files = Array.from(event.dataTransfer.files).map((file: any) => {
|
||||
if (!file.path && electronModule?.webUtils) {
|
||||
file.path = electronModule.webUtils.getPathForFile(file);
|
||||
}
|
||||
return file;
|
||||
});
|
||||
uploadFiles(files);
|
||||
} else {
|
||||
const files = await fromEvent(event);
|
||||
uploadFiles(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
import { apiCall, apiOff, apiOn } from './api';
|
||||
import getElectron from './getElectron';
|
||||
import resolveApi, { resolveApiHeaders } from './resolveApi';
|
||||
|
||||
export interface FetchAllCallbacks {
|
||||
/** Called with each page of rows as they arrive. */
|
||||
onPage(rows: object[]): void;
|
||||
/** Called once when all data has been received. */
|
||||
onFinished(): void;
|
||||
/** Called if an error occurs. */
|
||||
onError(message: string): void;
|
||||
}
|
||||
|
||||
export interface FetchAllHandle {
|
||||
/** Signal the loader to stop fetching. */
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
const STREAM_BATCH_SIZE = 1000;
|
||||
const WEB_PAGE_SIZE = 5000;
|
||||
|
||||
/**
|
||||
* Fetches all rows from a JSONL source.
|
||||
*
|
||||
* Electron: uses paginated `jsldata/get-rows` via IPC (already fast).
|
||||
* Web: waits for source to finish, then streams the entire JSONL file in a
|
||||
* single HTTP request via `jsldata/stream-rows`, parsing lines
|
||||
* progressively with ReadableStream. Falls back to paginated reads
|
||||
* with larger page sizes if streaming is unavailable.
|
||||
*/
|
||||
export function fetchAll(
|
||||
jslid: string,
|
||||
loadDataPage: (offset: number, limit: number) => Promise<any>,
|
||||
callbacks: FetchAllCallbacks,
|
||||
pageSize: number = 100
|
||||
): FetchAllHandle {
|
||||
const isElectron = !!getElectron();
|
||||
|
||||
if (isElectron) {
|
||||
return fetchAllPaginated(jslid, loadDataPage, callbacks, pageSize);
|
||||
} else {
|
||||
return fetchAllWeb(jslid, loadDataPage, callbacks);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Web strategy: listen to SSE stats for progress, once source is finished
|
||||
* stream the entire JSONL in one HTTP request.
|
||||
*/
|
||||
function fetchAllWeb(
|
||||
jslid: string,
|
||||
loadDataPage: (offset: number, limit: number) => Promise<any>,
|
||||
callbacks: FetchAllCallbacks
|
||||
): FetchAllHandle {
|
||||
let cancelled = false;
|
||||
let streamStarted = false;
|
||||
let abortController: AbortController | null = null;
|
||||
let streamReader: ReadableStreamDefaultReader<Uint8Array> | null = null;
|
||||
|
||||
// Initialize cancelFn before registering the SSE handler to avoid TDZ errors
|
||||
// if an immediate stats event triggers fallbackToPaginated() before initialization.
|
||||
let cancelFn = () => {
|
||||
cancelled = true;
|
||||
if (streamReader) {
|
||||
streamReader.cancel().catch(() => {});
|
||||
streamReader = null;
|
||||
}
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
abortController = null;
|
||||
}
|
||||
cleanup();
|
||||
};
|
||||
|
||||
const handleStats = (stats: { rowCount: number; changeIndex: number; isFinished: boolean }) => {
|
||||
if (cancelled || streamStarted) return;
|
||||
|
||||
// Report progress while source is still writing
|
||||
if (!stats.isFinished) {
|
||||
callbacks.onPage([]); // trigger UI update with count info
|
||||
return;
|
||||
}
|
||||
|
||||
// Source finished — stream all rows at once
|
||||
streamStarted = true;
|
||||
startStream();
|
||||
};
|
||||
|
||||
apiOn(`jsldata-stats-${jslid}`, handleStats);
|
||||
|
||||
async function startStream() {
|
||||
abortController = new AbortController();
|
||||
try {
|
||||
const resp = await fetch(`${resolveApi()}/jsldata/stream-rows?jslid=${encodeURIComponent(jslid)}`, {
|
||||
method: 'GET',
|
||||
cache: 'no-cache',
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
...resolveApiHeaders(),
|
||||
},
|
||||
});
|
||||
|
||||
if (!resp.body || resp.status === 404 || resp.status === 405) {
|
||||
// Streaming endpoint not available in this environment — fall back to paginated reads
|
||||
cleanup();
|
||||
fallbackToPaginated();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
// Non-recoverable server error (e.g. 403 security rejection, 5xx) — surface it
|
||||
callbacks.onError(`HTTP ${resp.status}: ${resp.statusText}`);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
streamReader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let isFirstLine = true;
|
||||
let batch: any[] = [];
|
||||
|
||||
while (!cancelled) {
|
||||
const { done, value } = await streamReader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (cancelled) break;
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
if (isFirstLine) {
|
||||
isFirstLine = false;
|
||||
// Check if first line is a header
|
||||
try {
|
||||
const parsed = JSON.parse(trimmed);
|
||||
if (parsed.__isStreamHeader) continue;
|
||||
// Not a header — it's a data row
|
||||
batch.push(parsed);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
batch.push(JSON.parse(trimmed));
|
||||
} catch {
|
||||
// skip malformed lines
|
||||
}
|
||||
if (batch.length >= STREAM_BATCH_SIZE) {
|
||||
if (cancelled) break;
|
||||
callbacks.onPage(batch);
|
||||
batch = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the decoder — any bytes held for multi-byte char completion are released
|
||||
const flushed = decoder.decode();
|
||||
if (flushed) buffer += flushed;
|
||||
|
||||
// Process remaining buffer
|
||||
const remainingBuffer = buffer.trim();
|
||||
if (remainingBuffer && !cancelled) {
|
||||
try {
|
||||
const parsed = JSON.parse(remainingBuffer);
|
||||
if (!parsed.__isStreamHeader) {
|
||||
batch.push(parsed);
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (batch.length > 0 && !cancelled) {
|
||||
callbacks.onPage(batch);
|
||||
}
|
||||
|
||||
if (!cancelled) {
|
||||
callbacks.onFinished();
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
callbacks.onError(err?.message ?? String(err));
|
||||
}
|
||||
} finally {
|
||||
streamReader = null;
|
||||
abortController = null;
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackToPaginated() {
|
||||
const handle = fetchAllPaginated(jslid, loadDataPage, callbacks, WEB_PAGE_SIZE);
|
||||
cancelFn = handle.cancel;
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
apiOff(`jsldata-stats-${jslid}`, handleStats);
|
||||
}
|
||||
|
||||
// Check if data is already finished
|
||||
checkInitialState();
|
||||
|
||||
async function checkInitialState() {
|
||||
try {
|
||||
const stats = await apiCall('jsldata/get-stats', { jslid });
|
||||
if (stats && stats.isFinished && stats.rowCount > 0) {
|
||||
streamStarted = true;
|
||||
startStream();
|
||||
} else if (stats && stats.isFinished && stats.rowCount === 0) {
|
||||
// Source finished with zero rows — no SSE event will follow, finish immediately
|
||||
cleanup();
|
||||
callbacks.onFinished();
|
||||
}
|
||||
// Source still writing or no stats yet — SSE events will trigger stream when done
|
||||
} catch {
|
||||
// Stats not available yet — SSE events will arrive
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
cancelFn();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated strategy (Electron / fallback): uses `jsldata/get-rows` with
|
||||
* SSE stats events to know when new data is available.
|
||||
*/
|
||||
function fetchAllPaginated(
|
||||
jslid: string,
|
||||
loadDataPage: (offset: number, limit: number) => Promise<any>,
|
||||
callbacks: FetchAllCallbacks,
|
||||
pageSize: number
|
||||
): FetchAllHandle {
|
||||
let cancelled = false;
|
||||
let finished = false;
|
||||
let offset = 0;
|
||||
let isRunning = false;
|
||||
let isSourceFinished = false;
|
||||
let drainRequested = false;
|
||||
|
||||
function finish() {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
callbacks.onFinished();
|
||||
cleanup();
|
||||
}
|
||||
|
||||
const handleStats = (stats: { rowCount: number; changeIndex: number; isFinished: boolean }) => {
|
||||
isSourceFinished = stats.isFinished;
|
||||
if (stats.rowCount > offset) {
|
||||
scheduleDrain();
|
||||
} else if (stats.isFinished && stats.rowCount === offset) {
|
||||
finish();
|
||||
}
|
||||
};
|
||||
|
||||
function scheduleDrain() {
|
||||
if (isRunning) {
|
||||
drainRequested = true;
|
||||
return;
|
||||
}
|
||||
drain();
|
||||
}
|
||||
|
||||
apiOn(`jsldata-stats-${jslid}`, handleStats);
|
||||
|
||||
async function drain() {
|
||||
if (isRunning || cancelled) return;
|
||||
isRunning = true;
|
||||
drainRequested = false;
|
||||
|
||||
try {
|
||||
while (!cancelled) {
|
||||
const rows = await loadDataPage(offset, pageSize);
|
||||
if (cancelled) break;
|
||||
|
||||
if (rows.errorMessage) {
|
||||
callbacks.onError(rows.errorMessage);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (rows.length > 0) {
|
||||
offset += rows.length;
|
||||
callbacks.onPage(rows);
|
||||
}
|
||||
|
||||
if (rows.length < pageSize) {
|
||||
if (isSourceFinished) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
}
|
||||
} catch (err) {
|
||||
if (!cancelled) {
|
||||
const msg = err?.message ?? String(err);
|
||||
if (msg.includes('ENOENT')) {
|
||||
// File not ready yet
|
||||
} else {
|
||||
callbacks.onError(msg);
|
||||
cleanup();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
isRunning = false;
|
||||
if (drainRequested && !cancelled) {
|
||||
scheduleDrain();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
apiOff(`jsldata-stats-${jslid}`, handleStats);
|
||||
}
|
||||
|
||||
checkInitialState();
|
||||
|
||||
async function checkInitialState() {
|
||||
try {
|
||||
const stats = await apiCall('jsldata/get-stats', { jslid });
|
||||
if (stats) {
|
||||
isSourceFinished = stats.isFinished;
|
||||
if (stats.rowCount > 0) {
|
||||
scheduleDrain();
|
||||
} else if (stats.isFinished && !cancelled) {
|
||||
// rowCount === 0: source finished empty — no SSE event will follow
|
||||
finish();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Stats not available yet
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
cancel() {
|
||||
cancelled = true;
|
||||
cleanup();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
unsubscribeCachePeek,
|
||||
} from './cache';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { derived } from 'svelte/store';
|
||||
import { derived, writable } from 'svelte/store';
|
||||
import { extendDatabaseInfo } from 'dbgate-tools';
|
||||
import { setLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall, apiOff, apiOn } from './api';
|
||||
@@ -171,15 +171,17 @@ const installedPluginsLoader = () => ({
|
||||
reloadTrigger: { key: `installed-plugins-changed` },
|
||||
});
|
||||
|
||||
const filesLoader = ({ folder }) => ({
|
||||
const filesLoader = ({ folder, parseFrontMatter = false }) => ({
|
||||
url: 'files/list',
|
||||
params: { folder },
|
||||
params: parseFrontMatter ? { folder, parseFrontMatter: true } : { folder },
|
||||
reloadTrigger: { key: `files-changed`, folder },
|
||||
errorValue: [],
|
||||
});
|
||||
const allFilesLoader = () => ({
|
||||
url: 'files/list-all',
|
||||
params: {},
|
||||
reloadTrigger: { key: `all-files-changed` },
|
||||
errorValue: [],
|
||||
});
|
||||
const authTypesLoader = ({ engine }) => ({
|
||||
url: 'plugins/auth-types',
|
||||
@@ -188,25 +190,34 @@ const authTypesLoader = ({ engine }) => ({
|
||||
errorValue: null,
|
||||
});
|
||||
|
||||
const publicCloudErrorStore = writable(false);
|
||||
const cloudContentErrorStore = writable(false);
|
||||
|
||||
const publicCloudFilesLoader = () => ({
|
||||
url: 'cloud/public-files',
|
||||
params: {},
|
||||
reloadTrigger: { key: `public-cloud-changed` },
|
||||
errorValue: [],
|
||||
onError: err => publicCloudErrorStore.set(!!err),
|
||||
});
|
||||
const cloudContentListLoader = () => ({
|
||||
url: 'cloud/content-list',
|
||||
params: {},
|
||||
reloadTrigger: { key: `cloud-content-changed` },
|
||||
errorValue: [],
|
||||
onError: err => cloudContentErrorStore.set(!!err),
|
||||
});
|
||||
const teamFilesLoader = () => ({
|
||||
url: 'team-files/list',
|
||||
params: {},
|
||||
reloadTrigger: { key: `team-files-changed` },
|
||||
errorValue: [],
|
||||
});
|
||||
const teamFoldersLoader = () => ({
|
||||
url: 'team-files/list-folders',
|
||||
params: {},
|
||||
reloadTrigger: { key: `team-folders-changed` },
|
||||
errorValue: [],
|
||||
});
|
||||
const promoWidgetLoader = () => ({
|
||||
url: 'cloud/premium-promo-widget',
|
||||
@@ -220,15 +231,17 @@ const fileThemesLoader = () => ({
|
||||
});
|
||||
|
||||
async function getCore(loader, args) {
|
||||
const { url, params, reloadTrigger, transform, onLoaded, errorValue } = loader(args);
|
||||
const { url, params, reloadTrigger, transform, onLoaded, onError, errorValue } = loader(args);
|
||||
const key = stableStringify({ url, ...params });
|
||||
|
||||
async function doLoad() {
|
||||
const resp = await apiCall(url, params);
|
||||
if (resp?.errorMessage && errorValue !== undefined) {
|
||||
if (onError) onError(resp.errorMessage);
|
||||
if (onLoaded) onLoaded(errorValue);
|
||||
return errorValue;
|
||||
}
|
||||
if (onError) onError(null);
|
||||
const res = (transform || (x => x))(resp);
|
||||
if (onLoaded) onLoaded(res);
|
||||
return res;
|
||||
@@ -551,6 +564,9 @@ export function getPublicCloudFiles(args) {
|
||||
export function usePublicCloudFiles(args = {}) {
|
||||
return useCore(publicCloudFilesLoader, args);
|
||||
}
|
||||
export function usePublicCloudError() {
|
||||
return publicCloudErrorStore;
|
||||
}
|
||||
|
||||
export function getCloudContentList(args) {
|
||||
return getCore(cloudContentListLoader, args);
|
||||
@@ -558,6 +574,9 @@ export function getCloudContentList(args) {
|
||||
export function useCloudContentList(args = {}) {
|
||||
return useCore(cloudContentListLoader, args);
|
||||
}
|
||||
export function useCloudContentError() {
|
||||
return cloudContentErrorStore;
|
||||
}
|
||||
|
||||
export function getTeamFiles(args) {
|
||||
return getCore(teamFilesLoader, args);
|
||||
|
||||
@@ -4,7 +4,12 @@
|
||||
|
||||
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||
import * as cloudContentAppObject from '../appobj/CloudContentAppObject.svelte';
|
||||
import { useCloudContentList, usePublicCloudFiles, useServerStatus } from '../utility/metadataLoaders';
|
||||
import {
|
||||
useCloudContentList,
|
||||
usePublicCloudFiles,
|
||||
useServerStatus,
|
||||
useCloudContentError,
|
||||
} from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
@@ -47,6 +52,7 @@
|
||||
|
||||
const cloudContentList = useCloudContentList();
|
||||
const serverStatus = useServerStatus();
|
||||
const cloudContentError = useCloudContentError();
|
||||
const cloudContentColorFactory = useCloudContentColorFactory();
|
||||
const connectionColorFactory = useConnectionColorFactory();
|
||||
|
||||
@@ -259,16 +265,19 @@
|
||||
icon="icon plus-thick"
|
||||
menu={createAddItemMenu}
|
||||
title={_t('privateCloudWidget.addNewConnectionOrFile', { defaultMessage: 'Add new connection or file' })}
|
||||
disabled={$cloudContentError}
|
||||
/>
|
||||
<DropDownButton
|
||||
icon="icon add-folder"
|
||||
menu={createAddFolderMenu}
|
||||
title={_t('privateCloudWidget.addNewFolder', { defaultMessage: 'Add new folder' })}
|
||||
disabled={$cloudContentError}
|
||||
/>
|
||||
<InlineButton
|
||||
on:click={handleRefreshContent}
|
||||
title={_t('privateCloudWidget.refreshFiles', { defaultMessage: 'Refresh files' })}
|
||||
data-testid="CloudItemsWidget_buttonRefreshContent"
|
||||
disabled={$cloudContentError}
|
||||
>
|
||||
<FontIcon icon="icon refresh" />
|
||||
</InlineButton>
|
||||
@@ -300,7 +309,14 @@
|
||||
groupContextMenu={createGroupContextMenu}
|
||||
/>
|
||||
|
||||
{#if !cloudContentFlat?.length}
|
||||
{#if $cloudContentError}
|
||||
<ErrorInfo
|
||||
message={_t('privateCloudWidget.cloudUnavailable', {
|
||||
defaultMessage: 'DbGate Cloud is temporarily unavailable',
|
||||
})}
|
||||
icon="img warn"
|
||||
/>
|
||||
{:else if !cloudContentFlat?.length}
|
||||
<ErrorInfo
|
||||
message={_t('privateCloudWidget.noContent', { defaultMessage: 'You have no content on DbGate cloud' })}
|
||||
icon="img info"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||
import * as publicCloudFileAppObject from '../appobj/PublicCloudFileAppObject.svelte';
|
||||
import { usePublicCloudFiles } from '../utility/metadataLoaders';
|
||||
import { usePublicCloudFiles, usePublicCloudError } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
@@ -20,6 +20,7 @@
|
||||
let filter = '';
|
||||
|
||||
const publicFiles = usePublicCloudFiles();
|
||||
const publicCloudError = usePublicCloudError();
|
||||
|
||||
function handleRefreshPublic() {
|
||||
refreshPublicCloudFiles(true);
|
||||
@@ -42,6 +43,7 @@
|
||||
on:click={handleRefreshPublic}
|
||||
title={_t('publicCloudWidget.refreshFiles', { defaultMessage: 'Refresh files' })}
|
||||
data-testid="CloudItemsWidget_buttonRefreshPublic"
|
||||
disabled={$publicCloudError}
|
||||
>
|
||||
<FontIcon icon="icon refresh" />
|
||||
</InlineButton>
|
||||
@@ -54,7 +56,14 @@
|
||||
{filter}
|
||||
/>
|
||||
|
||||
{#if !$publicFiles?.length}
|
||||
{#if $publicCloudError}
|
||||
<ErrorInfo
|
||||
message={_t('publicCloudWidget.cloudUnavailable', {
|
||||
defaultMessage: 'DbGate Cloud is temporarily unavailable',
|
||||
})}
|
||||
icon="img warn"
|
||||
/>
|
||||
{:else if !$publicFiles?.length}
|
||||
<ErrorInfo
|
||||
message={_t('publicCloudWidget.noFilesFound', { defaultMessage: 'No files found for your configuration' })}
|
||||
/>
|
||||
|
||||
@@ -8,16 +8,20 @@
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useFiles, useTeamFiles } from '../utility/metadataLoaders';
|
||||
import { useFiles, useTeamFiles, useConnectionList, useCloudContentList } from '../utility/metadataLoaders';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
|
||||
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
|
||||
import { DATA_FOLDER_NAMES, getConnectionLabel } from 'dbgate-tools';
|
||||
import { _t } from '../translations';
|
||||
import { currentDatabase } from '../stores';
|
||||
|
||||
let filter = '';
|
||||
let selectedConnectionId = '';
|
||||
|
||||
const sqlFiles = useFiles({ folder: 'sql' });
|
||||
const connectionList = useConnectionList();
|
||||
const cloudContentList = useCloudContentList();
|
||||
const sqlFiles = useFiles({ folder: 'sql', parseFrontMatter: true });
|
||||
const shellFiles = useFiles({ folder: 'shell' });
|
||||
const markdownFiles = useFiles({ folder: 'markdown' });
|
||||
const chartFiles = useFiles({ folder: 'charts' });
|
||||
@@ -33,22 +37,105 @@
|
||||
const appFiles = useFiles({ folder: 'apps' });
|
||||
const teamFiles = useTeamFiles({});
|
||||
|
||||
function makeConnectionKey(connectionId: string, databaseName: string | undefined) {
|
||||
return databaseName ? `${connectionId}::${databaseName}` : connectionId;
|
||||
}
|
||||
|
||||
$: cloudIdToLabel = _.fromPairs(
|
||||
(($cloudContentList || []) as any[])
|
||||
.flatMap(fld => fld.items ?? [])
|
||||
.filter(item => item.type === 'connection' && item.folid && item.cntid)
|
||||
.map(item => [`cloud://${item.folid}/${item.cntid}`, item.name as string])
|
||||
);
|
||||
|
||||
$: connectionDbOptions = _.uniqBy(
|
||||
(($sqlFiles || []) as any[])
|
||||
.filter(f => f.connectionId)
|
||||
.map(f => {
|
||||
const conn = (($connectionList || []) as any[]).find(c => c._id === f.connectionId);
|
||||
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[f.connectionId] || f.connectionId;
|
||||
const label = f.databaseName ? `${connLabel} - ${f.databaseName}` : connLabel;
|
||||
return {
|
||||
value: makeConnectionKey(f.connectionId, f.databaseName),
|
||||
label: label as string,
|
||||
connectionId: f.connectionId,
|
||||
databaseName: f.databaseName,
|
||||
};
|
||||
}),
|
||||
x => x.value
|
||||
);
|
||||
|
||||
$: connectionOptions = (() => {
|
||||
const grouped = _.groupBy(connectionDbOptions, o => o.connectionId);
|
||||
const connectionGroups = Object.entries(grouped)
|
||||
.map(([connId, items]) => {
|
||||
const conn = (($connectionList || []) as any[]).find(c => c._id === connId);
|
||||
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[connId] || connId;
|
||||
const hasMultipleDbs = items.length > 1 || items.some(i => i.databaseName);
|
||||
const dbItems = [...items].sort((a, b) => (a.databaseName || '').localeCompare(b.databaseName || ''));
|
||||
|
||||
return {
|
||||
sortLabel: connLabel || connId,
|
||||
options: [
|
||||
...(hasMultipleDbs
|
||||
? [
|
||||
{
|
||||
value: `conn-all::${connId}`,
|
||||
label: `${connLabel} - ${_t('files.allDatabases', { defaultMessage: 'all databases' })}`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...dbItems,
|
||||
],
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.sortLabel.localeCompare(b.sortLabel));
|
||||
|
||||
return connectionGroups.flatMap(group => group.options);
|
||||
})();
|
||||
|
||||
$: currentDbFilterOption = (() => {
|
||||
if (!$currentDatabase?.connection?._id) return null;
|
||||
const connId = $currentDatabase.connection._id;
|
||||
const dbName = $currentDatabase.name;
|
||||
const hasFiles = (($sqlFiles || []) as any[]).some(f => f.connectionId === connId && f.databaseName === dbName);
|
||||
if (!hasFiles) return null;
|
||||
const conn = (($connectionList || []) as any[]).find(c => c._id === connId);
|
||||
const connLabel = (conn && getConnectionLabel(conn)) || cloudIdToLabel[connId] || connId;
|
||||
const label = dbName ? `${connLabel} - ${dbName}` : connLabel;
|
||||
return { value: `current-db`, label };
|
||||
})();
|
||||
|
||||
$: filteredSqlFiles = (() => {
|
||||
if (!selectedConnectionId) return $sqlFiles || [];
|
||||
if (selectedConnectionId === 'current-db') {
|
||||
const connId = $currentDatabase?.connection?._id;
|
||||
const dbName = $currentDatabase?.name;
|
||||
return ($sqlFiles || []).filter(f => f.connectionId === connId && f.databaseName === dbName);
|
||||
}
|
||||
if (selectedConnectionId.startsWith('conn-all::')) {
|
||||
const connId = selectedConnectionId.slice('conn-all::'.length);
|
||||
return ($sqlFiles || []).filter(f => f.connectionId === connId);
|
||||
}
|
||||
return ($sqlFiles || []).filter(f => makeConnectionKey(f.connectionId, f.databaseName) === selectedConnectionId);
|
||||
})();
|
||||
|
||||
$: files = [
|
||||
...($sqlFiles || []),
|
||||
...($shellFiles || []),
|
||||
...($markdownFiles || []),
|
||||
...($chartFiles || []),
|
||||
...($queryFiles || []),
|
||||
...($sqliteFiles || []),
|
||||
...($diagramFiles || []),
|
||||
...($perspectiveFiles || []),
|
||||
...($importExportJobFiles || []),
|
||||
...($modelTransformFiles || []),
|
||||
...($themeFiles || []),
|
||||
...((isProApp() && $dataDeployJobFiles) || []),
|
||||
...((isProApp() && $dbCompareJobFiles) || []),
|
||||
...((isProApp() && $appFiles) || []),
|
||||
...($teamFiles || []),
|
||||
...filteredSqlFiles,
|
||||
...(selectedConnectionId ? [] : $shellFiles || []),
|
||||
...(selectedConnectionId ? [] : $markdownFiles || []),
|
||||
...(selectedConnectionId ? [] : $chartFiles || []),
|
||||
...(selectedConnectionId ? [] : $queryFiles || []),
|
||||
...(selectedConnectionId ? [] : $sqliteFiles || []),
|
||||
...(selectedConnectionId ? [] : $diagramFiles || []),
|
||||
...(selectedConnectionId ? [] : $perspectiveFiles || []),
|
||||
...(selectedConnectionId ? [] : $importExportJobFiles || []),
|
||||
...(selectedConnectionId ? [] : $modelTransformFiles || []),
|
||||
...(selectedConnectionId ? [] : $themeFiles || []),
|
||||
...((isProApp() && !selectedConnectionId && $dataDeployJobFiles) || []),
|
||||
...((isProApp() && !selectedConnectionId && $dbCompareJobFiles) || []),
|
||||
...((isProApp() && !selectedConnectionId && $appFiles) || []),
|
||||
...(selectedConnectionId ? [] : $teamFiles || []),
|
||||
];
|
||||
|
||||
function handleRefreshFiles() {
|
||||
@@ -92,6 +179,41 @@
|
||||
</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
|
||||
{#if connectionOptions.length > 0}
|
||||
<div class="connection-filter">
|
||||
<div class="mr-1">{_t('files.connection', { defaultMessage: 'Connection' })}:</div>
|
||||
<select
|
||||
class="connection-select"
|
||||
value={selectedConnectionId}
|
||||
on:change={e => {
|
||||
selectedConnectionId = e.currentTarget.value;
|
||||
}}
|
||||
data-testid="SavedFilesList_connectionFilter"
|
||||
>
|
||||
<option value="">{_t('files.allConnections', { defaultMessage: 'All connections' })}</option>
|
||||
{#if currentDbFilterOption}
|
||||
<option value="current-db"
|
||||
>{_t('files.currentDatabase', { defaultMessage: 'Current database' })}: {currentDbFilterOption.label}</option
|
||||
>
|
||||
{/if}
|
||||
{#each connectionOptions as opt}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{#if selectedConnectionId}
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
selectedConnectionId = '';
|
||||
}}
|
||||
title={_t('files.clearConnectionFilter', { defaultMessage: 'Clear connection filter' })}
|
||||
data-testid="SavedFilesList_clearConnectionFilter"
|
||||
>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<WidgetsInnerContainer>
|
||||
<AppObjectList
|
||||
list={files}
|
||||
@@ -103,3 +225,29 @@
|
||||
{filter}
|
||||
/>
|
||||
</WidgetsInnerContainer>
|
||||
|
||||
<style>
|
||||
.connection-filter {
|
||||
display: flex;
|
||||
border-bottom: var(--theme-card-border);
|
||||
margin-bottom: 5px;
|
||||
align-items: center;
|
||||
padding-left: 5px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.connection-select {
|
||||
flex: 1 1 0%;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
height: 24px;
|
||||
background-color: var(--theme-searchbox-background);
|
||||
border: var(--theme-searchbox-border);
|
||||
box-shadow: none;
|
||||
cursor: pointer;
|
||||
color: var(--theme-font-1);
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
margin-right: 4px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -310,7 +310,7 @@
|
||||
data-testid="SqlObjectList_searchMenuDropDown"
|
||||
/>
|
||||
{#if !filter}
|
||||
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
|
||||
<DropDownButton icon="icon plus-thick" menu={createAddMenu} data-testid="SqlObjectList_addButton" />
|
||||
{/if}
|
||||
<DropDownButton
|
||||
menu={createRefreshDatabaseMenu}
|
||||
|
||||
@@ -181,7 +181,7 @@
|
||||
</div>
|
||||
<div class="container">
|
||||
{#each contextItems || [] as item}
|
||||
<div class="item" class:clickable={item.clickable} on:click={item.onClick}>
|
||||
<div class="item" class:clickable={item.clickable} on:click={item.onClick} title={item.title || null}>
|
||||
{#if item.icon}
|
||||
<FontIcon icon={item.icon} padRight />
|
||||
{/if}
|
||||
|
||||
@@ -8,14 +8,15 @@
|
||||
export let clickable = false;
|
||||
export let icon = null;
|
||||
export let onClick = null;
|
||||
export let title = null;
|
||||
|
||||
const key = uuidv1();
|
||||
const tabid = getContext('tabid');
|
||||
|
||||
onMount(() => {
|
||||
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
|
||||
updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
|
||||
});
|
||||
onDestroy(() => updateStatuBarInfoItem(tabid, key, null));
|
||||
|
||||
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick });
|
||||
$: updateStatuBarInfoItem(tabid, key, { text, icon, clickable, onClick, title });
|
||||
</script>
|
||||
|
||||
@@ -71,14 +71,19 @@ const driver = {
|
||||
// called for retrieve data (eg. browse in data grid) and for update database
|
||||
async query(dbhan, query, options) {
|
||||
const offset = options?.range?.offset;
|
||||
const commandTimeout = options?.commandTimeout;
|
||||
const executeOptions = {};
|
||||
if (commandTimeout) {
|
||||
executeOptions.readTimeout = parseInt(commandTimeout);
|
||||
}
|
||||
if (options?.discardResult) {
|
||||
await dbhan.client.execute(query);
|
||||
await dbhan.client.execute(query, [], executeOptions);
|
||||
return {
|
||||
rows: [],
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
const result = await dbhan.client.execute(query);
|
||||
const result = await dbhan.client.execute(query, [], executeOptions);
|
||||
if (!result.rows?.[0]) {
|
||||
return {
|
||||
rows: [],
|
||||
|
||||
@@ -89,7 +89,7 @@ const driver = {
|
||||
title: 'Cassandra',
|
||||
icon: cassandraIcon,
|
||||
showConnectionField: (field, values) =>
|
||||
['server', 'singleDatabase', 'localDataCenter', 'isReadOnly', 'user', 'password'].includes(field),
|
||||
['server', 'singleDatabase', 'localDataCenter', 'isReadOnly', 'user', 'password', 'allowedDatabases', 'allowedDatabasesRegex'].includes(field),
|
||||
getQuerySplitterOptions: (usage) =>
|
||||
usage == 'editor'
|
||||
? { ...mysqlSplitterOptions, ignoreComments: true, preventSingleLineSplit: true }
|
||||
|
||||
@@ -25,6 +25,7 @@ const driver = {
|
||||
},
|
||||
// called for retrieve data (eg. browse in data grid) and for update database
|
||||
async query(dbhan, query, options) {
|
||||
const commandTimeout = options?.commandTimeout;
|
||||
if (options?.discardResult) {
|
||||
await dbhan.client.command({
|
||||
query,
|
||||
@@ -34,10 +35,14 @@ const driver = {
|
||||
columns: [],
|
||||
};
|
||||
} else {
|
||||
const resultSet = await dbhan.client.query({
|
||||
const queryOptions = {
|
||||
query,
|
||||
format: 'JSONCompactEachRowWithNamesAndTypes',
|
||||
});
|
||||
};
|
||||
if (commandTimeout) {
|
||||
queryOptions.settings = { max_execution_time: Math.ceil(parseInt(commandTimeout) / 1000) };
|
||||
}
|
||||
const resultSet = await dbhan.client.query(queryOptions);
|
||||
|
||||
const dataSet = await resultSet.json();
|
||||
if (!dataSet?.[0]) {
|
||||
|
||||
@@ -152,7 +152,7 @@ const driver = {
|
||||
supportsIncrementalAnalysis: true,
|
||||
icon: clickhouseIcon,
|
||||
showConnectionField: (field, values) => {
|
||||
return ['databaseUrl', 'defaultDatabase', 'singleDatabase', 'isReadOnly', 'user', 'password'].includes(field);
|
||||
return ['databaseUrl', 'defaultDatabase', 'singleDatabase', 'isReadOnly', 'user', 'password', 'allowedDatabases', 'allowedDatabasesRegex'].includes(field);
|
||||
},
|
||||
getQuerySplitterOptions: (usage) =>
|
||||
usage == 'editor'
|
||||
|
||||
@@ -40,6 +40,6 @@
|
||||
"dbgate-query-splitter": "^4.12.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@duckdb/node-api": "^1.2.1-alpha.16"
|
||||
"@duckdb/node-api": "^1.5.0-r.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,11 +48,12 @@ const driver = {
|
||||
|
||||
return {
|
||||
client: connection,
|
||||
instance,
|
||||
};
|
||||
},
|
||||
async close(dbhan) {
|
||||
dbhan.client.disconnect();
|
||||
dbhan.client.close();
|
||||
dbhan.client.disconnectSync();
|
||||
dbhan.instance.closeSync();
|
||||
},
|
||||
async query(dbhan, sql, { readonly } = {}) {
|
||||
const res = await dbhan.client.runAndReadAll(sql);
|
||||
|
||||
@@ -15,7 +15,7 @@ const {
|
||||
function getColumnsInfo(columnNames, columnTypes) {
|
||||
const columns = [];
|
||||
|
||||
for (let i = columnNames.length - 1; i >= 0; i--) {
|
||||
for (let i = 0; i < columnNames.length; i++) {
|
||||
columns.push({
|
||||
columnName: columnNames[i],
|
||||
dataType: columnTypes[i].toString(),
|
||||
|
||||
@@ -6,7 +6,7 @@ const Analyser = require('./Analyser');
|
||||
const isPromise = require('is-promise');
|
||||
const mongodb = require('mongodb');
|
||||
const { ObjectId } = require('mongodb');
|
||||
const { EJSON } = require('bson');
|
||||
const { EJSON, Binary } = require('bson');
|
||||
const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse, getLogger } = require('dbgate-tools');
|
||||
const createBulkInsertStream = require('./createBulkInsertStream');
|
||||
const {
|
||||
@@ -53,8 +53,18 @@ function findArrayResult(resValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function BinData(_subType, base64) {
|
||||
return Buffer.from(base64, 'base64');
|
||||
function BinData(subType, base64) {
|
||||
let numericSubType;
|
||||
if (typeof subType === 'string') {
|
||||
const hex = subType.startsWith('0x') || subType.startsWith('0X') ? subType.slice(2) : subType;
|
||||
numericSubType = parseInt(hex, 16);
|
||||
} else {
|
||||
numericSubType = subType;
|
||||
}
|
||||
if (!Number.isInteger(numericSubType) || numericSubType < 0 || numericSubType > 255) {
|
||||
throw new TypeError(`BinData subType must be an integer between 0 and 255, got: ${subType}`);
|
||||
}
|
||||
return new Binary(Buffer.from(base64, 'base64'), numericSubType);
|
||||
}
|
||||
|
||||
async function getScriptableDb(dbhan) {
|
||||
@@ -487,7 +497,11 @@ const drivers = driverBases.map((driverBase) => ({
|
||||
|
||||
const collection = dbhan.getDatabase().collection(options.pureName);
|
||||
if (options.countDocuments) {
|
||||
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {});
|
||||
const countOptions = {};
|
||||
if (options.commandTimeout) {
|
||||
countOptions.maxTimeMS = parseInt(options.commandTimeout);
|
||||
}
|
||||
const count = await collection.countDocuments(deserializeMongoData(mongoCondition) || {}, countOptions);
|
||||
return { count };
|
||||
} else if (options.aggregate) {
|
||||
let cursor = await collection.aggregate(deserializeMongoData(convertToMongoAggregate(options.aggregate)));
|
||||
|
||||
@@ -74,6 +74,7 @@ const mongoDriverBase = {
|
||||
|
||||
showConnectionField: (field, values) => {
|
||||
if (field == 'useDatabaseUrl') return true;
|
||||
if (field == 'allowedDatabases' || field == 'allowedDatabasesRegex') return true;
|
||||
if (values.useDatabaseUrl) {
|
||||
return ['databaseUrl', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes(field);
|
||||
}
|
||||
|
||||
@@ -93,12 +93,16 @@ const driver = {
|
||||
platformInfo?.isWindows && (authType == 'sspi' || authType == 'sql') ? 'msnodesqlv8' : 'tedious';
|
||||
const client = connectionType == 'msnodesqlv8' ? await nativeConnect(conn) : await tediousConnect(conn);
|
||||
|
||||
return {
|
||||
const dbhan = {
|
||||
client,
|
||||
connectionType,
|
||||
database: conn.database,
|
||||
conid: conn.conid,
|
||||
};
|
||||
if (conn.defaultIsolationLevel) {
|
||||
await this.setTransactionIsolationLevel(dbhan, conn.defaultIsolationLevel);
|
||||
}
|
||||
return dbhan;
|
||||
},
|
||||
async close(dbhan) {
|
||||
return dbhan.client.close();
|
||||
@@ -173,6 +177,13 @@ const driver = {
|
||||
await this.query(dbhan, `KILL ${processId}`);
|
||||
},
|
||||
|
||||
async setTransactionIsolationLevel(dbhan, level) {
|
||||
if (this.isolationLevels && level && !this.isolationLevels.includes(level)) {
|
||||
throw new Error(`Isolation level "${level}" is not supported. Supported levels: ${this.isolationLevels.join(', ')}`);
|
||||
}
|
||||
await this.query(dbhan, `SET TRANSACTION ISOLATION LEVEL ${level}`);
|
||||
},
|
||||
|
||||
async serverSummary(dbhan) {
|
||||
const [variables, processes, databases] = await Promise.all([
|
||||
this.listVariables(dbhan),
|
||||
|
||||
@@ -119,7 +119,7 @@ async function tediousQueryCore(dbhan, sql, options) {
|
||||
columns: [],
|
||||
});
|
||||
}
|
||||
const { addDriverNativeColumn, discardResult } = options || {};
|
||||
const { addDriverNativeColumn, discardResult, commandTimeout } = options || {};
|
||||
return new Promise((resolve, reject) => {
|
||||
const result = {
|
||||
rows: [],
|
||||
@@ -129,6 +129,9 @@ async function tediousQueryCore(dbhan, sql, options) {
|
||||
if (err) reject(err);
|
||||
else resolve(result);
|
||||
});
|
||||
if (commandTimeout) {
|
||||
request.setTimeout(parseInt(commandTimeout));
|
||||
}
|
||||
request.on('columnMetadata', function (columns) {
|
||||
result.columns = extractTediousColumns(columns, addDriverNativeColumn);
|
||||
});
|
||||
|
||||
@@ -165,6 +165,9 @@ const driver = {
|
||||
'singleDatabase',
|
||||
'isReadOnly',
|
||||
'useSeparateSchemas',
|
||||
'defaultIsolationLevel',
|
||||
'allowedDatabases',
|
||||
'allowedDatabasesRegex',
|
||||
].includes(field) ||
|
||||
(field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi') ||
|
||||
(field == 'windowsDomain' && values.authType != 'sql' && values.authType != 'sspi' && values.authType != 'msentra'),
|
||||
@@ -179,7 +182,9 @@ const driver = {
|
||||
defaultPort: 1433,
|
||||
defaultAuthTypeName: 'tedious',
|
||||
supportsTransactions: true,
|
||||
isolationLevels: ['READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SNAPSHOT', 'SERIALIZABLE'],
|
||||
supportsIncrementalAnalysis: true,
|
||||
defaultIsolationLevel: 'READ COMMITTED',
|
||||
// databaseUrlPlaceholder: 'e.g. server=localhost&authentication.type=default&authentication.type.user=myuser&authentication.type.password=pwd&options.database=mydb',
|
||||
|
||||
getNewObjectTemplates() {
|
||||
|
||||
@@ -78,6 +78,9 @@ const drivers = driverBases.map(driverBase => ({
|
||||
if (isReadOnly) {
|
||||
await this.query(dbhan, 'SET SESSION TRANSACTION READ ONLY');
|
||||
}
|
||||
if (props.defaultIsolationLevel) {
|
||||
await this.setTransactionIsolationLevel(dbhan, props.defaultIsolationLevel);
|
||||
}
|
||||
return dbhan;
|
||||
},
|
||||
close(dbhan) {
|
||||
@@ -105,9 +108,18 @@ const drivers = driverBases.map(driverBase => ({
|
||||
};
|
||||
}
|
||||
|
||||
const commandTimeout = options?.commandTimeout;
|
||||
const queryOptions = {};
|
||||
if (commandTimeout) {
|
||||
queryOptions.timeout = parseInt(commandTimeout);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dbhan.client.query(sql, function (error, results, fields) {
|
||||
if (error) reject(error);
|
||||
dbhan.client.query({ sql, ...queryOptions }, function (error, results, fields) {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
const columns = extractColumns(fields);
|
||||
resolve({ rows: results && columns && results.map && results.map(row => modifyRow(zipDataRow(row, columns), columns)), columns });
|
||||
});
|
||||
@@ -244,6 +256,13 @@ const drivers = driverBases.map(driverBase => ({
|
||||
await this.query(dbhan, `KILL ${processId}`);
|
||||
},
|
||||
|
||||
async setTransactionIsolationLevel(dbhan, level) {
|
||||
if (this.isolationLevels && level && !this.isolationLevels.includes(level)) {
|
||||
throw new Error(`Isolation level "${level}" is not supported. Supported levels: ${this.isolationLevels.join(', ')}`);
|
||||
}
|
||||
await this.query(dbhan, `SET SESSION TRANSACTION ISOLATION LEVEL ${level}`);
|
||||
},
|
||||
|
||||
async serverSummary(dbhan) {
|
||||
const [variables, processes, databases] = await Promise.all([
|
||||
this.listVariables(dbhan),
|
||||
|
||||
@@ -162,7 +162,7 @@ const mysqlDialect = {
|
||||
const mysqlDriverBase = {
|
||||
...driverBase,
|
||||
showConnectionField: (field, values) => {
|
||||
if (['authType', 'user', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes(field)) {
|
||||
if (['authType', 'user', 'defaultDatabase', 'singleDatabase', 'isReadOnly', 'allowedDatabases', 'allowedDatabasesRegex', 'defaultIsolationLevel'].includes(field)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,9 @@ const mysqlDriverBase = {
|
||||
defaultAuthTypeName: 'hostPort',
|
||||
defaultSocketPath: '/var/run/mysqld/mysqld.sock',
|
||||
supportsTransactions: true,
|
||||
isolationLevels: ['READ UNCOMMITTED', 'READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE'],
|
||||
supportsIncrementalAnalysis: true,
|
||||
defaultIsolationLevel: 'REPEATABLE READ',
|
||||
|
||||
getNewObjectTemplates() {
|
||||
return [
|
||||
|
||||
@@ -37,10 +37,18 @@ function zipDataRow(rowArray, columns) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
function nativeDateToIsoString(date) {
|
||||
const pad = n => String(n).padStart(2, '0');
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
||||
}
|
||||
|
||||
function modifyRow(row, columns) {
|
||||
columns.forEach(col => {
|
||||
if (Buffer.isBuffer(row[col.columnName])) {
|
||||
row[col.columnName] = { $binary: { base64: row[col.columnName].toString('base64') } };
|
||||
const val = row[col.columnName];
|
||||
if (val instanceof Date) {
|
||||
row[col.columnName] = nativeDateToIsoString(val);
|
||||
} else if (Buffer.isBuffer(val)) {
|
||||
row[col.columnName] = { $binary: { base64: val.toString('base64') } };
|
||||
}
|
||||
});
|
||||
return row;
|
||||
@@ -99,7 +107,7 @@ const driver = {
|
||||
async close(dbhan) {
|
||||
return dbhan.client.close();
|
||||
},
|
||||
async query(dbhan, sql) {
|
||||
async query(dbhan, sql, options) {
|
||||
if (sql == null || sql.trim() == '') {
|
||||
return {
|
||||
rows: [],
|
||||
@@ -112,7 +120,21 @@ const driver = {
|
||||
sql = mtrim[1];
|
||||
}
|
||||
|
||||
const res = await dbhan.client.execute(sql);
|
||||
const commandTimeout = options?.commandTimeout;
|
||||
let previousCallTimeout;
|
||||
if (commandTimeout) {
|
||||
previousCallTimeout = dbhan.client.callTimeout;
|
||||
dbhan.client.callTimeout = parseInt(commandTimeout);
|
||||
}
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await dbhan.client.execute(sql);
|
||||
} finally {
|
||||
if (commandTimeout) {
|
||||
dbhan.client.callTimeout = previousCallTimeout || 0;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const columns = extractOracleColumns(res.metaData);
|
||||
return { rows: (res.rows || []).map(row => modifyRow(zipDataRow(row, columns), columns)), columns };
|
||||
|
||||
@@ -112,6 +112,7 @@ const oracleDriver = {
|
||||
showConnectionField: (field, values, { config }) => {
|
||||
if (field == 'useDatabaseUrl') return true;
|
||||
if (field == 'authType') return true;
|
||||
if (field == 'allowedDatabases' || field == 'allowedDatabasesRegex') return true;
|
||||
if (field == 'clientLibraryPath') return config?.isElectron && values.authType == 'thick';
|
||||
|
||||
if (values.useDatabaseUrl) {
|
||||
|
||||
@@ -115,6 +115,7 @@ const drivers = driverBases.map(driverBase => ({
|
||||
isReadOnly,
|
||||
authType,
|
||||
socketPath,
|
||||
defaultIsolationLevel,
|
||||
} = props;
|
||||
let options = null;
|
||||
|
||||
@@ -172,20 +173,34 @@ const drivers = driverBases.map(driverBase => ({
|
||||
if (isReadOnly) {
|
||||
await this.query(dbhan, 'SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY');
|
||||
}
|
||||
if (defaultIsolationLevel) {
|
||||
await this.setTransactionIsolationLevel(dbhan, defaultIsolationLevel);
|
||||
}
|
||||
|
||||
return dbhan;
|
||||
},
|
||||
async close(dbhan) {
|
||||
return dbhan.client.end();
|
||||
},
|
||||
async query(dbhan, sql) {
|
||||
async query(dbhan, sql, options) {
|
||||
if (sql == null) {
|
||||
return {
|
||||
rows: [],
|
||||
columns: [],
|
||||
};
|
||||
}
|
||||
const res = await dbhan.client.query({ text: sql, rowMode: 'array' });
|
||||
const commandTimeout = options?.commandTimeout;
|
||||
if (commandTimeout) {
|
||||
await dbhan.client.query({ text: `SET statement_timeout = ${parseInt(commandTimeout)}` });
|
||||
}
|
||||
let res;
|
||||
try {
|
||||
res = await dbhan.client.query({ text: sql, rowMode: 'array' });
|
||||
} finally {
|
||||
if (commandTimeout) {
|
||||
await dbhan.client.query({ text: 'SET statement_timeout = 0' }).catch(() => {});
|
||||
}
|
||||
}
|
||||
const columns = extractPostgresColumns(res, dbhan);
|
||||
|
||||
const transormableTypeNames = Object.values(dbhan.typeIdToName ?? {});
|
||||
@@ -413,6 +428,13 @@ const drivers = driverBases.map(driverBase => ({
|
||||
return result;
|
||||
},
|
||||
|
||||
async setTransactionIsolationLevel(dbhan, level) {
|
||||
if (this.isolationLevels && level && !this.isolationLevels.includes(level)) {
|
||||
throw new Error(`Isolation level "${level}" is not supported. Supported levels: ${this.isolationLevels.join(', ')}`);
|
||||
}
|
||||
await this.query(dbhan, `SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL ${level}`);
|
||||
},
|
||||
|
||||
async listDatabasesFull(dbhan) {
|
||||
const { rows } = await this.query(dbhan, sql.listDatabases);
|
||||
return rows;
|
||||
|
||||
@@ -123,7 +123,9 @@ const dialect = {
|
||||
const postgresDriverBase = {
|
||||
...driverBase,
|
||||
supportsTransactions: true,
|
||||
isolationLevels: ['READ COMMITTED', 'REPEATABLE READ', 'SERIALIZABLE'],
|
||||
supportsIncrementalAnalysis: true,
|
||||
defaultIsolationLevel: 'READ COMMITTED',
|
||||
dumperClass: Dumper,
|
||||
dialect,
|
||||
// showConnectionField: (field, values) =>
|
||||
@@ -142,7 +144,7 @@ const postgresDriverBase = {
|
||||
databaseUrlPlaceholder: 'e.g. postgresql://user:password@localhost:5432/default_database',
|
||||
|
||||
showConnectionField: (field, values) => {
|
||||
const allowedFields = ['useDatabaseUrl', 'authType', 'user', 'isReadOnly', 'useSeparateSchemas'];
|
||||
const allowedFields = ['useDatabaseUrl', 'authType', 'user', 'isReadOnly', 'useSeparateSchemas', 'allowedDatabases', 'allowedDatabasesRegex', 'defaultIsolationLevel'];
|
||||
|
||||
if (values.authType == 'awsIam') {
|
||||
allowedFields.push('awsRegion', 'secretAccessKey', 'accessKeyId');
|
||||
@@ -424,7 +426,7 @@ const redshiftDriver = {
|
||||
databaseUrlPlaceholder: 'e.g. redshift-cluster-1.xxxx.redshift.amazonaws.com:5439/dev',
|
||||
icon: redshiftIcon,
|
||||
showConnectionField: (field, values) =>
|
||||
['databaseUrl', 'user', 'password', 'isReadOnly', 'useSeparateSchemas'].includes(field),
|
||||
['databaseUrl', 'user', 'password', 'isReadOnly', 'useSeparateSchemas', 'allowedDatabases', 'allowedDatabasesRegex'].includes(field),
|
||||
beforeConnectionSave: connection => {
|
||||
const { databaseUrl } = connection;
|
||||
if (databaseUrl) {
|
||||
|
||||
@@ -5,7 +5,7 @@ function changeDependencies(deps, version) {
|
||||
if (!deps) return;
|
||||
for (const key of Object.keys(deps)) {
|
||||
if (key.startsWith('dbgate-') && key != 'dbgate-plugin-tools' && key != 'dbgate-query-splitter') {
|
||||
deps[key] = `^${version}`;
|
||||
deps[key] = `${version}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,6 +46,7 @@ changePackageFile('packages/datalib', json.version);
|
||||
changePackageFile('packages/serve', json.version);
|
||||
changePackageFile('packages/filterparser', json.version);
|
||||
changePackageFile('packages/dbmodel', json.version);
|
||||
changePackageFile('packages/rest', json.version);
|
||||
|
||||
if (fs.existsSync('packer/azure-ubuntu.pkr.hcl')) {
|
||||
const text = fs.readFileSync('packer/azure-ubuntu.pkr.hcl', { encoding: 'utf-8' });
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "O aplikaci",
|
||||
"command.about.show": "Zobrazit",
|
||||
"command.about.toolbar": "O aplikaci",
|
||||
"command.apiQuery": "API dotaz",
|
||||
"command.app.checkForUpdates": "Zkontrolovat dostupné aktualizace",
|
||||
"command.app.disconnect": "Odpojit",
|
||||
"command.app.loggedUser": "Přihlášený uživatel",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "Návrhář",
|
||||
"command.designer.arrange": "Uspořádat",
|
||||
"command.designer.exportDiagram": "Exportovat diagram",
|
||||
"command.designer.exportDiagramPng": "Exportovat diagram jako PNG",
|
||||
"command.designer.openSql": "Otevřít SQL",
|
||||
"command.designer.remove": "Odebrat",
|
||||
"command.designer.removeSelectedTables": "Odebrat vybrané tabulky",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Složka",
|
||||
"command.folder.openData": "Otevřít složku dat",
|
||||
"command.folder.openLogs": "Otevřít logy",
|
||||
"command.gqlConnection": "GraphQL připojení",
|
||||
"command.gqlConnection.toggleCellDataView": "Přepnout zobrazení dat buňky",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Zobrazit data buňky",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Data buňky",
|
||||
"command.graphql.chat": "GraphQL chat",
|
||||
"command.internal": "Interní",
|
||||
"command.internal.loadCampaigns": "Načíst seznam kampaní",
|
||||
"command.internal.showCampaigns": "Zobrazit kampaně",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Přidat složku připojení",
|
||||
"command.new.diagram": "ER Diagram",
|
||||
"command.new.duckdbDatabase": "Nová DuckDB databáze",
|
||||
"command.new.graphqlQuery": "GraphQL dotaz",
|
||||
"command.new.jsonl": "JSON Lines",
|
||||
"command.new.markdown": "Markdown stránka",
|
||||
"command.new.modelCompare": "Porovnat DB",
|
||||
"command.new.modelTransform": "Transformace modelu",
|
||||
"command.new.newApplication": "Nová aplikace",
|
||||
"command.new.newDiagram": "Nový ER diagram",
|
||||
"command.new.newGraphqlQuery": "Nový GraphQL dotaz",
|
||||
"command.new.newJsonl": "Nový JSON lines soubor",
|
||||
"command.new.newModelTransform": "Nová transformace modelu",
|
||||
"command.new.newPerspective": "Nová perspektiva",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Uložit",
|
||||
"command.redo": "Znovu",
|
||||
"command.replace": "Nahradit",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "Spustit",
|
||||
"command.save": "Uložit",
|
||||
"command.saveAs": "Uložit jako",
|
||||
"command.saveToDisk": "Uložit na disk",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "{extension} soubory",
|
||||
"common.files.allFiles": "Všechny soubory",
|
||||
"common.general": "Obecné",
|
||||
"common.graphqlChat": "GraphQL chat",
|
||||
"common.graphqlQuery": "GraphQL dotaz",
|
||||
"common.import": "Import",
|
||||
"common.kill": "Ukončit",
|
||||
"common.loadingData": "Načítání dat",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "ID přístupového klíče",
|
||||
"connection.allowedDatabases": "Povolené databáze, jedna na řádek",
|
||||
"connection.allowedDatabasesRegex": "Regulární výraz pro povolené databáze",
|
||||
"connection.apiKeyHeader": "Hlavička API klíče",
|
||||
"connection.apiKeyValue": "Hodnota API klíče",
|
||||
"connection.apiQuery": "API dotaz",
|
||||
"connection.apiServerUrl1": "URL API serveru",
|
||||
"connection.apiServerUrl2": "URL sekundárního API serveru",
|
||||
"connection.askPassword": "Neukládat, ptát se na heslo",
|
||||
"connection.askUser": "Neukládat, ptát se na přihlašovací jméno a heslo",
|
||||
"connection.authToken": "Autorizační token",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "Soubor databáze (cesta na serveru)",
|
||||
"connection.databaseUrl": "URL databáze",
|
||||
"connection.defaultDatabase": "Výchozí databáze",
|
||||
"connection.defaultIsolationLevel": "Výchozí úroveň izolace",
|
||||
"connection.delete": "Odstranit",
|
||||
"connection.deleteConfirm": "Opravdu smazat připojení {name}?",
|
||||
"connection.deleteFolderConfirm": "Opravdu smazat složku {folder}? Připojení ve složce budou přesunuta do kořenové složky.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Typ databáze",
|
||||
"connection.engineDriverNotFound": "Ovladač databáze {engine} nebyl nalezen, zkontrolujte nainstalované pluginy a změňte typ databáze v dialogu úpravy připojení",
|
||||
"connection.fillDetails": "Vyplňte detaily připojení k databázi",
|
||||
"connection.httpProxyPassword": "Heslo HTTP proxy",
|
||||
"connection.httpProxyUrl": "URL HTTP proxy",
|
||||
"connection.httpProxyUser": "Uživatel HTTP proxy",
|
||||
"connection.isReadOnly": "Je pouze pro čtení",
|
||||
"connection.keySeparator": "Oddělovač klíčů",
|
||||
"connection.localDataCenter": "Lokální datové centrum",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "Žádná data",
|
||||
"dataForm.outOfBounds": "Mimo rozsah: {current} / {total}",
|
||||
"dataForm.rowCount": "Řádek: {current} / {total}",
|
||||
"dataForm.rowCountMany": "Řádek: {current} / mnoho",
|
||||
"dataGrid.chooseValue": "Vybrat hodnotu z {field}",
|
||||
"dataGrid.codeHighlighting": "Zvýraznění kódu:",
|
||||
"dataGrid.codeHighlighting.none": "Žádné (neformátovaný text)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Export",
|
||||
"database.exportDbModel": "Export DB model",
|
||||
"database.generateScript": "Vygenerovat skript",
|
||||
"database.graphqlChat": "GraphQL chat",
|
||||
"database.import": "Import",
|
||||
"database.newApplication": "Nová aplikace",
|
||||
"database.newCollection": "Nová kolekce/kontejner",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Otevřít dotaz",
|
||||
"datagrid.resetFilter": "Resetovat filtr",
|
||||
"datagrid.resetView": "Resetovat zobrazení",
|
||||
"datagrid.rowCountMany": "Mnoho",
|
||||
"datagrid.rows": "Řádky",
|
||||
"datagrid.searchMacros": "Hledat makra",
|
||||
"datagrid.selectedInfo.count": "Počet",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Součet",
|
||||
"datagrid.setFormat": "Nastavit formát: ",
|
||||
"datagrid.structure": "Struktura",
|
||||
"datagrid.structure.notLoaded": "Nebyla načtena žádná struktura, pravděpodobně tabulka v aktuální databázi neexistuje",
|
||||
"datagrid.structure.waiting": "Čekání na strukturu",
|
||||
"datagrid.useMacro": "Použít makro",
|
||||
"dbKeysTreeNode.deleteBranch": "Smazat větev",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Přidat všechny tabulky",
|
||||
"designer.all": "Vše",
|
||||
"designer.allKeys": "Všechny klíče",
|
||||
"designer.chooseTableColor": "Vybrat barvu tabulky",
|
||||
"designer.columnProperties": "Vlastnosti sloupce",
|
||||
"designer.columns": "Sloupce - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Datový typ: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "Soubory návrháře dotazů",
|
||||
"file.sqlFiles": "Soubory SQL",
|
||||
"file.sqliteDatabase": "Databáze SQLite",
|
||||
"files.allConnections": "Všechna připojení",
|
||||
"files.allDatabases": "všechny databáze",
|
||||
"files.allSupportedFiles": "Všechny podporované soubory",
|
||||
"files.clearConnectionFilter": "Vymazat filtr připojení",
|
||||
"files.connection": "Připojení",
|
||||
"files.currentDatabase": "Aktuální databáze",
|
||||
"files.openFile": "Otevřít soubor",
|
||||
"files.refreshFiles": "Obnovit soubory",
|
||||
"files.savedFiles": "Uložené soubory",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Hodnota",
|
||||
"indexEditor.filteredIndexCondition": "Podmínka filtrovaného indexu",
|
||||
"indexEditor.indexName": "Název indexu",
|
||||
"indexEditor.indexType": "Typ indexu",
|
||||
"indexEditor.isUnique": "Je jedinečný index",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Sloupec z",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "ER Diagram není pro aktuální databázi k dispozici",
|
||||
"newObject.exportDescription": "Exportovat do souboru jako CSV, JSON, Excel nebo jiné databáze",
|
||||
"newObject.exportDisabled": "Export není pro aktuální databázi k dispozici",
|
||||
"newObject.graphqlChatDescription": "Chatovat s GraphQL API pomocí AI",
|
||||
"newObject.graphqlChatDisabled": "GraphQL chat není pro aktuální připojení k dispozici",
|
||||
"newObject.graphqlQueryDescription": "Psát dotazy, vybírat atributy a argumenty",
|
||||
"newObject.graphqlQueryDisabled": "GraphQL dotaz není pro aktuální databázi k dispozici",
|
||||
"newObject.perspectiveDescription": "Propojte data z více databází",
|
||||
"newObject.queryDesignerDescription": "Vizuálně navrhnout SQL dotazy",
|
||||
"newObject.queryDesignerDisabled": "Návrhář dotazů není pro aktuální databázi k dispozici",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Přidat nové připojení nebo soubor",
|
||||
"privateCloudWidget.addNewFolder": "Přidat novou složku",
|
||||
"privateCloudWidget.administrateAccess": "Spravovat přístup",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud je dočasně nedostupný",
|
||||
"privateCloudWidget.createConnection": "Vytvořit připojení na DbGate Cloud",
|
||||
"privateCloudWidget.createSharedFolder": "Vytvořit sdílenou složku",
|
||||
"privateCloudWidget.deleteFolder": "Smazat složku",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Přejmenovat složku",
|
||||
"privateCloudWidget.searchPlaceholder": "Hledat cloudová připojení a soubory",
|
||||
"privateCloudWidget.yourInviteLink": "Váš pozvánkový odkaz (ve tvaru dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud je dočasně nedostupný",
|
||||
"publicCloudWidget.noFilesFound": "Pro vaši konfiguraci nebyly nalezeny žádné soubory",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Jsou zobrazeny pouze soubory relevantní pro vaše připojení, platformu a edici DbGate. Nejprve prosím definujte připojení.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Veřejná znalostní báze",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Agregace",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Sloupec/Výraz",
|
||||
"query.defaultIsolationLevel": "Výchozí",
|
||||
"query.defaultIsolationLevelNamed": "Výchozí ({level})",
|
||||
"query.download": "stáhnout",
|
||||
"query.filter": "Filtr",
|
||||
"query.groupBy": "Seskupit podle",
|
||||
"query.groupFilter": "Filtr skupiny",
|
||||
"query.isolationLevel": "Úroveň izolace",
|
||||
"query.limitRows": "Omezit na {queryRowsLimit} řádků",
|
||||
"query.named": ":proměnná",
|
||||
"query.noParameters": "(žádné parametry)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Otevřít detail při navigaci klávesnicí",
|
||||
"settings.behaviour.singleClickPreview": "Když jedním kliknutím nebo výběrem souboru v zobrazení \"Tabulky, Pohledy, Funkce\", je zobrazen v režimu náhledu a znovu používá existující kartu (karta náhledu). To je užitečné, pokud rychle procházíte tabulky a nechcete, aby každá navštívená tabulka měla svou vlastní kartu. Když začnete upravovat tabulku nebo použijete dvojklik pro otevření tabulky ze zobrazení \"Tabulky\", nová karta je věnována této tabulce.",
|
||||
"settings.behaviour.useTabPreviewMode": "Použít režim náhledu na kartě",
|
||||
"settings.checkAll": "Zaškrtnout vše / Zrušit zaškrtnutí všeho",
|
||||
"settings.confirmations": "Potvrzení",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Přeskočit potvrzení při ukládání údajů kolekce (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Přeskočit potvrzení při ukládání údajů tabulky (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Každý 3. a 6. řádek",
|
||||
"settings.dataGrid.coloringMode.none": "Žádný",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Výchozí interval automatického obnovení mřížky (v sekundách)",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Zakázat automatické otevírání zobrazení dat buňky",
|
||||
"settings.dataGrid.pageSize": "Velikost stránky (počet řádků pro inkrementální načítání, musí být mezi 5 a 50000)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Zobrazit všechny sloupce při hledání",
|
||||
"settings.dataGrid.showHintColumns": "Zobrazit nápovědu pro cizí klíče",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Kliknutí na tabulku",
|
||||
"settings.defaultActions.useLastUsedAction": "Použít naposledy použitou akci",
|
||||
"settings.defaultActions.viewClick": "Kliknutí na pohled",
|
||||
"settings.drivers": "Ovladače",
|
||||
"settings.editor.keybinds": "Klávesové zkratky editoru",
|
||||
"settings.editor.wordWrap": "Povolit zalamování textu",
|
||||
"settings.externalTools": "Externí nástroje",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "Licenční klíč je neplatný",
|
||||
"settings.other.licenseKey.valid": "Licenční klíč je platný",
|
||||
"settings.other.licenseKey.validTo": "Licence platná do:",
|
||||
"settings.other.toolBarPosition": "Pozice nástrojové lišty",
|
||||
"settings.other.toolBarPosition.bottom": "Dole",
|
||||
"settings.other.toolBarPosition.top": "Nahoře",
|
||||
"settings.session": "Dotazové relace",
|
||||
"settings.session.autoClose": "Automatické uzavření dotazových relací po období nečinnosti",
|
||||
"settings.session.autoCloseTimeout": "Interval, po kterém je dotazová relace bez aktivity uzavřena (v minutách)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "Vynechat hodnoty NULL",
|
||||
"sqlGenerator.searchTablesOrObjects": "Hledat tabulky nebo objekty",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Přeskočit autoincrement sloupec",
|
||||
"sqlGenerator.skipComputedColumns": "Přeskočit vypočítané sloupce",
|
||||
"sqlGenerator.sqlGenerator": "SQL generátor",
|
||||
"sqlGenerator.sqlTruncated": "SQL zkráceno, překročen limit velikosti souboru",
|
||||
"sqlGenerator.tables": "Tabulky",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Výchozí hodnota",
|
||||
"tableEditor.dependencies": "Závislosti",
|
||||
"tableEditor.foreignKeys": "Cizí klíče ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Typ",
|
||||
"tableEditor.indexes": "Indexy ({indexCount})",
|
||||
"tableEditor.isPersisted": "Je persistentní",
|
||||
"tableEditor.isSparse": "Je řídký",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Odstranit",
|
||||
"tableEditor.tablename": "Název tabulky",
|
||||
"tableEditor.tableproperties": "Vlastnosti tabulky",
|
||||
"tableEditor.unique": "Jedinečný",
|
||||
"tableEditor.uniqueConstraints": "Omezení jedinečnosti ({constraintCount})",
|
||||
"tableEditor.yes": "ANO",
|
||||
"tableStructure.alter": "Upravit tabulku",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Kolekce/kontejnery",
|
||||
"widget.databaseContent": "Obsah databáze",
|
||||
"widget.databases": "Databáze",
|
||||
"widget.endpoints": "Endpointy",
|
||||
"widget.keys": "Klíče",
|
||||
"widget.pinned": "Připnuté",
|
||||
"widget.tablesViewsFunctions": "Tabulky, pohledy, funkce",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "Otevřené karty",
|
||||
"widgets.premiumPromo": "Premium promo",
|
||||
"widgets.queryHistoryAndClosedTabs": "Historie dotazů a zavřené karty"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "Über DbGate",
|
||||
"command.about.show": "Anzeigen",
|
||||
"command.about.toolbar": "Über DbGate",
|
||||
"command.apiQuery": "API-Abfrage",
|
||||
"command.app.checkForUpdates": "Nach Updates suchen",
|
||||
"command.app.disconnect": "Trennen",
|
||||
"command.app.loggedUser": "Angemeldeter Benutzer",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "Designer",
|
||||
"command.designer.arrange": "Anordnen",
|
||||
"command.designer.exportDiagram": "Diagramm exportieren",
|
||||
"command.designer.exportDiagramPng": "Diagramm als PNG exportieren",
|
||||
"command.designer.openSql": "SQL öffnen",
|
||||
"command.designer.remove": "Entfernen",
|
||||
"command.designer.removeSelectedTables": "Ausgewählte Tabellen entfernen",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Ordner",
|
||||
"command.folder.openData": "Datenordner öffnen",
|
||||
"command.folder.openLogs": "Protokolle öffnen",
|
||||
"command.gqlConnection": "GraphQL-Verbindung",
|
||||
"command.gqlConnection.toggleCellDataView": "Zell-Datenansicht umschalten",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Zelldaten anzeigen",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Zelldaten",
|
||||
"command.graphql.chat": "GraphQL-Chat",
|
||||
"command.internal": "Intern",
|
||||
"command.internal.loadCampaigns": "Kampagnenliste laden",
|
||||
"command.internal.showCampaigns": "Kampagnen anzeigen",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Verbindungsordner hinzufügen",
|
||||
"command.new.diagram": "ER-Diagramm",
|
||||
"command.new.duckdbDatabase": "Neue DuckDB-Datenbank",
|
||||
"command.new.graphqlQuery": "GraphQL-Abfrage",
|
||||
"command.new.jsonl": "JSON-Zeilen",
|
||||
"command.new.markdown": "Markdown-Seite",
|
||||
"command.new.modelCompare": "DB vergleichen",
|
||||
"command.new.modelTransform": "Modell transformieren",
|
||||
"command.new.newApplication": "Neue Anwendung",
|
||||
"command.new.newDiagram": "Neues ER-Diagramm",
|
||||
"command.new.newGraphqlQuery": "Neue GraphQL-Abfrage",
|
||||
"command.new.newJsonl": "Neue JSON-Zeilen-Datei",
|
||||
"command.new.newModelTransform": "Neue Modelltransformation",
|
||||
"command.new.newPerspective": "Neue Perspektive",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Speichern",
|
||||
"command.redo": "Wiederholen",
|
||||
"command.replace": "Ersetzen",
|
||||
"command.restApi": "REST-API",
|
||||
"command.restApi.execute": "Ausführen",
|
||||
"command.save": "Speichern",
|
||||
"command.saveAs": "Speichern unter",
|
||||
"command.saveToDisk": "Auf Festplatte speichern",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "{extension}-Dateien",
|
||||
"common.files.allFiles": "Alle Dateien",
|
||||
"common.general": "Allgemein",
|
||||
"common.graphqlChat": "GraphQL-Chat",
|
||||
"common.graphqlQuery": "GraphQL-Abfrage",
|
||||
"common.import": "Importieren",
|
||||
"common.kill": "Beenden",
|
||||
"common.loadingData": "Lade Daten",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "Zugriffsschlüssel-ID",
|
||||
"connection.allowedDatabases": "Erlaubte Datenbanken, eine pro Zeile",
|
||||
"connection.allowedDatabasesRegex": "Regulärer Ausdruck für erlaubte Datenbanken",
|
||||
"connection.apiKeyHeader": "API-Schlüssel-Header",
|
||||
"connection.apiKeyValue": "API-Schlüssel-Wert",
|
||||
"connection.apiQuery": "API-Abfrage",
|
||||
"connection.apiServerUrl1": "API-Server-URL",
|
||||
"connection.apiServerUrl2": "API-sekundäre Server-URL",
|
||||
"connection.askPassword": "Nicht speichern, nach Passwort fragen",
|
||||
"connection.askUser": "Nicht speichern, nach Benutzername und Passwort fragen",
|
||||
"connection.authToken": "Auth-Token",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "Datenbankdatei (Pfad auf Server)",
|
||||
"connection.databaseUrl": "Datenbank-URL",
|
||||
"connection.defaultDatabase": "Standarddatenbank",
|
||||
"connection.defaultIsolationLevel": "Standard-Isolationsstufe",
|
||||
"connection.delete": "Löschen",
|
||||
"connection.deleteConfirm": "Verbindung {name} wirklich löschen?",
|
||||
"connection.deleteFolderConfirm": "Ordner {folder} wirklich löschen? Verbindungen im Ordner werden in den Stammordner verschoben.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Datenbank-Engine",
|
||||
"connection.engineDriverNotFound": "Engine-Treiber {engine} nicht gefunden, überprüfen Sie installierte Plugins und ändern Sie die Engine im Verbindungsbearbeitungsdialog",
|
||||
"connection.fillDetails": "Datenbankverbindungsdetails ausfüllen",
|
||||
"connection.httpProxyPassword": "HTTP-Proxy-Passwort",
|
||||
"connection.httpProxyUrl": "HTTP-Proxy-URL",
|
||||
"connection.httpProxyUser": "HTTP-Proxy-Benutzer",
|
||||
"connection.isReadOnly": "Nur Lesezugriff",
|
||||
"connection.keySeparator": "Schlüsseltrennzeichen",
|
||||
"connection.localDataCenter": "Lokales Rechenzentrum",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "Keine Daten",
|
||||
"dataForm.outOfBounds": "Außerhalb des Bereichs: {current} / {total}",
|
||||
"dataForm.rowCount": "Zeile: {current} / {total}",
|
||||
"dataForm.rowCountMany": "Zeile: {current} / Viele",
|
||||
"dataGrid.chooseValue": "Wert aus {field} auswählen",
|
||||
"dataGrid.codeHighlighting": "Code-Hervorhebung:",
|
||||
"dataGrid.codeHighlighting.none": "Keine (unformatierter Text)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Exportieren",
|
||||
"database.exportDbModel": "DB-Modell exportieren",
|
||||
"database.generateScript": "Skript generieren",
|
||||
"database.graphqlChat": "GraphQL-Chat",
|
||||
"database.import": "Importieren",
|
||||
"database.newApplication": "Neue Anwendung",
|
||||
"database.newCollection": "Neue Sammlung/Container",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Abfrage öffnen",
|
||||
"datagrid.resetFilter": "Filter zurücksetzen",
|
||||
"datagrid.resetView": "Ansicht zurücksetzen",
|
||||
"datagrid.rowCountMany": "Viele",
|
||||
"datagrid.rows": "Zeilen",
|
||||
"datagrid.searchMacros": "Makros suchen",
|
||||
"datagrid.selectedInfo.count": "Anzahl",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Summe",
|
||||
"datagrid.setFormat": "Format festlegen: ",
|
||||
"datagrid.structure": "Struktur",
|
||||
"datagrid.structure.notLoaded": "Es wurde keine Struktur geladen, wahrscheinlich existiert die Tabelle in der aktuellen Datenbank nicht",
|
||||
"datagrid.structure.waiting": "Warte auf Struktur",
|
||||
"datagrid.useMacro": "Makro verwenden",
|
||||
"dbKeysTreeNode.deleteBranch": "Zweig löschen",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Alle Tabellen hinzufügen",
|
||||
"designer.all": "Alle",
|
||||
"designer.allKeys": "Alle Schlüssel",
|
||||
"designer.chooseTableColor": "Tabellenfarbe auswählen",
|
||||
"designer.columnProperties": "Spalteneigenschaften",
|
||||
"designer.columns": "Spalten - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Datentyp: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "Abfrage-Designer-Dateien",
|
||||
"file.sqlFiles": "SQL-Dateien",
|
||||
"file.sqliteDatabase": "SQLite-Datenbank",
|
||||
"files.allConnections": "Alle Verbindungen",
|
||||
"files.allDatabases": "alle Datenbanken",
|
||||
"files.allSupportedFiles": "Alle unterstützten Dateien",
|
||||
"files.clearConnectionFilter": "Verbindungsfilter löschen",
|
||||
"files.connection": "Verbindung",
|
||||
"files.currentDatabase": "Aktuelle Datenbank",
|
||||
"files.openFile": "Datei öffnen",
|
||||
"files.refreshFiles": "Dateien aktualisieren",
|
||||
"files.savedFiles": "Gespeicherte Dateien",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Wert",
|
||||
"indexEditor.filteredIndexCondition": "Gefilterte Index-Bedingung",
|
||||
"indexEditor.indexName": "Index-Name",
|
||||
"indexEditor.indexType": "Index-Typ",
|
||||
"indexEditor.isUnique": "Ist eindeutiger Index",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Spalte von",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "ER-Diagramm ist für aktuelle Datenbank nicht verfügbar",
|
||||
"newObject.exportDescription": "In Datei wie CSV, JSON, Excel oder andere DB exportieren",
|
||||
"newObject.exportDisabled": "Export ist für aktuelle Datenbank nicht verfügbar",
|
||||
"newObject.graphqlChatDescription": "Mit Ihrer GraphQL-API per KI chatten",
|
||||
"newObject.graphqlChatDisabled": "GraphQL-Chat ist für aktuelle Verbindung nicht verfügbar",
|
||||
"newObject.graphqlQueryDescription": "Abfragen schreiben, Attribute und Argumente auswählen",
|
||||
"newObject.graphqlQueryDisabled": "GraphQL-Abfrage ist für aktuelle Datenbank nicht verfügbar",
|
||||
"newObject.perspectiveDescription": "Komplexe Daten aus mehreren Datenbanken verknüpfen",
|
||||
"newObject.queryDesignerDescription": "SQL-Abfragen visuell entwerfen",
|
||||
"newObject.queryDesignerDisabled": "Abfrage-Designer ist für aktuelle Datenbank nicht verfügbar",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Neue Verbindung oder Datei hinzufügen",
|
||||
"privateCloudWidget.addNewFolder": "Neuen Ordner hinzufügen",
|
||||
"privateCloudWidget.administrateAccess": "Zugriff verwalten",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud ist vorübergehend nicht verfügbar",
|
||||
"privateCloudWidget.createConnection": "Verbindung in DbGate Cloud erstellen",
|
||||
"privateCloudWidget.createSharedFolder": "Freigegebenen Ordner erstellen",
|
||||
"privateCloudWidget.deleteFolder": "Ordner löschen",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Ordner umbenennen",
|
||||
"privateCloudWidget.searchPlaceholder": "Cloud-Verbindungen und Dateien suchen",
|
||||
"privateCloudWidget.yourInviteLink": "Ihr Einladungslink (im Format dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud ist vorübergehend nicht verfügbar",
|
||||
"publicCloudWidget.noFilesFound": "Keine Dateien für Ihre Konfiguration gefunden",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Es werden nur Dateien aufgelistet, die für Ihre Verbindungen, Plattform und DbGate-Edition relevant sind. Bitte definieren Sie zuerst Verbindungen.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Öffentliche Wissensdatenbank",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Aggregation",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Spalte/Ausdruck",
|
||||
"query.defaultIsolationLevel": "Standard",
|
||||
"query.defaultIsolationLevelNamed": "Standard ({level})",
|
||||
"query.download": "herunterladen",
|
||||
"query.filter": "Filter",
|
||||
"query.groupBy": "Gruppieren nach",
|
||||
"query.groupFilter": "Gruppenfilter",
|
||||
"query.isolationLevel": "Isolationsstufe",
|
||||
"query.limitRows": "Auf {queryRowsLimit} Zeilen begrenzen",
|
||||
"query.named": ":Variable",
|
||||
"query.noParameters": "(keine Parameter)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Details bei Tastaturnavigation öffnen",
|
||||
"settings.behaviour.singleClickPreview": "Wenn Sie in der Ansicht \"Tabellen, Sichten, Funktionen\" einfach klicken oder eine Datei auswählen, wird sie im Vorschaumodus angezeigt und verwendet einen vorhandenen Tab (Vorschau-Tab) wieder. Dies ist nützlich, wenn Sie schnell durch Tabellen blättern und nicht jede besuchte Tabelle in einem eigenen Tab haben möchten. Wenn Sie mit der Bearbeitung der Tabelle beginnen oder per Doppelklick die Tabelle aus der \"Tabellen\"-Ansicht öffnen, wird dieser Tabelle ein neuer Tab gewidmet.",
|
||||
"settings.behaviour.useTabPreviewMode": "Tab-Vorschaumodus verwenden",
|
||||
"settings.checkAll": "Alle auswählen / Alle abwählen",
|
||||
"settings.confirmations": "Bestätigungen",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Bestätigung beim Speichern von Sammlungsdaten überspringen (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Bestätigung beim Speichern von Tabellendaten überspringen (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Jede 3. und 6. Zeile",
|
||||
"settings.dataGrid.coloringMode.none": "Keine",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Standard-Raster-Auto-Aktualisierungsintervall in Sekunden",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Automatische Zell-Datenansicht deaktivieren",
|
||||
"settings.dataGrid.pageSize": "Seitengröße (Anzahl der Zeilen für inkrementelles Laden, muss zwischen 5 und 50000 liegen)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Alle Spalten beim Suchen anzeigen",
|
||||
"settings.dataGrid.showHintColumns": "Fremdschlüssel-Hinweise anzeigen",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Klick auf Tabelle",
|
||||
"settings.defaultActions.useLastUsedAction": "Zuletzt verwendete Aktion verwenden",
|
||||
"settings.defaultActions.viewClick": "Klick auf Ansicht",
|
||||
"settings.drivers": "Treiber",
|
||||
"settings.editor.keybinds": "Editor-Tastenkombinationen",
|
||||
"settings.editor.wordWrap": "Zeilenumbruch aktivieren",
|
||||
"settings.externalTools": "Externe Werkzeuge",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "Lizenzschlüssel ist ungültig",
|
||||
"settings.other.licenseKey.valid": "Lizenzschlüssel ist gültig",
|
||||
"settings.other.licenseKey.validTo": "Lizenz gültig bis:",
|
||||
"settings.other.toolBarPosition": "Symbolleistenposition",
|
||||
"settings.other.toolBarPosition.bottom": "Unten",
|
||||
"settings.other.toolBarPosition.top": "Oben",
|
||||
"settings.session": "Abfrage-Sitzungen",
|
||||
"settings.session.autoClose": "Automatisches Schließen von Abfrage-Sitzungen nach Zeitraum ohne Aktivität",
|
||||
"settings.session.autoCloseTimeout": "Intervall, nach dem Abfrage-Sitzung ohne Aktivität geschlossen wird (in Minuten)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "NULL-Werte auslassen",
|
||||
"sqlGenerator.searchTablesOrObjects": "Tabellen oder Objekte suchen",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Autoincrement-Spalte überspringen",
|
||||
"sqlGenerator.skipComputedColumns": "Berechnete Spalten überspringen",
|
||||
"sqlGenerator.sqlGenerator": "SQL-Generator",
|
||||
"sqlGenerator.sqlTruncated": "SQL gekürzt, Dateigrößenlimit überschritten",
|
||||
"sqlGenerator.tables": "Tabellen",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Standardwert",
|
||||
"tableEditor.dependencies": "Abhängigkeiten",
|
||||
"tableEditor.foreignKeys": "Fremdschlüssel ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Typ",
|
||||
"tableEditor.indexes": "Indizes ({indexCount})",
|
||||
"tableEditor.isPersisted": "Ist persistent",
|
||||
"tableEditor.isSparse": "Sparse (dünn besetzt)",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Entfernen",
|
||||
"tableEditor.tablename": "Tabellenname",
|
||||
"tableEditor.tableproperties": "Tabelleneigenschaften",
|
||||
"tableEditor.unique": "Eindeutig",
|
||||
"tableEditor.uniqueConstraints": "Eindeutigkeitseinschränkungen ({constraintCount})",
|
||||
"tableEditor.yes": "JA",
|
||||
"tableStructure.alter": "Tabelle ändern",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Sammlungen/Container",
|
||||
"widget.databaseContent": "Datenbankinhalt",
|
||||
"widget.databases": "Datenbanken",
|
||||
"widget.endpoints": "Endpoints",
|
||||
"widget.keys": "Schlüssel",
|
||||
"widget.pinned": "Angeheftet",
|
||||
"widget.tablesViewsFunctions": "Tabellen, Ansichten, Funktionen",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "Geöffnete Tabs",
|
||||
"widgets.premiumPromo": "Premium-Werbung",
|
||||
"widgets.queryHistoryAndClosedTabs": "Abfrageverlauf & geschlossene Tabs"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "About",
|
||||
"command.about.show": "Show",
|
||||
"command.about.toolbar": "About",
|
||||
"command.apiQuery": "API Query",
|
||||
"command.app.checkForUpdates": "Check for updates",
|
||||
"command.app.disconnect": "Disconnect",
|
||||
"command.app.loggedUser": "Logged user",
|
||||
@@ -244,7 +245,8 @@
|
||||
"command.datgrid.hideColumn": "Hide column",
|
||||
"command.designer": "Designer",
|
||||
"command.designer.arrange": "Arrange",
|
||||
"command.designer.exportDiagram": "Export diagram",
|
||||
"command.designer.exportDiagram": "Export diagram as HTML",
|
||||
"command.designer.exportDiagramPng": "Export diagram as PNG",
|
||||
"command.designer.openSql": "Open SQL",
|
||||
"command.designer.remove": "Remove",
|
||||
"command.designer.removeSelectedTables": "Remove selected tables",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Folder",
|
||||
"command.folder.openData": "Open data folder",
|
||||
"command.folder.openLogs": "Open logs",
|
||||
"command.gqlConnection": "GraphQL Connection",
|
||||
"command.gqlConnection.toggleCellDataView": "Toggle cell data view",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Show cell data",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Cell Data",
|
||||
"command.graphql.chat": "GraphQL chat",
|
||||
"command.internal": "Internal",
|
||||
"command.internal.loadCampaigns": "Load campaign list",
|
||||
"command.internal.showCampaigns": "Show campaigns",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Add connection folder",
|
||||
"command.new.diagram": "ER Diagram",
|
||||
"command.new.duckdbDatabase": "New DuckDB database",
|
||||
"command.new.graphqlQuery": "GraphQL Query",
|
||||
"command.new.jsonl": "JSON Lines",
|
||||
"command.new.markdown": "Markdown page",
|
||||
"command.new.modelCompare": "Compare DB",
|
||||
"command.new.modelTransform": "Model transform",
|
||||
"command.new.newApplication": "New application",
|
||||
"command.new.newDiagram": "New ER diagram",
|
||||
"command.new.newGraphqlQuery": "New GraphQL Query",
|
||||
"command.new.newJsonl": "New JSON lines file",
|
||||
"command.new.newModelTransform": "New model transform",
|
||||
"command.new.newPerspective": "New perspective",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Save",
|
||||
"command.redo": "Redo",
|
||||
"command.replace": "Replace",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "Execute",
|
||||
"command.save": "Save",
|
||||
"command.saveAs": "Save As",
|
||||
"command.saveToDisk": "Save to disk",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "{extension} files",
|
||||
"common.files.allFiles": "All Files",
|
||||
"common.general": "General",
|
||||
"common.graphqlChat": "GraphQL Chat",
|
||||
"common.graphqlQuery": "GraphQL Query",
|
||||
"common.import": "Import",
|
||||
"common.kill": "Kill",
|
||||
"common.loadingData": "Loading data",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "Access Key ID",
|
||||
"connection.allowedDatabases": "Allowed databases, one per line",
|
||||
"connection.allowedDatabasesRegex": "Allowed databases regular expression",
|
||||
"connection.apiKeyHeader": "API Key Header",
|
||||
"connection.apiKeyValue": "API Key Value",
|
||||
"connection.apiQuery": "API Query",
|
||||
"connection.apiServerUrl1": "API Server URL",
|
||||
"connection.apiServerUrl2": "API Secondary Server URL",
|
||||
"connection.askPassword": "Don't save, ask for password",
|
||||
"connection.askUser": "Don't save, ask for login and password",
|
||||
"connection.authToken": "Auth token",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "Database file (path on server)",
|
||||
"connection.databaseUrl": "Database URL",
|
||||
"connection.defaultDatabase": "Default database",
|
||||
"connection.defaultIsolationLevel": "Default isolation level",
|
||||
"connection.delete": "Delete",
|
||||
"connection.deleteConfirm": "Really delete connection {name}?",
|
||||
"connection.deleteFolderConfirm": "Really delete folder {folder}? Connections in folder will be moved into root folder.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Database engine",
|
||||
"connection.engineDriverNotFound": "Engine driver {engine} not found, review installed plugins and change engine in edit connection dialog",
|
||||
"connection.fillDetails": "Fill database connection details",
|
||||
"connection.httpProxyPassword": "HTTP Proxy Password",
|
||||
"connection.httpProxyUrl": "HTTP Proxy URL",
|
||||
"connection.httpProxyUser": "HTTP Proxy User",
|
||||
"connection.isReadOnly": "Is read only",
|
||||
"connection.keySeparator": "Key separator",
|
||||
"connection.localDataCenter": "Local DataCenter",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "No data",
|
||||
"dataForm.outOfBounds": "Out of bounds: {current} / {total}",
|
||||
"dataForm.rowCount": "Row: {current} / {total}",
|
||||
"dataForm.rowCountMany": "Row: {current} / Many",
|
||||
"dataGrid.chooseValue": "Choose value from {field}",
|
||||
"dataGrid.codeHighlighting": "Code highlighting:",
|
||||
"dataGrid.codeHighlighting.none": "None (raw text)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Export",
|
||||
"database.exportDbModel": "Export DB model",
|
||||
"database.generateScript": "Generate script",
|
||||
"database.graphqlChat": "GraphQL chat",
|
||||
"database.import": "Import",
|
||||
"database.newApplication": "New application",
|
||||
"database.newCollection": "New collection/container",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Open Query",
|
||||
"datagrid.resetFilter": "Reset filter",
|
||||
"datagrid.resetView": "Reset view",
|
||||
"datagrid.rowCountMany": "Many",
|
||||
"datagrid.rows": "Rows",
|
||||
"datagrid.searchMacros": "Search macros",
|
||||
"datagrid.selectedInfo.count": "Count",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Sum",
|
||||
"datagrid.setFormat": "Set format: ",
|
||||
"datagrid.structure": "Structure",
|
||||
"datagrid.structure.notLoaded": "No structure was loaded, probably table doesn't exist in current database",
|
||||
"datagrid.structure.waiting": "Waiting for structure",
|
||||
"datagrid.useMacro": "Use macro",
|
||||
"dbKeysTreeNode.deleteBranch": "Delete branch",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Add all tables",
|
||||
"designer.all": "All",
|
||||
"designer.allKeys": "All Keys",
|
||||
"designer.chooseTableColor": "Choose table color",
|
||||
"designer.columnProperties": "Column properties",
|
||||
"designer.columns": "Columns - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Data type: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "Query designer files",
|
||||
"file.sqlFiles": "SQL files",
|
||||
"file.sqliteDatabase": "SQLite database",
|
||||
"files.allConnections": "All connections",
|
||||
"files.allDatabases": "all databases",
|
||||
"files.allSupportedFiles": "All supported files",
|
||||
"files.clearConnectionFilter": "Clear connection filter",
|
||||
"files.connection": "Connection",
|
||||
"files.currentDatabase": "Current database",
|
||||
"files.openFile": "Open file",
|
||||
"files.refreshFiles": "Refresh files",
|
||||
"files.savedFiles": "Saved files",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Value",
|
||||
"indexEditor.filteredIndexCondition": "Filtered index condition",
|
||||
"indexEditor.indexName": "Index name",
|
||||
"indexEditor.indexType": "Index type",
|
||||
"indexEditor.isUnique": "Is unique index",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Column from",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "ER Diagram is not available for current database",
|
||||
"newObject.exportDescription": "Export to file like CSV, JSON, Excel, or other DB",
|
||||
"newObject.exportDisabled": "Export is not available for current database",
|
||||
"newObject.graphqlChatDescription": "Chat with your GraphQL API using AI",
|
||||
"newObject.graphqlChatDisabled": "GraphQL chat is not available for current connection",
|
||||
"newObject.graphqlQueryDescription": "Write queries, choose attributes and arguments",
|
||||
"newObject.graphqlQueryDisabled": "GraphQL Query is not available for current database",
|
||||
"newObject.perspectiveDescription": "Join complex data from multiple databases",
|
||||
"newObject.queryDesignerDescription": "Design SQL queries visually",
|
||||
"newObject.queryDesignerDisabled": "Query Designer is not available for current database",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Add new connection or file",
|
||||
"privateCloudWidget.addNewFolder": "Add new folder",
|
||||
"privateCloudWidget.administrateAccess": "Administrate access",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud is temporarily unavailable",
|
||||
"privateCloudWidget.createConnection": "Create connection on DbGate Cloud",
|
||||
"privateCloudWidget.createSharedFolder": "Create shared folder",
|
||||
"privateCloudWidget.deleteFolder": "Delete folder",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Rename folder",
|
||||
"privateCloudWidget.searchPlaceholder": "Search cloud connections and files",
|
||||
"privateCloudWidget.yourInviteLink": "Your invite link (in form dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud is temporarily unavailable",
|
||||
"publicCloudWidget.noFilesFound": "No files found for your configuration",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Only files relevant for your connections, platform and DbGate edition are listed. Please define connections at first.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Public Knowledge Base",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Aggregate",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Column/Expression",
|
||||
"query.defaultIsolationLevel": "Default",
|
||||
"query.defaultIsolationLevelNamed": "Default ({level})",
|
||||
"query.download": "download",
|
||||
"query.filter": "Filter",
|
||||
"query.groupBy": "Group by",
|
||||
"query.groupFilter": "Group filter",
|
||||
"query.isolationLevel": "Isolation level",
|
||||
"query.limitRows": "Limit {queryRowsLimit} rows",
|
||||
"query.named": ":variable",
|
||||
"query.noParameters": "(no parameters)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Open detail on keyboard navigation",
|
||||
"settings.behaviour.singleClickPreview": "When you single-click or select a file in the \"Tables, Views, Functions\" view, it is shown in a preview mode and reuses an existing tab (preview tab). This is useful if you are quickly browsing tables and don\\'t want every visited table to have its own tab. When you start editing the table or use double-click to open the table from the \"Tables\" view, a new tab is dedicated to that table.",
|
||||
"settings.behaviour.useTabPreviewMode": "Use tab preview mode",
|
||||
"settings.checkAll": "Check all / Uncheck all",
|
||||
"settings.confirmations": "Confirmations",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Skip confirmation when saving collection data (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Skip confirmation when saving table data (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Every 3rd and 6th row",
|
||||
"settings.dataGrid.coloringMode.none": "None",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Default grid auto refresh interval in seconds",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Disable automatic Cell Data View",
|
||||
"settings.dataGrid.pageSize": "Page size (number of rows for incremental loading, must be between 5 and 50000)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Show all columns when searching",
|
||||
"settings.dataGrid.showHintColumns": "Show foreign key hints",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Table click",
|
||||
"settings.defaultActions.useLastUsedAction": "Use last used action",
|
||||
"settings.defaultActions.viewClick": "View click",
|
||||
"settings.drivers": "Drivers",
|
||||
"settings.editor.keybinds": "Editor keybinds",
|
||||
"settings.editor.wordWrap": "Enable word wrap",
|
||||
"settings.externalTools": "External Tools",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "License key is invalid",
|
||||
"settings.other.licenseKey.valid": "License key is valid",
|
||||
"settings.other.licenseKey.validTo": "License valid to:",
|
||||
"settings.other.toolBarPosition": "Tool bar position",
|
||||
"settings.other.toolBarPosition.bottom": "Bottom",
|
||||
"settings.other.toolBarPosition.top": "Top",
|
||||
"settings.session": "Query sessions",
|
||||
"settings.session.autoClose": "Automatic close query sessions after period without any activity",
|
||||
"settings.session.autoCloseTimeout": "Interval, after which query session without activity is closed (in minutes)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "Omit NULL values",
|
||||
"sqlGenerator.searchTablesOrObjects": "Search tables or objects",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Skip autoincrement column",
|
||||
"sqlGenerator.skipComputedColumns": "Skip computed columns",
|
||||
"sqlGenerator.sqlGenerator": "SQL Generator",
|
||||
"sqlGenerator.sqlTruncated": "SQL truncated, file size limit exceed",
|
||||
"sqlGenerator.tables": "Tables",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Default value",
|
||||
"tableEditor.dependencies": "Dependencies",
|
||||
"tableEditor.foreignKeys": "Foreign keys ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Type",
|
||||
"tableEditor.indexes": "Indexes ({indexCount})",
|
||||
"tableEditor.isPersisted": "Is Persisted",
|
||||
"tableEditor.isSparse": "Is Sparse",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Remove",
|
||||
"tableEditor.tablename": "Table name",
|
||||
"tableEditor.tableproperties": "Table properties",
|
||||
"tableEditor.unique": "Unique",
|
||||
"tableEditor.uniqueConstraints": "Unique constraints ({constraintCount})",
|
||||
"tableEditor.yes": "YES",
|
||||
"tableStructure.alter": "Alter table",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Collections/containers",
|
||||
"widget.databaseContent": "Database content",
|
||||
"widget.databases": "Databases",
|
||||
"widget.endpoints": "Endpoints",
|
||||
"widget.keys": "Keys",
|
||||
"widget.pinned": "Pinned",
|
||||
"widget.tablesViewsFunctions": "Tables, views, functions",
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "Acerca de",
|
||||
"command.about.show": "Mostrar",
|
||||
"command.about.toolbar": "Acerca de",
|
||||
"command.apiQuery": "Consulta API",
|
||||
"command.app.checkForUpdates": "Buscar actualizaciones",
|
||||
"command.app.disconnect": "Desconectar",
|
||||
"command.app.loggedUser": "Usuario conectado",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "Diseñador",
|
||||
"command.designer.arrange": "Organizar",
|
||||
"command.designer.exportDiagram": "Exportar diagrama",
|
||||
"command.designer.exportDiagramPng": "Exportar diagrama como PNG",
|
||||
"command.designer.openSql": "Abrir SQL",
|
||||
"command.designer.remove": "Eliminar",
|
||||
"command.designer.removeSelectedTables": "Eliminar tablas seleccionadas",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Carpeta",
|
||||
"command.folder.openData": "Abrir carpeta de datos",
|
||||
"command.folder.openLogs": "Abrir registros",
|
||||
"command.gqlConnection": "Conexión GraphQL",
|
||||
"command.gqlConnection.toggleCellDataView": "Alternar vista de datos de celda",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Mostrar datos de celda",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Datos de celda",
|
||||
"command.graphql.chat": "Chat GraphQL",
|
||||
"command.internal": "Interno",
|
||||
"command.internal.loadCampaigns": "Cargar lista de campañas",
|
||||
"command.internal.showCampaigns": "Mostrar campañas",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Agregar carpeta de conexión",
|
||||
"command.new.diagram": "Diagrama ER",
|
||||
"command.new.duckdbDatabase": "Nueva base de datos DuckDB",
|
||||
"command.new.graphqlQuery": "Consulta GraphQL",
|
||||
"command.new.jsonl": "Líneas JSON",
|
||||
"command.new.markdown": "Página Markdown",
|
||||
"command.new.modelCompare": "Comparar BD",
|
||||
"command.new.modelTransform": "Transformación de modelo",
|
||||
"command.new.newApplication": "Nueva aplicación",
|
||||
"command.new.newDiagram": "Nuevo diagrama ER",
|
||||
"command.new.newGraphqlQuery": "Nueva consulta GraphQL",
|
||||
"command.new.newJsonl": "Nuevo archivo de líneas JSON",
|
||||
"command.new.newModelTransform": "Nueva transformación de modelo",
|
||||
"command.new.newPerspective": "Nueva perspectiva",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Guardar",
|
||||
"command.redo": "Rehacer",
|
||||
"command.replace": "Reemplazar",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "Ejecutar",
|
||||
"command.save": "Guardar",
|
||||
"command.saveAs": "Guardar como",
|
||||
"command.saveToDisk": "Guardar en disco",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "Archivos {extension}",
|
||||
"common.files.allFiles": "Todos los archivos",
|
||||
"common.general": "General",
|
||||
"common.graphqlChat": "Chat GraphQL",
|
||||
"common.graphqlQuery": "Consulta GraphQL",
|
||||
"common.import": "Importar",
|
||||
"common.kill": "Terminar",
|
||||
"common.loadingData": "Cargando datos",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "ID de clave de acceso",
|
||||
"connection.allowedDatabases": "Bases de datos permitidas, una por línea",
|
||||
"connection.allowedDatabasesRegex": "Expresión regular de bases de datos permitidas",
|
||||
"connection.apiKeyHeader": "Encabezado de clave API",
|
||||
"connection.apiKeyValue": "Valor de clave API",
|
||||
"connection.apiQuery": "Consulta API",
|
||||
"connection.apiServerUrl1": "URL de servidor API",
|
||||
"connection.apiServerUrl2": "URL secundaria de servidor API",
|
||||
"connection.askPassword": "No guardar, preguntar por contraseña",
|
||||
"connection.askUser": "No guardar, preguntar por usuario y contraseña",
|
||||
"connection.authToken": "Token de autenticación",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "Archivo de base de datos (ruta en servidor)",
|
||||
"connection.databaseUrl": "URL de base de datos",
|
||||
"connection.defaultDatabase": "Base de datos predeterminada",
|
||||
"connection.defaultIsolationLevel": "Nivel de aislamiento predeterminado",
|
||||
"connection.delete": "Eliminar",
|
||||
"connection.deleteConfirm": "¿Realmente eliminar la conexión {name}?",
|
||||
"connection.deleteFolderConfirm": "¿Realmente eliminar la carpeta {folder}? Las conexiones en la carpeta se moverán a la carpeta raíz.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Motor de base de datos",
|
||||
"connection.engineDriverNotFound": "Controlador de motor {engine} no encontrado, revise los plugins instalados y cambie el motor en el diálogo de edición de conexión",
|
||||
"connection.fillDetails": "Completar detalles de conexión de base de datos",
|
||||
"connection.httpProxyPassword": "Contraseña de proxy HTTP",
|
||||
"connection.httpProxyUrl": "URL de proxy HTTP",
|
||||
"connection.httpProxyUser": "Usuario de proxy HTTP",
|
||||
"connection.isReadOnly": "Es solo lectura",
|
||||
"connection.keySeparator": "Separador de claves",
|
||||
"connection.localDataCenter": "Centro de datos local",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "Sin datos",
|
||||
"dataForm.outOfBounds": "Fuera de límites: {current} / {total}",
|
||||
"dataForm.rowCount": "Fila: {current} / {total}",
|
||||
"dataForm.rowCountMany": "Fila: {current} / Muchas",
|
||||
"dataGrid.chooseValue": "Elegir valor de {field}",
|
||||
"dataGrid.codeHighlighting": "Resaltado de código:",
|
||||
"dataGrid.codeHighlighting.none": "Ninguno (texto sin formato)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Exportar",
|
||||
"database.exportDbModel": "Exportar modelo de BD",
|
||||
"database.generateScript": "Generar script",
|
||||
"database.graphqlChat": "Chat GraphQL",
|
||||
"database.import": "Importar",
|
||||
"database.newApplication": "Nueva aplicación",
|
||||
"database.newCollection": "Nueva colección/contenedor",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Abrir consulta",
|
||||
"datagrid.resetFilter": "Restablecer filtro",
|
||||
"datagrid.resetView": "Restablecer vista",
|
||||
"datagrid.rowCountMany": "Muchas",
|
||||
"datagrid.rows": "Filas",
|
||||
"datagrid.searchMacros": "Buscar macros",
|
||||
"datagrid.selectedInfo.count": "Cantidad",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Suma",
|
||||
"datagrid.setFormat": "Establecer formato: ",
|
||||
"datagrid.structure": "Estructura",
|
||||
"datagrid.structure.notLoaded": "No se cargó ninguna estructura, probablemente la tabla no existe en la base de datos actual",
|
||||
"datagrid.structure.waiting": "Esperando estructura",
|
||||
"datagrid.useMacro": "Usar macro",
|
||||
"dbKeysTreeNode.deleteBranch": "Eliminar rama",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Agregar todas las tablas",
|
||||
"designer.all": "Todo",
|
||||
"designer.allKeys": "Todas las claves",
|
||||
"designer.chooseTableColor": "Elegir color de tabla",
|
||||
"designer.columnProperties": "Propiedades de columna",
|
||||
"designer.columns": "Columnas - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Tipo de dato: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "Archivos de diseñador de consultas",
|
||||
"file.sqlFiles": "Archivos SQL",
|
||||
"file.sqliteDatabase": "Base de datos SQLite",
|
||||
"files.allConnections": "Todas las conexiones",
|
||||
"files.allDatabases": "todas las bases de datos",
|
||||
"files.allSupportedFiles": "Todos los archivos soportados",
|
||||
"files.clearConnectionFilter": "Limpiar filtro de conexión",
|
||||
"files.connection": "Conexión",
|
||||
"files.currentDatabase": "Base de datos actual",
|
||||
"files.openFile": "Abrir archivo",
|
||||
"files.refreshFiles": "Refrescar archivos",
|
||||
"files.savedFiles": "Archivos guardados",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Valor",
|
||||
"indexEditor.filteredIndexCondition": "Condición de índice filtrado",
|
||||
"indexEditor.indexName": "Nombre de índice",
|
||||
"indexEditor.indexType": "Tipo de índice",
|
||||
"indexEditor.isUnique": "Es índice único",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Columna desde",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "El diagrama ER no está disponible para la base de datos actual",
|
||||
"newObject.exportDescription": "Exportar a archivo como CSV, JSON, Excel u otra BD",
|
||||
"newObject.exportDisabled": "La exportación no está disponible para la base de datos actual",
|
||||
"newObject.graphqlChatDescription": "Chatear con su API GraphQL usando IA",
|
||||
"newObject.graphqlChatDisabled": "El chat GraphQL no está disponible para la conexión actual",
|
||||
"newObject.graphqlQueryDescription": "Escribir consultas, elegir atributos y argumentos",
|
||||
"newObject.graphqlQueryDisabled": "La consulta GraphQL no está disponible para la base de datos actual",
|
||||
"newObject.perspectiveDescription": "Unir datos complejos de múltiples bases de datos",
|
||||
"newObject.queryDesignerDescription": "Diseñar consultas SQL visualmente",
|
||||
"newObject.queryDesignerDisabled": "El diseñador de consultas no está disponible para la base de datos actual",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Agregar nueva conexión o archivo",
|
||||
"privateCloudWidget.addNewFolder": "Agregar nueva carpeta",
|
||||
"privateCloudWidget.administrateAccess": "Administrar acceso",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud está temporalmente no disponible",
|
||||
"privateCloudWidget.createConnection": "Crear conexión en DbGate Cloud",
|
||||
"privateCloudWidget.createSharedFolder": "Crear carpeta compartida",
|
||||
"privateCloudWidget.deleteFolder": "Eliminar carpeta",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Renombrar carpeta",
|
||||
"privateCloudWidget.searchPlaceholder": "Buscar conexiones y archivos en la nube",
|
||||
"privateCloudWidget.yourInviteLink": "Su enlace de invitación (en forma dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud está temporalmente no disponible",
|
||||
"publicCloudWidget.noFilesFound": "No se encontraron archivos para su configuración",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Solo se listan archivos relevantes para sus conexiones, plataforma y edición de DbGate. Defina primero las conexiones.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Base de conocimiento pública",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Agregar",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Columna/Expresión",
|
||||
"query.defaultIsolationLevel": "Predeterminado",
|
||||
"query.defaultIsolationLevelNamed": "Predeterminado ({level})",
|
||||
"query.download": "descargar",
|
||||
"query.filter": "Filtro",
|
||||
"query.groupBy": "Agrupar por",
|
||||
"query.groupFilter": "Filtro de grupo",
|
||||
"query.isolationLevel": "Nivel de aislamiento",
|
||||
"query.limitRows": "Limitar {queryRowsLimit} filas",
|
||||
"query.named": ":variable",
|
||||
"query.noParameters": "(sin parámetros)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Abrir detalle en navegación con teclado",
|
||||
"settings.behaviour.singleClickPreview": "Cuando hace clic único o selecciona un archivo en la vista \"Tablas, Vistas, Funciones\", se muestra en modo de vista previa y reutiliza una pestaña existente (pestaña de vista previa). Esto es útil si está navegando rápidamente por las tablas y no desea que cada tabla visitada tenga su propia pestaña. Cuando comience a editar la tabla o use doble clic para abrir la tabla desde la vista \"Tablas\", se dedica una nueva pestaña a esa tabla.",
|
||||
"settings.behaviour.useTabPreviewMode": "Usar modo de vista previa de pestaña",
|
||||
"settings.checkAll": "Seleccionar todo / Deseleccionar todo",
|
||||
"settings.confirmations": "Confirmaciones",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Omitir confirmación al guardar datos de colección (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Omitir confirmación al guardar datos de tabla (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Cada tercera y sexta fila",
|
||||
"settings.dataGrid.coloringMode.none": "Ninguno",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Intervalo predeterminado de recarga automática de cuadrícula en segundos",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Deshabilitar vista de datos de celda automática",
|
||||
"settings.dataGrid.pageSize": "Tamaño de página (número de filas para carga incremental, debe estar entre 5 y 50000)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Mostrar todas las columnas al buscar",
|
||||
"settings.dataGrid.showHintColumns": "Mostrar sugerencias de claves foráneas",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Clic en tabla",
|
||||
"settings.defaultActions.useLastUsedAction": "Usar última acción utilizada",
|
||||
"settings.defaultActions.viewClick": "Clic en vista",
|
||||
"settings.drivers": "Controladores",
|
||||
"settings.editor.keybinds": "Atajos de teclado del editor",
|
||||
"settings.editor.wordWrap": "Habilitar ajuste de línea",
|
||||
"settings.externalTools": "Herramientas externas",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "La clave de licencia no es válida",
|
||||
"settings.other.licenseKey.valid": "La clave de licencia es válida",
|
||||
"settings.other.licenseKey.validTo": "Licencia válida hasta:",
|
||||
"settings.other.toolBarPosition": "Posición de barra de herramientas",
|
||||
"settings.other.toolBarPosition.bottom": "Abajo",
|
||||
"settings.other.toolBarPosition.top": "Arriba",
|
||||
"settings.session": "Sesiones de consulta",
|
||||
"settings.session.autoClose": "Cerrar automáticamente las sesiones de consulta después de un período sin actividad",
|
||||
"settings.session.autoCloseTimeout": "Intervalo después del cual se cierra la sesión de consulta sin actividad (en minutos)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "Omitir valores NULL",
|
||||
"sqlGenerator.searchTablesOrObjects": "Buscar tablas u objetos",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Omitir columna autoincremental",
|
||||
"sqlGenerator.skipComputedColumns": "Omitir columnas calculadas",
|
||||
"sqlGenerator.sqlGenerator": "Generador SQL",
|
||||
"sqlGenerator.sqlTruncated": "SQL truncado, se excedió el límite de tamaño de archivo",
|
||||
"sqlGenerator.tables": "Tablas",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Valor predeterminado",
|
||||
"tableEditor.dependencies": "Dependencias",
|
||||
"tableEditor.foreignKeys": "Claves foráneas ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Tipo",
|
||||
"tableEditor.indexes": "Índices ({indexCount})",
|
||||
"tableEditor.isPersisted": "Es persistente",
|
||||
"tableEditor.isSparse": "Es dispersa",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Eliminar",
|
||||
"tableEditor.tablename": "Nombre de tabla",
|
||||
"tableEditor.tableproperties": "Propiedades de la tabla",
|
||||
"tableEditor.unique": "Único",
|
||||
"tableEditor.uniqueConstraints": "Restricciones únicas ({constraintCount})",
|
||||
"tableEditor.yes": "SÍ",
|
||||
"tableStructure.alter": "Modificar tabla",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Colecciones/contenedores",
|
||||
"widget.databaseContent": "Contenido de la base de datos",
|
||||
"widget.databases": "Bases de datos",
|
||||
"widget.endpoints": "Endpoints",
|
||||
"widget.keys": "Claves",
|
||||
"widget.pinned": "Anclado",
|
||||
"widget.tablesViewsFunctions": "Tablas, vistas, funciones",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "Pestañas abiertas",
|
||||
"widgets.premiumPromo": "Promoción Premium",
|
||||
"widgets.queryHistoryAndClosedTabs": "Historial de consultas y pestañas cerradas"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "À propos",
|
||||
"command.about.show": "Afficher",
|
||||
"command.about.toolbar": "À propos",
|
||||
"command.apiQuery": "Requête API",
|
||||
"command.app.checkForUpdates": "Vérifier les mises à jour",
|
||||
"command.app.disconnect": "Déconnecter",
|
||||
"command.app.loggedUser": "Utilisateur connecté",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "Concepteur",
|
||||
"command.designer.arrange": "Organiser",
|
||||
"command.designer.exportDiagram": "Exporter le diagramme",
|
||||
"command.designer.exportDiagramPng": "Exporter le diagramme en PNG",
|
||||
"command.designer.openSql": "Ouvrir SQL",
|
||||
"command.designer.remove": "Supprimer",
|
||||
"command.designer.removeSelectedTables": "Supprimer les tables sélectionnées",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Dossier",
|
||||
"command.folder.openData": "Ouvrir le dossier de données",
|
||||
"command.folder.openLogs": "Ouvrir les journaux",
|
||||
"command.gqlConnection": "Connexion GraphQL",
|
||||
"command.gqlConnection.toggleCellDataView": "Basculer la vue des données de cellule",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Afficher les données de cellule",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Données de cellule",
|
||||
"command.graphql.chat": "Chat GraphQL",
|
||||
"command.internal": "Interne",
|
||||
"command.internal.loadCampaigns": "Charger la liste des campagnes",
|
||||
"command.internal.showCampaigns": "Afficher les campagnes",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Ajouter un dossier de connexion",
|
||||
"command.new.diagram": "Diagramme ER",
|
||||
"command.new.duckdbDatabase": "Nouvelle base de données DuckDB",
|
||||
"command.new.graphqlQuery": "Requête GraphQL",
|
||||
"command.new.jsonl": "Lignes JSON",
|
||||
"command.new.markdown": "Page Markdown",
|
||||
"command.new.modelCompare": "Comparer les bases de données",
|
||||
"command.new.modelTransform": "Transformation de modèle",
|
||||
"command.new.newApplication": "Nouvelle application",
|
||||
"command.new.newDiagram": "Nouveau diagramme ER",
|
||||
"command.new.newGraphqlQuery": "Nouvelle requête GraphQL",
|
||||
"command.new.newJsonl": "Nouveau fichier de lignes JSON",
|
||||
"command.new.newModelTransform": "Nouvelle transformation de modèle",
|
||||
"command.new.newPerspective": "Nouvelle perspective",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Enregistrer",
|
||||
"command.redo": "Rétablir",
|
||||
"command.replace": "Remplacer",
|
||||
"command.restApi": "API REST",
|
||||
"command.restApi.execute": "Exécuter",
|
||||
"command.save": "Enregistrer",
|
||||
"command.saveAs": "Enregistrer sous",
|
||||
"command.saveToDisk": "Enregistrer sur le disque",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "Fichiers {extension}",
|
||||
"common.files.allFiles": "Tous les fichiers",
|
||||
"common.general": "Général",
|
||||
"common.graphqlChat": "Chat GraphQL",
|
||||
"common.graphqlQuery": "Requête GraphQL",
|
||||
"common.import": "Importer",
|
||||
"common.kill": "Tuer",
|
||||
"common.loadingData": "Chargement des données",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "ID de clé d'accès",
|
||||
"connection.allowedDatabases": "Bases de données autorisées, une par ligne",
|
||||
"connection.allowedDatabasesRegex": "Expression régulière des bases de données autorisées",
|
||||
"connection.apiKeyHeader": "En-tête de clé API",
|
||||
"connection.apiKeyValue": "Valeur de clé API",
|
||||
"connection.apiQuery": "Requête API",
|
||||
"connection.apiServerUrl1": "URL du serveur API",
|
||||
"connection.apiServerUrl2": "URL secondaire du serveur API",
|
||||
"connection.askPassword": "Ne pas enregistrer, demander le mot de passe",
|
||||
"connection.askUser": "Ne pas enregistrer, demander l'identifiant et le mot de passe",
|
||||
"connection.authToken": "Jeton d'authentification",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "Fichier de base de données (chemin sur le serveur)",
|
||||
"connection.databaseUrl": "URL de la base de données",
|
||||
"connection.defaultDatabase": "Base de données par défaut",
|
||||
"connection.defaultIsolationLevel": "Niveau d'isolation par défaut",
|
||||
"connection.delete": "Supprimer",
|
||||
"connection.deleteConfirm": "Vraiment supprimer la connexion {name} ?",
|
||||
"connection.deleteFolderConfirm": "Vraiment supprimer le dossier {folder} ? Les connexions du dossier seront déplacées dans le dossier racine.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Moteur de base de données",
|
||||
"connection.engineDriverNotFound": "Pilote du moteur {engine} introuvable, vérifiez les plugins installés et modifiez le moteur dans la boîte de dialogue de modification de connexion",
|
||||
"connection.fillDetails": "Remplir les détails de connexion à la base de données",
|
||||
"connection.httpProxyPassword": "Mot de passe du proxy HTTP",
|
||||
"connection.httpProxyUrl": "URL du proxy HTTP",
|
||||
"connection.httpProxyUser": "Utilisateur du proxy HTTP",
|
||||
"connection.isReadOnly": "Lecture seule",
|
||||
"connection.keySeparator": "Séparateur de clé",
|
||||
"connection.localDataCenter": "Centre de données local",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "Aucune donnée",
|
||||
"dataForm.outOfBounds": "Hors limites : {current} / {total}",
|
||||
"dataForm.rowCount": "Ligne : {current} / {total}",
|
||||
"dataForm.rowCountMany": "Ligne : {current} / Plusieurs",
|
||||
"dataGrid.chooseValue": "Choisir une valeur de {field}",
|
||||
"dataGrid.codeHighlighting": "Coloration syntaxique :",
|
||||
"dataGrid.codeHighlighting.none": "Aucune (texte brut)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Exporter",
|
||||
"database.exportDbModel": "Exporter le modèle de base de données",
|
||||
"database.generateScript": "Générer un script",
|
||||
"database.graphqlChat": "Chat GraphQL",
|
||||
"database.import": "Importer",
|
||||
"database.newApplication": "Nouvelle application",
|
||||
"database.newCollection": "Nouvelle collection/conteneur",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Ouvrir la requête",
|
||||
"datagrid.resetFilter": "Réinitialiser le filtre",
|
||||
"datagrid.resetView": "Réinitialiser la vue",
|
||||
"datagrid.rowCountMany": "Plusieurs",
|
||||
"datagrid.rows": "Lignes",
|
||||
"datagrid.searchMacros": "Rechercher des macros",
|
||||
"datagrid.selectedInfo.count": "Nombre",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Somme",
|
||||
"datagrid.setFormat": "Définir le format : ",
|
||||
"datagrid.structure": "Structure",
|
||||
"datagrid.structure.notLoaded": "Aucune structure n'a été chargée, la table n'existe probablement pas dans la base de données actuelle",
|
||||
"datagrid.structure.waiting": "En attente de la structure",
|
||||
"datagrid.useMacro": "Utiliser la macro",
|
||||
"dbKeysTreeNode.deleteBranch": "Supprimer la branche",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Ajouter toutes les tables",
|
||||
"designer.all": "Tout",
|
||||
"designer.allKeys": "Toutes les clés",
|
||||
"designer.chooseTableColor": "Choisir la couleur de la table",
|
||||
"designer.columnProperties": "Propriétés de colonne",
|
||||
"designer.columns": "Colonnes - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Type de données : {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "Fichiers du concepteur de requêtes",
|
||||
"file.sqlFiles": "Fichiers SQL",
|
||||
"file.sqliteDatabase": "Base de données SQLite",
|
||||
"files.allConnections": "Toutes les connexions",
|
||||
"files.allDatabases": "toutes les bases de données",
|
||||
"files.allSupportedFiles": "Tous les fichiers pris en charge",
|
||||
"files.clearConnectionFilter": "Effacer le filtre de connexion",
|
||||
"files.connection": "Connexion",
|
||||
"files.currentDatabase": "Base de données actuelle",
|
||||
"files.openFile": "Ouvrir le fichier",
|
||||
"files.refreshFiles": "Rafraîchir les fichiers",
|
||||
"files.savedFiles": "Fichiers enregistrés",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Valeur",
|
||||
"indexEditor.filteredIndexCondition": "Condition d'index filtré",
|
||||
"indexEditor.indexName": "Nom de l'index",
|
||||
"indexEditor.indexType": "Type d'index",
|
||||
"indexEditor.isUnique": "Est un index unique",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Colonne de",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "Le diagramme ER n'est pas disponible pour la base de données actuelle",
|
||||
"newObject.exportDescription": "Exporter vers un fichier comme CSV, JSON, Excel ou autre BD",
|
||||
"newObject.exportDisabled": "L'exportation n'est pas disponible pour la base de données actuelle",
|
||||
"newObject.graphqlChatDescription": "Discuter avec votre API GraphQL à l'aide de l'IA",
|
||||
"newObject.graphqlChatDisabled": "Le chat GraphQL n'est pas disponible pour la connexion actuelle",
|
||||
"newObject.graphqlQueryDescription": "Écrire des requêtes, choisir des attributs et des arguments",
|
||||
"newObject.graphqlQueryDisabled": "La requête GraphQL n'est pas disponible pour la base de données actuelle",
|
||||
"newObject.perspectiveDescription": "Joindre des données complexes de plusieurs bases de données",
|
||||
"newObject.queryDesignerDescription": "Concevoir des requêtes SQL visuellement",
|
||||
"newObject.queryDesignerDisabled": "Le concepteur de requêtes n'est pas disponible pour la base de données actuelle",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Ajouter une nouvelle connexion ou un fichier",
|
||||
"privateCloudWidget.addNewFolder": "Ajouter un nouveau dossier",
|
||||
"privateCloudWidget.administrateAccess": "Gérer les accès",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud est temporairement indisponible",
|
||||
"privateCloudWidget.createConnection": "Créer une connexion sur DbGate Cloud",
|
||||
"privateCloudWidget.createSharedFolder": "Créer un dossier partagé",
|
||||
"privateCloudWidget.deleteFolder": "Supprimer le dossier",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Renommer le dossier",
|
||||
"privateCloudWidget.searchPlaceholder": "Rechercher des connexions et fichiers cloud",
|
||||
"privateCloudWidget.yourInviteLink": "Votre lien d'invitation (sous la forme dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud est temporairement indisponible",
|
||||
"publicCloudWidget.noFilesFound": "Aucun fichier trouvé pour votre configuration",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Seuls les fichiers pertinents pour vos connexions, votre plateforme et votre édition de DbGate sont listés. Veuillez d'abord définir des connexions.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Base de connaissances publique",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Agrégat",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Colonne/Expression",
|
||||
"query.defaultIsolationLevel": "Par défaut",
|
||||
"query.defaultIsolationLevelNamed": "Par défaut ({level})",
|
||||
"query.download": "télécharger",
|
||||
"query.filter": "Filtre",
|
||||
"query.groupBy": "Grouper par",
|
||||
"query.groupFilter": "Filtre de groupe",
|
||||
"query.isolationLevel": "Niveau d'isolation",
|
||||
"query.limitRows": "Limiter à {queryRowsLimit} lignes",
|
||||
"query.named": ":variable",
|
||||
"query.noParameters": "(aucun paramètre)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Ouvrir les détails lors de la navigation au clavier",
|
||||
"settings.behaviour.singleClickPreview": "Lorsque vous cliquez une fois ou sélectionnez un fichier dans la vue \"Tables, Vues, Fonctions\", il est affiché en mode aperçu et réutilise un onglet existant (onglet d'aperçu). Ceci est utile si vous parcourez rapidement les tables et ne voulez pas que chaque table visitée ait son propre onglet. Lorsque vous commencez à éditer la table ou utilisez le double-clic pour ouvrir la table depuis la vue \"Tables\", un nouvel onglet est dédié à cette table.",
|
||||
"settings.behaviour.useTabPreviewMode": "Utiliser le mode aperçu d'onglet",
|
||||
"settings.checkAll": "Tout cocher / Tout décocher",
|
||||
"settings.confirmations": "Confirmations",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Ignorer la confirmation lors de l'enregistrement des données de collection (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Ignorer la confirmation lors de l'enregistrement des données de table (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Chaque 3e et 6e ligne",
|
||||
"settings.dataGrid.coloringMode.none": "Aucun",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Intervalle par défaut de rafraîchissement automatique de la grille en secondes",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Désactiver l'ouverture automatique de la vue des données de cellule",
|
||||
"settings.dataGrid.pageSize": "Taille de page (nombre de lignes pour le chargement incrémental, doit être entre 5 et 50000)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Afficher toutes les colonnes lors de la recherche",
|
||||
"settings.dataGrid.showHintColumns": "Afficher les indices de clé étrangère",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Clic sur table",
|
||||
"settings.defaultActions.useLastUsedAction": "Utiliser la dernière action utilisée",
|
||||
"settings.defaultActions.viewClick": "Clic sur vue",
|
||||
"settings.drivers": "Pilotes",
|
||||
"settings.editor.keybinds": "Raccourcis clavier de l'éditeur",
|
||||
"settings.editor.wordWrap": "Activer le retour à la ligne",
|
||||
"settings.externalTools": "Outils externes",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "La clé de licence n'est pas valide",
|
||||
"settings.other.licenseKey.valid": "La clé de licence est valide",
|
||||
"settings.other.licenseKey.validTo": "Licence valide jusqu'au :",
|
||||
"settings.other.toolBarPosition": "Position de la barre d'outils",
|
||||
"settings.other.toolBarPosition.bottom": "Bas",
|
||||
"settings.other.toolBarPosition.top": "Haut",
|
||||
"settings.session": "Sessions de requêtes",
|
||||
"settings.session.autoClose": "Fermeture automatique des sessions de requêtes après une période sans activité",
|
||||
"settings.session.autoCloseTimeout": "Intervalle après lequel une session de requête sans activité est fermée (en minutes)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "Omettre les valeurs NULL",
|
||||
"sqlGenerator.searchTablesOrObjects": "Rechercher des tables ou des objets",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Ignorer la colonne auto-incrémentée",
|
||||
"sqlGenerator.skipComputedColumns": "Ignorer les colonnes calculées",
|
||||
"sqlGenerator.sqlGenerator": "Générateur SQL",
|
||||
"sqlGenerator.sqlTruncated": "SQL tronqué, taille de fichier dépassée",
|
||||
"sqlGenerator.tables": "Tables",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Valeur par défaut",
|
||||
"tableEditor.dependencies": "Dépendances",
|
||||
"tableEditor.foreignKeys": "Clés étrangères ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Type",
|
||||
"tableEditor.indexes": "Index ({indexCount})",
|
||||
"tableEditor.isPersisted": "Valeur persistée",
|
||||
"tableEditor.isSparse": "Est Sparse",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Supprimer",
|
||||
"tableEditor.tablename": "Nom de table",
|
||||
"tableEditor.tableproperties": "Propriétés de la table",
|
||||
"tableEditor.unique": "Unique",
|
||||
"tableEditor.uniqueConstraints": "Contraintes uniques ({constraintCount})",
|
||||
"tableEditor.yes": "OUI",
|
||||
"tableStructure.alter": "Modifier la table",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Collections/conteneurs",
|
||||
"widget.databaseContent": "Contenu de la base de données",
|
||||
"widget.databases": "Bases de données",
|
||||
"widget.endpoints": "Points de terminaison",
|
||||
"widget.keys": "Clés",
|
||||
"widget.pinned": "Épinglé",
|
||||
"widget.tablesViewsFunctions": "Tables, vues, fonctions",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "Onglets ouverts",
|
||||
"widgets.premiumPromo": "Promotion Premium",
|
||||
"widgets.queryHistoryAndClosedTabs": "Historique des requêtes et onglets fermés"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "Informazioni",
|
||||
"command.about.show": "Mostra",
|
||||
"command.about.toolbar": "Informazioni",
|
||||
"command.apiQuery": "Query API",
|
||||
"command.app.checkForUpdates": "Verifica aggiornamenti",
|
||||
"command.app.disconnect": "Disconnetti",
|
||||
"command.app.loggedUser": "Utente connesso",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "Designer",
|
||||
"command.designer.arrange": "Disponi",
|
||||
"command.designer.exportDiagram": "Esporta diagramma",
|
||||
"command.designer.exportDiagramPng": "Esporta diagramma come PNG",
|
||||
"command.designer.openSql": "Apri SQL",
|
||||
"command.designer.remove": "Rimuovi",
|
||||
"command.designer.removeSelectedTables": "Rimuovi tabelle selezionate",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "Cartella",
|
||||
"command.folder.openData": "Apri cartella dati",
|
||||
"command.folder.openLogs": "Apri log",
|
||||
"command.gqlConnection": "Connessione GraphQL",
|
||||
"command.gqlConnection.toggleCellDataView": "Attiva/Disattiva vista dati cella",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "Mostra dati cella",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "Dati cella",
|
||||
"command.graphql.chat": "Chat GraphQL",
|
||||
"command.internal": "Interno",
|
||||
"command.internal.loadCampaigns": "Carica elenco campagne",
|
||||
"command.internal.showCampaigns": "Mostra campagne",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "Aggiungi cartella connessioni",
|
||||
"command.new.diagram": "Diagramma ER",
|
||||
"command.new.duckdbDatabase": "Nuovo database DuckDB",
|
||||
"command.new.graphqlQuery": "Query GraphQL",
|
||||
"command.new.jsonl": "JSON Lines",
|
||||
"command.new.markdown": "Pagina Markdown",
|
||||
"command.new.modelCompare": "Confronta DB",
|
||||
"command.new.modelTransform": "Trasforma modello",
|
||||
"command.new.newApplication": "Nuova applicazione",
|
||||
"command.new.newDiagram": "Nuovo diagramma ER",
|
||||
"command.new.newGraphqlQuery": "Nuova query GraphQL",
|
||||
"command.new.newJsonl": "Nuovo file JSON lines",
|
||||
"command.new.newModelTransform": "Nuova trasformazione modello",
|
||||
"command.new.newPerspective": "Nuova prospettiva",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "Salva",
|
||||
"command.redo": "Ripeti",
|
||||
"command.replace": "Sostituisci",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "Esegui",
|
||||
"command.save": "Salva",
|
||||
"command.saveAs": "Salva come",
|
||||
"command.saveToDisk": "Salva su disco",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "File {extension}",
|
||||
"common.files.allFiles": "Tutti i file",
|
||||
"common.general": "Generale",
|
||||
"common.graphqlChat": "Chat GraphQL",
|
||||
"common.graphqlQuery": "Query GraphQL",
|
||||
"common.import": "Importa",
|
||||
"common.kill": "Termina",
|
||||
"common.loadingData": "Caricamento dati",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "Access Key ID",
|
||||
"connection.allowedDatabases": "Database consentiti, uno per riga",
|
||||
"connection.allowedDatabasesRegex": "Espressione regolare database consentiti",
|
||||
"connection.apiKeyHeader": "Header API Key",
|
||||
"connection.apiKeyValue": "Valore API Key",
|
||||
"connection.apiQuery": "Query API",
|
||||
"connection.apiServerUrl1": "URL server API",
|
||||
"connection.apiServerUrl2": "URL server API secondario",
|
||||
"connection.askPassword": "Non salvare, richiedi password",
|
||||
"connection.askUser": "Non salvare, richiedi login e password",
|
||||
"connection.authToken": "Token di autenticazione",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "File database (percorso sul server)",
|
||||
"connection.databaseUrl": "URL database",
|
||||
"connection.defaultDatabase": "Database predefinito",
|
||||
"connection.defaultIsolationLevel": "Livello di isolamento predefinito",
|
||||
"connection.delete": "Elimina",
|
||||
"connection.deleteConfirm": "Eliminare davvero la connessione {name}?",
|
||||
"connection.deleteFolderConfirm": "Eliminare davvero la cartella {folder}? Le connessioni nella cartella saranno spostate nella cartella radice.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "Motore database",
|
||||
"connection.engineDriverNotFound": "Driver motore {engine} non trovato, rivedi i plugin installati e cambia motore nella finestra di modifica connessione",
|
||||
"connection.fillDetails": "Compila dettagli connessione database",
|
||||
"connection.httpProxyPassword": "Password proxy HTTP",
|
||||
"connection.httpProxyUrl": "URL proxy HTTP",
|
||||
"connection.httpProxyUser": "Utente proxy HTTP",
|
||||
"connection.isReadOnly": "È sola lettura",
|
||||
"connection.keySeparator": "Separatore chiavi",
|
||||
"connection.localDataCenter": "DataCenter locale",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "Nessun dato",
|
||||
"dataForm.outOfBounds": "Fuori limite: {current} / {total}",
|
||||
"dataForm.rowCount": "Riga: {current} / {total}",
|
||||
"dataForm.rowCountMany": "Riga: {current} / Molte",
|
||||
"dataGrid.chooseValue": "Scegli valore da {field}",
|
||||
"dataGrid.codeHighlighting": "Evidenziazione codice:",
|
||||
"dataGrid.codeHighlighting.none": "Nessuna (testo grezzo)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "Esporta",
|
||||
"database.exportDbModel": "Esporta modello DB",
|
||||
"database.generateScript": "Genera script",
|
||||
"database.graphqlChat": "Chat GraphQL",
|
||||
"database.import": "Importa",
|
||||
"database.newApplication": "Nuova applicazione",
|
||||
"database.newCollection": "Nuova collezione/container",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "Apri query",
|
||||
"datagrid.resetFilter": "Ripristina filtro",
|
||||
"datagrid.resetView": "Ripristina vista",
|
||||
"datagrid.rowCountMany": "Molte",
|
||||
"datagrid.rows": "Righe",
|
||||
"datagrid.searchMacros": "Cerca macro",
|
||||
"datagrid.selectedInfo.count": "Conteggio",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "Somma",
|
||||
"datagrid.setFormat": "Imposta formato: ",
|
||||
"datagrid.structure": "Struttura",
|
||||
"datagrid.structure.notLoaded": "Nessuna struttura caricata, probabilmente la tabella non esiste nel database corrente",
|
||||
"datagrid.structure.waiting": "In attesa della struttura",
|
||||
"datagrid.useMacro": "Usa macro",
|
||||
"dbKeysTreeNode.deleteBranch": "Elimina ramo",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "Aggiungi tutte le tabelle",
|
||||
"designer.all": "Tutto",
|
||||
"designer.allKeys": "Tutte le chiavi",
|
||||
"designer.chooseTableColor": "Scegli colore tabella",
|
||||
"designer.columnProperties": "Proprietà colonna",
|
||||
"designer.columns": "Colonne - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "Tipo dato: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "File designer query",
|
||||
"file.sqlFiles": "File SQL",
|
||||
"file.sqliteDatabase": "Database SQLite",
|
||||
"files.allConnections": "Tutte le connessioni",
|
||||
"files.allDatabases": "tutti i database",
|
||||
"files.allSupportedFiles": "Tutti i file supportati",
|
||||
"files.clearConnectionFilter": "Cancella filtro connessione",
|
||||
"files.connection": "Connessione",
|
||||
"files.currentDatabase": "Database corrente",
|
||||
"files.openFile": "Apri file",
|
||||
"files.refreshFiles": "Aggiorna file",
|
||||
"files.savedFiles": "File salvati",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "Valore",
|
||||
"indexEditor.filteredIndexCondition": "Condizione indice filtrato",
|
||||
"indexEditor.indexName": "Nome indice",
|
||||
"indexEditor.indexType": "Tipo indice",
|
||||
"indexEditor.isUnique": "È indice univoco",
|
||||
"insertJoin.alias": "Alias",
|
||||
"insertJoin.columnFrom": "Colonna da",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "Il diagramma ER non è disponibile per il database corrente",
|
||||
"newObject.exportDescription": "Esporta in file come CSV, JSON, Excel o altro DB",
|
||||
"newObject.exportDisabled": "L'esportazione non è disponibile per il database corrente",
|
||||
"newObject.graphqlChatDescription": "Chatta con la tua API GraphQL usando l'AI",
|
||||
"newObject.graphqlChatDisabled": "La chat GraphQL non è disponibile per la connessione corrente",
|
||||
"newObject.graphqlQueryDescription": "Scrivi query, scegli attributi e argomenti",
|
||||
"newObject.graphqlQueryDisabled": "La query GraphQL non è disponibile per il database corrente",
|
||||
"newObject.perspectiveDescription": "Unisci dati complessi da più database",
|
||||
"newObject.queryDesignerDescription": "Progetta query SQL visivamente",
|
||||
"newObject.queryDesignerDisabled": "Il designer query non è disponibile per il database corrente",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "Aggiungi nuova connessione o file",
|
||||
"privateCloudWidget.addNewFolder": "Aggiungi nuova cartella",
|
||||
"privateCloudWidget.administrateAccess": "Amministra accesso",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate Cloud è temporaneamente non disponibile",
|
||||
"privateCloudWidget.createConnection": "Crea connessione su DbGate Cloud",
|
||||
"privateCloudWidget.createSharedFolder": "Crea cartella condivisa",
|
||||
"privateCloudWidget.deleteFolder": "Elimina cartella",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "Rinomina cartella",
|
||||
"privateCloudWidget.searchPlaceholder": "Cerca connessioni e file cloud",
|
||||
"privateCloudWidget.yourInviteLink": "Il tuo link di invito (nel formato dbgate://folder/xxx)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate Cloud è temporaneamente non disponibile",
|
||||
"publicCloudWidget.noFilesFound": "Nessun file trovato per la tua configurazione",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "Sono elencati solo i file rilevanti per le tue connessioni, piattaforma ed edizione DbGate. Definisci prima le connessioni.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "Knowledge Base pubblica",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "Aggrega",
|
||||
"query.alias": "Alias",
|
||||
"query.columnExpression": "Colonna/Espressione",
|
||||
"query.defaultIsolationLevel": "Predefinito",
|
||||
"query.defaultIsolationLevelNamed": "Predefinito ({level})",
|
||||
"query.download": "scarica",
|
||||
"query.filter": "Filtro",
|
||||
"query.groupBy": "Raggruppa per",
|
||||
"query.groupFilter": "Filtro gruppo",
|
||||
"query.isolationLevel": "Livello di isolamento",
|
||||
"query.limitRows": "Limita a {queryRowsLimit} righe",
|
||||
"query.named": ":variabile",
|
||||
"query.noParameters": "(nessun parametro)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "Apri dettaglio con navigazione tastiera",
|
||||
"settings.behaviour.singleClickPreview": "Quando fai un singolo clic o selezioni un file nella vista \"Tabelle, Viste, Funzioni\", viene mostrato in modalità anteprima e riutilizza una scheda esistente (scheda anteprima). Questo è utile se stai navigando rapidamente le tabelle e non vuoi che ogni tabella visitata abbia la propria scheda. Quando inizi a modificare la tabella o usi il doppio clic per aprire la tabella dalla vista \"Tabelle\", una nuova scheda viene dedicata a quella tabella.",
|
||||
"settings.behaviour.useTabPreviewMode": "Usa modalità anteprima scheda",
|
||||
"settings.checkAll": "Seleziona tutto / Deseleziona tutto",
|
||||
"settings.confirmations": "Conferme",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "Salta conferma quando salvi dati collezione (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "Salta conferma quando salvi dati tabella (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "Ogni 3a e 6a riga",
|
||||
"settings.dataGrid.coloringMode.none": "Nessuna",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "Intervallo predefinito aggiornamento automatico griglia in secondi",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "Disabilita apertura automatica Vista dati cella",
|
||||
"settings.dataGrid.pageSize": "Dimensione pagina (numero di righe per caricamento incrementale, deve essere tra 5 e 50000)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "Mostra tutte le colonne durante la ricerca",
|
||||
"settings.dataGrid.showHintColumns": "Mostra suggerimenti chiavi esterne",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "Clic tabella",
|
||||
"settings.defaultActions.useLastUsedAction": "Usa ultima azione utilizzata",
|
||||
"settings.defaultActions.viewClick": "Clic vista",
|
||||
"settings.drivers": "Driver",
|
||||
"settings.editor.keybinds": "Scorciatoie editor",
|
||||
"settings.editor.wordWrap": "Abilita a capo automatico",
|
||||
"settings.externalTools": "Strumenti esterni",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "La chiave licenza non è valida",
|
||||
"settings.other.licenseKey.valid": "La chiave licenza è valida",
|
||||
"settings.other.licenseKey.validTo": "Licenza valida fino a:",
|
||||
"settings.other.toolBarPosition": "Posizione barra strumenti",
|
||||
"settings.other.toolBarPosition.bottom": "In basso",
|
||||
"settings.other.toolBarPosition.top": "In alto",
|
||||
"settings.session": "Sessioni query",
|
||||
"settings.session.autoClose": "Chiusura automatica sessioni query dopo periodo senza attività",
|
||||
"settings.session.autoCloseTimeout": "Intervallo dopo il quale la sessione query senza attività viene chiusa (in minuti)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "Ometti valori NULL",
|
||||
"sqlGenerator.searchTablesOrObjects": "Cerca tabelle o oggetti",
|
||||
"sqlGenerator.skipAutoincrementColumn": "Salta colonna autoincrement",
|
||||
"sqlGenerator.skipComputedColumns": "Salta colonne calcolate",
|
||||
"sqlGenerator.sqlGenerator": "Generatore SQL",
|
||||
"sqlGenerator.sqlTruncated": "SQL troncato, superato limite dimensione file",
|
||||
"sqlGenerator.tables": "Tabelle",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "Valore predefinito",
|
||||
"tableEditor.dependencies": "Dipendenze",
|
||||
"tableEditor.foreignKeys": "Chiavi esterne ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "Tipo",
|
||||
"tableEditor.indexes": "Indici ({indexCount})",
|
||||
"tableEditor.isPersisted": "È persistente",
|
||||
"tableEditor.isSparse": "È Sparse",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "Rimuovi",
|
||||
"tableEditor.tablename": "Nome tabella",
|
||||
"tableEditor.tableproperties": "Proprietà tabella",
|
||||
"tableEditor.unique": "Univoco",
|
||||
"tableEditor.uniqueConstraints": "Vincoli univoci ({constraintCount})",
|
||||
"tableEditor.yes": "SÌ",
|
||||
"tableStructure.alter": "Modifica tabella",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "Collezioni/container",
|
||||
"widget.databaseContent": "Contenuto database",
|
||||
"widget.databases": "Database",
|
||||
"widget.endpoints": "Endpoint",
|
||||
"widget.keys": "Chiavi",
|
||||
"widget.pinned": "Fissate",
|
||||
"widget.tablesViewsFunctions": "Tabelle, viste, funzioni",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "Schede aperte",
|
||||
"widgets.premiumPromo": "Promo Premium",
|
||||
"widgets.queryHistoryAndClosedTabs": "Cronologia query e schede chiuse"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "について",
|
||||
"command.about.show": "表示",
|
||||
"command.about.toolbar": "について",
|
||||
"command.apiQuery": "APIクエリ",
|
||||
"command.app.checkForUpdates": "更新を確認",
|
||||
"command.app.disconnect": "切断",
|
||||
"command.app.loggedUser": "ログインユーザー",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "デザイナー",
|
||||
"command.designer.arrange": "配置",
|
||||
"command.designer.exportDiagram": "ダイアグラムをエクスポート",
|
||||
"command.designer.exportDiagramPng": "ダイアグラムをPNGとしてエクスポート",
|
||||
"command.designer.openSql": "SQLを開く",
|
||||
"command.designer.remove": "削除",
|
||||
"command.designer.removeSelectedTables": "選択したテーブルを削除",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "フォルダー",
|
||||
"command.folder.openData": "データフォルダーを開く",
|
||||
"command.folder.openLogs": "ログを開く",
|
||||
"command.gqlConnection": "GraphQL接続",
|
||||
"command.gqlConnection.toggleCellDataView": "セルデータビューの切り替え",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "セルデータを表示",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "セルデータ",
|
||||
"command.graphql.chat": "GraphQLチャット",
|
||||
"command.internal": "内部",
|
||||
"command.internal.loadCampaigns": "キャンペーンリストを読み込み",
|
||||
"command.internal.showCampaigns": "キャンペーンを表示",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "接続フォルダーを追加",
|
||||
"command.new.diagram": "ERダイアグラム",
|
||||
"command.new.duckdbDatabase": "新しいDuckDBデータベース",
|
||||
"command.new.graphqlQuery": "GraphQLクエリ",
|
||||
"command.new.jsonl": "JSON Lines",
|
||||
"command.new.markdown": "Markdownページ",
|
||||
"command.new.modelCompare": "DBを比較",
|
||||
"command.new.modelTransform": "モデル変換",
|
||||
"command.new.newApplication": "新しいアプリケーション",
|
||||
"command.new.newDiagram": "新しいERダイアグラム",
|
||||
"command.new.newGraphqlQuery": "新しいGraphQLクエリ",
|
||||
"command.new.newJsonl": "新しいJSON linesファイル",
|
||||
"command.new.newModelTransform": "新しいモデル変換",
|
||||
"command.new.newPerspective": "新しいパースペクティブ",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "保存",
|
||||
"command.redo": "やり直す",
|
||||
"command.replace": "置換",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "実行",
|
||||
"command.save": "保存",
|
||||
"command.saveAs": "名前を付けて保存",
|
||||
"command.saveToDisk": "ディスクに保存",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "{extension} ファイル",
|
||||
"common.files.allFiles": "すべてのファイル",
|
||||
"common.general": "一般",
|
||||
"common.graphqlChat": "GraphQLチャット",
|
||||
"common.graphqlQuery": "GraphQLクエリ",
|
||||
"common.import": "インポート",
|
||||
"common.kill": "強制終了",
|
||||
"common.loadingData": "データを読み込み中",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "アクセスキーID",
|
||||
"connection.allowedDatabases": "許可されたデータベース、1行に1つ",
|
||||
"connection.allowedDatabasesRegex": "許可されたデータベースの正規表現",
|
||||
"connection.apiKeyHeader": "APIキーのヘッダー",
|
||||
"connection.apiKeyValue": "APIキーの値",
|
||||
"connection.apiQuery": "APIクエリ",
|
||||
"connection.apiServerUrl1": "APIサーバーURL",
|
||||
"connection.apiServerUrl2": "APIセカンダリサーバーURL",
|
||||
"connection.askPassword": "保存せず、パスワードを問い合わせる",
|
||||
"connection.askUser": "保存せず、ログインとパスワードを問い合わせる",
|
||||
"connection.authToken": "認証トークン",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "データベースファイル(サーバー上のパス)",
|
||||
"connection.databaseUrl": "データベースURL",
|
||||
"connection.defaultDatabase": "デフォルトデータベース",
|
||||
"connection.defaultIsolationLevel": "デフォルト分離レベル",
|
||||
"connection.delete": "削除",
|
||||
"connection.deleteConfirm": "本当に接続 {name} を削除しますか?",
|
||||
"connection.deleteFolderConfirm": "本当にフォルダー {folder} を削除しますか?フォルダー内の接続はルートフォルダーに移動されます。",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "データベースエンジン",
|
||||
"connection.engineDriverNotFound": "エンジンドライバー {engine} が見つかりません。インストールされたプラグインを確認し、接続編集ダイアログでエンジンを変更してください",
|
||||
"connection.fillDetails": "データベース接続詳細を入力",
|
||||
"connection.httpProxyPassword": "HTTPプロキシパスワード",
|
||||
"connection.httpProxyUrl": "HTTPプロキシURL",
|
||||
"connection.httpProxyUser": "HTTPプロキシユーザー",
|
||||
"connection.isReadOnly": "読み取り専用",
|
||||
"connection.keySeparator": "キーセパレーター",
|
||||
"connection.localDataCenter": "ローカルデータセンター",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "データがありません",
|
||||
"dataForm.outOfBounds": "範囲外: {current} / {total}",
|
||||
"dataForm.rowCount": "行: {current} / {total}",
|
||||
"dataForm.rowCountMany": "行: {current} / 多数",
|
||||
"dataGrid.chooseValue": "{field} から値を選択",
|
||||
"dataGrid.codeHighlighting": "コードハイライト:",
|
||||
"dataGrid.codeHighlighting.none": "なし(生テキスト)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "エクスポート",
|
||||
"database.exportDbModel": "DBモデルをエクスポート",
|
||||
"database.generateScript": "スクリプトを生成",
|
||||
"database.graphqlChat": "GraphQLチャット",
|
||||
"database.import": "インポート",
|
||||
"database.newApplication": "新しいアプリケーション",
|
||||
"database.newCollection": "新しいコレクション/コンテナー",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "クエリを開く",
|
||||
"datagrid.resetFilter": "フィルターをリセット",
|
||||
"datagrid.resetView": "表示をリセット",
|
||||
"datagrid.rowCountMany": "多数",
|
||||
"datagrid.rows": "行",
|
||||
"datagrid.searchMacros": "Search macros",
|
||||
"datagrid.selectedInfo.count": "件数",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "合計",
|
||||
"datagrid.setFormat": "Set format: ",
|
||||
"datagrid.structure": "Structure",
|
||||
"datagrid.structure.notLoaded": "構造が読み込まれていません。おそらく現在のデータベースにテーブルが存在しません",
|
||||
"datagrid.structure.waiting": "構造を待機中",
|
||||
"datagrid.useMacro": "マクロを使用",
|
||||
"dbKeysTreeNode.deleteBranch": "ブランチを削除",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "すべてのテーブルを追加",
|
||||
"designer.all": "すべて",
|
||||
"designer.allKeys": "すべてのキー",
|
||||
"designer.chooseTableColor": "テーブルの色を選択",
|
||||
"designer.columnProperties": "カラムプロパティ",
|
||||
"designer.columns": "カラム - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "データ型: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "クエリデザイナーファイル",
|
||||
"file.sqlFiles": "SQLファイル",
|
||||
"file.sqliteDatabase": "SQLiteデータベース",
|
||||
"files.allConnections": "すべての接続",
|
||||
"files.allDatabases": "すべてのデータベース",
|
||||
"files.allSupportedFiles": "サポートされているすべてのファイル",
|
||||
"files.clearConnectionFilter": "接続フィルターをクリア",
|
||||
"files.connection": "接続",
|
||||
"files.currentDatabase": "現在のデータベース",
|
||||
"files.openFile": "ファイルを開く",
|
||||
"files.refreshFiles": "ファイルを更新",
|
||||
"files.savedFiles": "保存されたファイル",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "値",
|
||||
"indexEditor.filteredIndexCondition": "フィルター済みインデックス条件",
|
||||
"indexEditor.indexName": "インデックス名",
|
||||
"indexEditor.indexType": "インデックスタイプ",
|
||||
"indexEditor.isUnique": "一意インデックス",
|
||||
"insertJoin.alias": "エイリアス",
|
||||
"insertJoin.columnFrom": "元カラム",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "現在のデータベースではER図が利用できません",
|
||||
"newObject.exportDescription": "CSV、JSON、Excel、または他のDBにエクスポート",
|
||||
"newObject.exportDisabled": "現在のデータベースではエクスポートが利用できません",
|
||||
"newObject.graphqlChatDescription": "AIを使用してGraphQL APIとチャット",
|
||||
"newObject.graphqlChatDisabled": "現在の接続ではGraphQLチャットが利用できません",
|
||||
"newObject.graphqlQueryDescription": "クエリを記述し、属性と引数を選択",
|
||||
"newObject.graphqlQueryDisabled": "現在のデータベースではGraphQLクエリが利用できません",
|
||||
"newObject.perspectiveDescription": "複数のデータベースから複雑なデータを結合",
|
||||
"newObject.queryDesignerDescription": "SQLクエリをビジュアルに設計",
|
||||
"newObject.queryDesignerDisabled": "現在のデータベースではクエリデザイナーが利用できません",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "新しい接続またはファイルを追加",
|
||||
"privateCloudWidget.addNewFolder": "新しいフォルダーを追加",
|
||||
"privateCloudWidget.administrateAccess": "アクセスを管理",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGateクラウドは一時的に利用できません",
|
||||
"privateCloudWidget.createConnection": "DbGateクラウドに接続を作成",
|
||||
"privateCloudWidget.createSharedFolder": "共有フォルダーを作成",
|
||||
"privateCloudWidget.deleteFolder": "フォルダーを削除",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "フォルダー名を変更",
|
||||
"privateCloudWidget.searchPlaceholder": "クラウド接続とファイルを検索",
|
||||
"privateCloudWidget.yourInviteLink": "招待リンク(dbgate://folder/xxx の形式)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGateクラウドは一時的に利用できません",
|
||||
"publicCloudWidget.noFilesFound": "設定に該当するファイルが見つかりません",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "接続、プラットフォーム、DbGateエディションに関連するファイルのみが一覧表示されます。まず接続を定義してください。",
|
||||
"publicCloudWidget.publicKnowledgeBase": "公開ナレッジベース",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "集計",
|
||||
"query.alias": "エイリアス",
|
||||
"query.columnExpression": "カラム/式",
|
||||
"query.defaultIsolationLevel": "デフォルト",
|
||||
"query.defaultIsolationLevelNamed": "デフォルト ({level})",
|
||||
"query.download": "ダウンロード",
|
||||
"query.filter": "フィルター",
|
||||
"query.groupBy": "グループ化",
|
||||
"query.groupFilter": "グループフィルター",
|
||||
"query.isolationLevel": "分離レベル",
|
||||
"query.limitRows": "{queryRowsLimit}行に制限",
|
||||
"query.named": ":variable",
|
||||
"query.noParameters": "(パラメーターなし)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "キーボードナビゲーションで詳細を開く",
|
||||
"settings.behaviour.singleClickPreview": "\"テーブル、ビュー、関数\"ビューでファイルをシングルクリックまたは選択すると、プレビューモードで表示され、既存のタブ(プレビュータブ)が再利用されます。これは、テーブルを素早く閲覧している際に、訪れたすべてのテーブルに独自のタブを持たせたくない場合に便利です。テーブルの編集を開始するか、\"テーブル\"ビューからダブルクリックでテーブルを開くと、そのテーブル専用の新しいタブが作成されます。",
|
||||
"settings.behaviour.useTabPreviewMode": "タブプレビューモードを使用",
|
||||
"settings.checkAll": "すべてチェック / すべてのチェックを外す",
|
||||
"settings.confirmations": "確認",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "コレクションデータ保存時の確認をスキップ (NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "テーブルデータ保存時の確認をスキップ (SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "3行目と6行目ごと",
|
||||
"settings.dataGrid.coloringMode.none": "なし",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "デフォルトグリッド自動更新間隔(秒)",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "セルデータビューの自動表示を無効化",
|
||||
"settings.dataGrid.pageSize": "ページサイズ(漸進的読み込みの行数5から50000の間)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "検索時にすべてのカラムを表示",
|
||||
"settings.dataGrid.showHintColumns": "外部キーヒントを表示",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "テーブルクリック",
|
||||
"settings.defaultActions.useLastUsedAction": "前回使用した動作を使用",
|
||||
"settings.defaultActions.viewClick": "ビュークリック",
|
||||
"settings.drivers": "ドライバー",
|
||||
"settings.editor.keybinds": "エディターキーバインド",
|
||||
"settings.editor.wordWrap": "ワードラップを有効化",
|
||||
"settings.externalTools": "外部ツール",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "ライセンスキーが無効です",
|
||||
"settings.other.licenseKey.valid": "ライセンスキーは有効です",
|
||||
"settings.other.licenseKey.validTo": "ライセンス有効期限:",
|
||||
"settings.other.toolBarPosition": "ツールバーの位置",
|
||||
"settings.other.toolBarPosition.bottom": "下",
|
||||
"settings.other.toolBarPosition.top": "上",
|
||||
"settings.session": "クエリセッション",
|
||||
"settings.session.autoClose": "活動がない期間後にクエリセッションを自動的に閉じる",
|
||||
"settings.session.autoCloseTimeout": "活動がないクエリセッションを閉じるまでの間隔(分)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "NULL値を省略",
|
||||
"sqlGenerator.searchTablesOrObjects": "テーブルまたはオブジェクトを検索",
|
||||
"sqlGenerator.skipAutoincrementColumn": "自動増分カラムをスキップ",
|
||||
"sqlGenerator.skipComputedColumns": "計算カラムをスキップ",
|
||||
"sqlGenerator.sqlGenerator": "SQLジェネレーター",
|
||||
"sqlGenerator.sqlTruncated": "SQLが切り詰められました。ファイルサイズの制限を超えています",
|
||||
"sqlGenerator.tables": "テーブル",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "デフォルト値",
|
||||
"tableEditor.dependencies": "依存関係",
|
||||
"tableEditor.foreignKeys": "外部キー ({foreignKeyCount})",
|
||||
"tableEditor.indexType": "タイプ",
|
||||
"tableEditor.indexes": "インデックス ({indexCount})",
|
||||
"tableEditor.isPersisted": "永続化",
|
||||
"tableEditor.isSparse": "疎",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "削除",
|
||||
"tableEditor.tablename": "テーブル名",
|
||||
"tableEditor.tableproperties": "テーブルプロパティ",
|
||||
"tableEditor.unique": "一意",
|
||||
"tableEditor.uniqueConstraints": "一意制約 ({constraintCount})",
|
||||
"tableEditor.yes": "YES",
|
||||
"tableStructure.alter": "テーブルを変更",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "コレクション/コンテナー",
|
||||
"widget.databaseContent": "データベースの内容",
|
||||
"widget.databases": "データベース",
|
||||
"widget.endpoints": "エンドポイント",
|
||||
"widget.keys": "キー",
|
||||
"widget.pinned": "固定済み",
|
||||
"widget.tablesViewsFunctions": "テーブル、ビュー、関数",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "開いているタブ",
|
||||
"widgets.premiumPromo": "Premiumプロモーション",
|
||||
"widgets.queryHistoryAndClosedTabs": "クエリ履歴と閉じたタブ"
|
||||
}
|
||||
}
|
||||
|
||||
+52
-2
@@ -113,6 +113,7 @@
|
||||
"command.about": "정보",
|
||||
"command.about.show": "표시",
|
||||
"command.about.toolbar": "정보",
|
||||
"command.apiQuery": "API 쿼리",
|
||||
"command.app.checkForUpdates": "업데이트 확인",
|
||||
"command.app.disconnect": "연결 끊기",
|
||||
"command.app.loggedUser": "로그인 사용자",
|
||||
@@ -245,6 +246,7 @@
|
||||
"command.designer": "디자이너",
|
||||
"command.designer.arrange": "정렬",
|
||||
"command.designer.exportDiagram": "다이어그램 내보내기",
|
||||
"command.designer.exportDiagramPng": "다이어그램을 PNG로 내보내기",
|
||||
"command.designer.openSql": "SQL 열기",
|
||||
"command.designer.remove": "제거",
|
||||
"command.designer.removeSelectedTables": "선택한 테이블 제거",
|
||||
@@ -272,6 +274,11 @@
|
||||
"command.folder": "폴더",
|
||||
"command.folder.openData": "데이터 폴더 열기",
|
||||
"command.folder.openLogs": "로그 열기",
|
||||
"command.gqlConnection": "GraphQL 연결",
|
||||
"command.gqlConnection.toggleCellDataView": "셀 데이터 보기 전환",
|
||||
"command.gqlConnection.toggleCellDataView.menu": "셀 데이터 표시",
|
||||
"command.gqlConnection.toggleCellDataView.toolbar": "셀 데이터",
|
||||
"command.graphql.chat": "GraphQL 채팅",
|
||||
"command.internal": "내부",
|
||||
"command.internal.loadCampaigns": "캠페인 목록 로드",
|
||||
"command.internal.showCampaigns": "캠페인 표시",
|
||||
@@ -294,12 +301,14 @@
|
||||
"command.new.connectionFolderToolbar": "연결 폴더 추가",
|
||||
"command.new.diagram": "ER 다이어그램",
|
||||
"command.new.duckdbDatabase": "새 DuckDB 데이터베이스",
|
||||
"command.new.graphqlQuery": "GraphQL 쿼리",
|
||||
"command.new.jsonl": "JSON Lines",
|
||||
"command.new.markdown": "Markdown 페이지",
|
||||
"command.new.modelCompare": "DB 비교",
|
||||
"command.new.modelTransform": "모델 변환",
|
||||
"command.new.newApplication": "새 애플리케이션",
|
||||
"command.new.newDiagram": "새 ER 다이어그램",
|
||||
"command.new.newGraphqlQuery": "새 GraphQL 쿼리",
|
||||
"command.new.newJsonl": "새 JSON Lines 파일",
|
||||
"command.new.newModelTransform": "새 모델 변환",
|
||||
"command.new.newPerspective": "새 퍼스펙티브",
|
||||
@@ -342,6 +351,8 @@
|
||||
"command.redisLikeData.save": "저장",
|
||||
"command.redo": "다시 실행",
|
||||
"command.replace": "바꾸기",
|
||||
"command.restApi": "REST API",
|
||||
"command.restApi.execute": "실행",
|
||||
"command.save": "저장",
|
||||
"command.saveAs": "다른 이름으로 저장",
|
||||
"command.saveToDisk": "디스크에 저장",
|
||||
@@ -432,6 +443,8 @@
|
||||
"common.fileType": "{extension} 파일",
|
||||
"common.files.allFiles": "모든 파일",
|
||||
"common.general": "일반",
|
||||
"common.graphqlChat": "GraphQL 채팅",
|
||||
"common.graphqlQuery": "GraphQL 쿼리",
|
||||
"common.import": "가져오기",
|
||||
"common.kill": "중지",
|
||||
"common.loadingData": "데이터 로딩 중",
|
||||
@@ -534,6 +547,11 @@
|
||||
"connection.accessKeyId": "Access Key ID",
|
||||
"connection.allowedDatabases": "허용된 데이터베이스(한 줄에 하나씩)",
|
||||
"connection.allowedDatabasesRegex": "허용된 데이터베이스 정규식",
|
||||
"connection.apiKeyHeader": "API 키 헤더",
|
||||
"connection.apiKeyValue": "API 키 값",
|
||||
"connection.apiQuery": "API 쿼리",
|
||||
"connection.apiServerUrl1": "API 서버 URL",
|
||||
"connection.apiServerUrl2": "API 보조 서버 URL",
|
||||
"connection.askPassword": "저장하지 않고 비밀번호 묻기",
|
||||
"connection.askUser": "저장하지 않고 로그인/비밀번호 묻기",
|
||||
"connection.authToken": "인증 토큰",
|
||||
@@ -555,6 +573,7 @@
|
||||
"connection.databaseFilePath": "데이터베이스 파일(서버 경로)",
|
||||
"connection.databaseUrl": "데이터베이스 URL",
|
||||
"connection.defaultDatabase": "기본 데이터베이스",
|
||||
"connection.defaultIsolationLevel": "기본 격리 수준",
|
||||
"connection.delete": "삭제",
|
||||
"connection.deleteConfirm": "연결 {name}을(를) 정말 삭제하시겠습니까?",
|
||||
"connection.deleteFolderConfirm": "폴더 {folder}을(를) 정말 삭제하시겠습니까? 폴더 안의 연결은 루트 폴더로 이동됩니다.",
|
||||
@@ -567,6 +586,9 @@
|
||||
"connection.engine": "데이터베이스 엔진",
|
||||
"connection.engineDriverNotFound": "엔진 드라이버 {engine}을(를) 찾을 수 없습니다. 설치된 플러그인을 확인하고 연결 편집 대화상자에서 엔진을 변경하세요",
|
||||
"connection.fillDetails": "데이터베이스 연결 세부 정보 입력",
|
||||
"connection.httpProxyPassword": "HTTP 프록시 비밀번호",
|
||||
"connection.httpProxyUrl": "HTTP 프록시 URL",
|
||||
"connection.httpProxyUser": "HTTP 프록시 사용자",
|
||||
"connection.isReadOnly": "읽기 전용",
|
||||
"connection.keySeparator": "키 구분자",
|
||||
"connection.localDataCenter": "로컬 데이터센터",
|
||||
@@ -626,6 +648,7 @@
|
||||
"dataForm.noData": "데이터 없음",
|
||||
"dataForm.outOfBounds": "범위를 벗어남: {current} / {total}",
|
||||
"dataForm.rowCount": "행: {current} / {total}",
|
||||
"dataForm.rowCountMany": "행: {current} / Many",
|
||||
"dataGrid.chooseValue": "{field}에서 값 선택",
|
||||
"dataGrid.codeHighlighting": "코드 하이라이팅:",
|
||||
"dataGrid.codeHighlighting.none": "없음(원시 텍스트)",
|
||||
@@ -664,6 +687,7 @@
|
||||
"database.export": "내보내기",
|
||||
"database.exportDbModel": "DB 모델 내보내기",
|
||||
"database.generateScript": "스크립트 생성",
|
||||
"database.graphqlChat": "GraphQL 채팅",
|
||||
"database.import": "가져오기",
|
||||
"database.newApplication": "새 애플리케이션",
|
||||
"database.newCollection": "새 컬렉션/컨테이너",
|
||||
@@ -764,6 +788,7 @@
|
||||
"datagrid.openQuery": "쿼리 열기",
|
||||
"datagrid.resetFilter": "필터 초기화",
|
||||
"datagrid.resetView": "보기 초기화",
|
||||
"datagrid.rowCountMany": "Many",
|
||||
"datagrid.rows": "행",
|
||||
"datagrid.searchMacros": "매크로 검색",
|
||||
"datagrid.selectedInfo.count": "개수",
|
||||
@@ -771,6 +796,7 @@
|
||||
"datagrid.selectedInfo.sum": "합계",
|
||||
"datagrid.setFormat": "형식 설정: ",
|
||||
"datagrid.structure": "구조",
|
||||
"datagrid.structure.notLoaded": "구조가 로드되지 않았습니다. 현재 데이터베이스에 테이블이 없을 수 있습니다",
|
||||
"datagrid.structure.waiting": "구조 대기 중",
|
||||
"datagrid.useMacro": "매크로 사용",
|
||||
"dbKeysTreeNode.deleteBranch": "브랜치 삭제",
|
||||
@@ -836,6 +862,7 @@
|
||||
"designer.addAllTables": "모든 테이블 추가",
|
||||
"designer.all": "전체",
|
||||
"designer.allKeys": "모든 키",
|
||||
"designer.chooseTableColor": "테이블 색상 선택",
|
||||
"designer.columnProperties": "컬럼 속성",
|
||||
"designer.columns": "컬럼 - { filterColumns }",
|
||||
"designer.dataTypeYesNo": "데이터 타입: {show}",
|
||||
@@ -900,7 +927,12 @@
|
||||
"file.queryDesignerFiles": "쿼리 디자이너 파일",
|
||||
"file.sqlFiles": "SQL 파일",
|
||||
"file.sqliteDatabase": "SQLite 데이터베이스",
|
||||
"files.allConnections": "모든 연결",
|
||||
"files.allDatabases": "모든 데이터베이스",
|
||||
"files.allSupportedFiles": "지원되는 모든 파일",
|
||||
"files.clearConnectionFilter": "연결 필터 지우기",
|
||||
"files.connection": "연결",
|
||||
"files.currentDatabase": "현재 데이터베이스",
|
||||
"files.openFile": "파일 열기",
|
||||
"files.refreshFiles": "파일 새로 고침",
|
||||
"files.savedFiles": "저장된 파일",
|
||||
@@ -1060,6 +1092,7 @@
|
||||
"importExport.value": "값",
|
||||
"indexEditor.filteredIndexCondition": "필터 인덱스 조건",
|
||||
"indexEditor.indexName": "인덱스 이름",
|
||||
"indexEditor.indexType": "인덱스 유형",
|
||||
"indexEditor.isUnique": "고유 인덱스",
|
||||
"insertJoin.alias": "별칭",
|
||||
"insertJoin.columnFrom": "원본 컬럼",
|
||||
@@ -1135,6 +1168,10 @@
|
||||
"newObject.erDiagramDisabled": "현재 데이터베이스에서는 ER 다이어그램을 사용할 수 없습니다",
|
||||
"newObject.exportDescription": "CSV, JSON, Excel 또는 다른 DB로 내보내기",
|
||||
"newObject.exportDisabled": "현재 데이터베이스에서는 내보내기를 사용할 수 없습니다",
|
||||
"newObject.graphqlChatDescription": "AI를 사용하여 GraphQL API와 대화",
|
||||
"newObject.graphqlChatDisabled": "현재 연결에서는 GraphQL 채팅을 사용할 수 없습니다",
|
||||
"newObject.graphqlQueryDescription": "쿼리를 작성하고 속성과 인수를 선택",
|
||||
"newObject.graphqlQueryDisabled": "현재 데이터베이스에서는 GraphQL 쿼리를 사용할 수 없습니다",
|
||||
"newObject.perspectiveDescription": "여러 데이터베이스의 복잡한 데이터를 조인",
|
||||
"newObject.queryDesignerDescription": "SQL 쿼리를 시각적으로 디자인",
|
||||
"newObject.queryDesignerDisabled": "현재 데이터베이스에서는 쿼리 디자이너를 사용할 수 없습니다",
|
||||
@@ -1158,6 +1195,7 @@
|
||||
"privateCloudWidget.addNewConnectionOrFile": "새 연결 또는 파일 추가",
|
||||
"privateCloudWidget.addNewFolder": "새 폴더 추가",
|
||||
"privateCloudWidget.administrateAccess": "접근 권한 관리",
|
||||
"privateCloudWidget.cloudUnavailable": "DbGate 클라우드는 일시적으로 사용할 수 없습니다",
|
||||
"privateCloudWidget.createConnection": "DbGate 클라우드에 연결 생성",
|
||||
"privateCloudWidget.createSharedFolder": "공유 폴더 생성",
|
||||
"privateCloudWidget.deleteFolder": "폴더 삭제",
|
||||
@@ -1170,6 +1208,7 @@
|
||||
"privateCloudWidget.renameFolder": "폴더 이름 변경",
|
||||
"privateCloudWidget.searchPlaceholder": "클라우드 연결 및 파일 검색",
|
||||
"privateCloudWidget.yourInviteLink": "초대 링크(dbgate://folder/xxx 형식)",
|
||||
"publicCloudWidget.cloudUnavailable": "DbGate 클라우드는 일시적으로 사용할 수 없습니다",
|
||||
"publicCloudWidget.noFilesFound": "구성에 해당하는 파일을 찾을 수 없습니다",
|
||||
"publicCloudWidget.onlyRelevantFilesListed": "연결, 플랫폼 및 DbGate 에디션과 관련된 파일만 표시됩니다. 먼저 연결을 정의하세요.",
|
||||
"publicCloudWidget.publicKnowledgeBase": "공개 지식 베이스",
|
||||
@@ -1191,10 +1230,13 @@
|
||||
"query.aggregate": "집계",
|
||||
"query.alias": "별칭",
|
||||
"query.columnExpression": "컬럼/표현식",
|
||||
"query.defaultIsolationLevel": "기본값",
|
||||
"query.defaultIsolationLevelNamed": "기본값({level})",
|
||||
"query.download": "다운로드",
|
||||
"query.filter": "필터",
|
||||
"query.groupBy": "그룹화",
|
||||
"query.groupFilter": "그룹 필터",
|
||||
"query.isolationLevel": "격리 수준",
|
||||
"query.limitRows": "{queryRowsLimit}행 제한",
|
||||
"query.named": ":variable",
|
||||
"query.noParameters": "(매개변수 없음)",
|
||||
@@ -1287,6 +1329,7 @@
|
||||
"settings.behaviour.openDetailOnArrows": "키보드 탐색 시 상세 열기",
|
||||
"settings.behaviour.singleClickPreview": "\"테이블, 뷰, 함수\" 보기에서 파일을 단일 클릭하거나 선택하면 미리보기 모드로 표시되며 기존 탭(미리보기 탭)을 재사용합니다. 이는 테이블을 빠르게 탐색하면서 방문한 각 테이블에 별도 탭을 만들고 싶지 않을 때 유용합니다. 테이블 편집을 시작하거나 \"테이블\" 보기에서 더블 클릭하여 테이블을 열면 해당 테이블에 전용 탭이 생성됩니다.",
|
||||
"settings.behaviour.useTabPreviewMode": "탭 미리보기 모드 사용",
|
||||
"settings.checkAll": "모두 선택 / 모두 선택 해제",
|
||||
"settings.confirmations": "확인",
|
||||
"settings.confirmations.skipConfirm.collectionDataSave": "컬렉션 데이터 저장 시 확인 건너뛰기(NoSQL)",
|
||||
"settings.confirmations.skipConfirm.tableDataSave": "테이블 데이터 저장 시 확인 건너뛰기(SQL)",
|
||||
@@ -1305,6 +1348,7 @@
|
||||
"settings.dataGrid.coloringMode.36": "3번째 및 6번째 행",
|
||||
"settings.dataGrid.coloringMode.none": "없음",
|
||||
"settings.dataGrid.defaultAutoRefreshInterval": "기본 그리드 자동 새로 고침 간격(초)",
|
||||
"settings.dataGrid.disableCellDataViewAutoOpen": "셀 데이터 보기 자동 열기 비활성화",
|
||||
"settings.dataGrid.pageSize": "페이지 크기(증분 로딩 행 수, 5~50000 사이)",
|
||||
"settings.dataGrid.showAllColumnsWhenSearch": "검색 시 모든 컬럼 표시",
|
||||
"settings.dataGrid.showHintColumns": "외래 키 힌트 표시",
|
||||
@@ -1331,6 +1375,7 @@
|
||||
"settings.defaultActions.tableClick": "테이블 클릭",
|
||||
"settings.defaultActions.useLastUsedAction": "마지막 사용 동작 사용",
|
||||
"settings.defaultActions.viewClick": "뷰 클릭",
|
||||
"settings.drivers": "드라이버",
|
||||
"settings.editor.keybinds": "편집기 키 바인딩",
|
||||
"settings.editor.wordWrap": "자동 줄 바꿈 사용",
|
||||
"settings.externalTools": "외부 도구",
|
||||
@@ -1355,6 +1400,9 @@
|
||||
"settings.other.licenseKey.invalid": "라이선스 키가 유효하지 않습니다",
|
||||
"settings.other.licenseKey.valid": "라이선스 키가 유효합니다",
|
||||
"settings.other.licenseKey.validTo": "라이선스 유효 기간:",
|
||||
"settings.other.toolBarPosition": "도구 모음 위치",
|
||||
"settings.other.toolBarPosition.bottom": "아래",
|
||||
"settings.other.toolBarPosition.top": "위",
|
||||
"settings.session": "쿼리 세션",
|
||||
"settings.session.autoClose": "활동이 없는 기간 후 쿼리 세션 자동 종료",
|
||||
"settings.session.autoCloseTimeout": "활동 없는 쿼리 세션을 종료하는 간격(분)",
|
||||
@@ -1393,6 +1441,7 @@
|
||||
"sqlGenerator.omitNulls": "NULL 값 생략",
|
||||
"sqlGenerator.searchTablesOrObjects": "테이블 또는 객체 검색",
|
||||
"sqlGenerator.skipAutoincrementColumn": "자동 증가 컬럼 건너뛰기",
|
||||
"sqlGenerator.skipComputedColumns": "계산 컬럼 건너뛰기",
|
||||
"sqlGenerator.sqlGenerator": "SQL 생성기",
|
||||
"sqlGenerator.sqlTruncated": "SQL이 잘렸습니다. 파일 크기 제한을 초과했습니다",
|
||||
"sqlGenerator.tables": "테이블",
|
||||
@@ -1467,6 +1516,7 @@
|
||||
"tableEditor.defaultValue": "기본값",
|
||||
"tableEditor.dependencies": "종속성",
|
||||
"tableEditor.foreignKeys": "외래 키({foreignKeyCount})",
|
||||
"tableEditor.indexType": "유형",
|
||||
"tableEditor.indexes": "인덱스({indexCount})",
|
||||
"tableEditor.isPersisted": "Persisted 여부",
|
||||
"tableEditor.isSparse": "Sparse 여부",
|
||||
@@ -1485,7 +1535,6 @@
|
||||
"tableEditor.remove": "제거",
|
||||
"tableEditor.tablename": "테이블 이름",
|
||||
"tableEditor.tableproperties": "테이블 속성",
|
||||
"tableEditor.unique": "고유",
|
||||
"tableEditor.uniqueConstraints": "고유 제약 조건({constraintCount})",
|
||||
"tableEditor.yes": "YES",
|
||||
"tableStructure.alter": "테이블 변경",
|
||||
@@ -1521,6 +1570,7 @@
|
||||
"widget.collectionsContainers": "컬렉션/컨테이너",
|
||||
"widget.databaseContent": "데이터베이스 콘텐츠",
|
||||
"widget.databases": "데이터베이스",
|
||||
"widget.endpoints": "엔드포인트",
|
||||
"widget.keys": "키",
|
||||
"widget.pinned": "고정됨",
|
||||
"widget.tablesViewsFunctions": "테이블, 뷰, 함수",
|
||||
@@ -1535,4 +1585,4 @@
|
||||
"widgets.openedTabs": "열린 탭",
|
||||
"widgets.premiumPromo": "프리미엄 프로모션",
|
||||
"widgets.queryHistoryAndClosedTabs": "쿼리 기록 및 닫은 탭"
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user