Compare commits

...

99 Commits

Author SHA1 Message Date
SPRINX0\prochazka 6bd81cbff5 restore table - comments 2025-12-01 12:17:00 +01:00
SPRINX0\prochazka b912190c5e delete part of restore script 2025-12-01 12:07:44 +01:00
SPRINX0\prochazka 8ae64a9dcf restore script - update 2025-12-01 11:06:18 +01:00
SPRINX0\prochazka d650d91d82 table restore script WIP 2025-12-01 10:08:00 +01:00
SPRINX0\prochazka d3322a4a15 Merge branch 'feature/table-backups' 2025-11-28 16:10:18 +01:00
SPRINX0\prochazka a65842e31f table backups 2025-11-28 16:07:36 +01:00
SPRINX0\prochazka 74fde66b51 lang texts 2025-11-28 15:37:34 +01:00
SPRINX0\prochazka c3ea155a7b refresh database options 2025-11-28 15:35:38 +01:00
Jan Prochazka c4b81e3d2c Merge pull request #1275 from dbgate/feature/settings-tab
Feature/settings tab
2025-11-28 13:59:41 +01:00
Stela Augustinova 6f6ed1a741 Remove unused tip styles from various settings components 2025-11-28 13:26:45 +01:00
Stela Augustinova 311680c090 Add commands for viewing application logs and managing plugins; update settings tab integration 2025-11-28 13:23:02 +01:00
SPRINX0\prochazka 5e54aa553a skip incremental analysis test for postgres 2025-11-28 12:47:59 +01:00
SPRINX0\prochazka 6913970830 postgresql primary key loading optimalization 2025-11-28 12:32:18 +01:00
Jan Prochazka 014e453e57 removed ttable incremental analysis for postgres 2025-11-28 12:13:32 +01:00
Jan Prochazka 25b5341f76 fix tests 2025-11-28 11:54:24 +01:00
SPRINX0\prochazka 1df51f9609 skip incremental analysis check 2025-11-28 11:01:22 +01:00
Jan Prochazka 65d13189b3 postgres analyser fixed - broken loading FKs in incremental analysis 2025-11-28 10:24:53 +01:00
Jan Prochazka 0913011120 test fix 2025-11-28 08:21:04 +01:00
Stela Augustinova 30ddc18eb1 Add ai settings component to SettingsTab 2025-11-27 16:35:45 +01:00
Jan Prochazka 697d755744 test fix 2025-11-27 16:25:24 +01:00
Jan Prochazka e3f23ddc79 fixed test 2025-11-27 15:44:31 +01:00
Jan Prochazka 094acc40e8 jest reporters 2025-11-27 15:13:28 +01:00
Jan Prochazka ebd4991de8 Revert "jest reporters"
This reverts commit d04a8fad4c.
2025-11-27 15:12:15 +01:00
Jan Prochazka d04a8fad4c jest reporters 2025-11-27 14:55:40 +01:00
CI workflows cb14bffc5a chore: auto-update github workflows 2025-11-27 13:48:04 +00:00
Jan Prochazka a4c4d17381 test reporters 2025-11-27 14:47:43 +01:00
Stela Augustinova 21eb27f6e3 Added new settings components, added wrapper to all setting components 2025-11-27 14:42:24 +01:00
Jan Prochazka abf0fc7942 incremental analysis in alter table tests 2025-11-27 14:06:48 +01:00
SPRINX0\prochazka c2703edfde prettier 2025-11-27 11:03:12 +01:00
SPRINX0\prochazka d14b90ab20 foreign key editor UX 2025-11-27 10:29:33 +01:00
SPRINX0\prochazka 48f4924932 prettier format 2025-11-27 09:29:39 +01:00
Stela Augustinova 765551988a Added keyboard shortcuts to settings tab 2025-11-27 09:23:36 +01:00
Stela Augustinova 4883eb0d1b Merge branch 'master' into feature/settings-tab 2025-11-27 09:12:34 +01:00
Stela Augustinova 06a9a93d1c Added translation tag for settings 2025-11-27 09:11:33 +01:00
SPRINX0\prochazka c92a0e1d43 fixed search in keyboard shortcuts #1273 2025-11-27 08:58:11 +01:00
Stela Augustinova c6b5ee164b Added settings tab and settings components 2025-11-26 16:21:07 +01:00
SPRINX0\prochazka c65075f887 v6.7.2-alpha.1 2025-11-26 14:43:54 +01:00
SPRINX0\prochazka a1f678a3a1 additional connnection options for dbmodel 2025-11-26 14:43:18 +01:00
Jan Prochazka fe3fefaa4e Merge pull request #1272 from dbgate/feature/translation4
Feature/translation4
2025-11-26 14:12:14 +01:00
Jan Prochazka 3ccebcb0d1 Merge pull request #1271 from dbgate/feature/translation-script
Feature/translation script
2025-11-26 14:10:01 +01:00
Jan Prochazka dbbae0eef2 Merge pull request #1270 from dbgate/feature/columns-filters-panel-visibility
Feature/columns filters panel visibility
2025-11-26 09:54:50 +01:00
Stela Augustinova f11c4881f3 Update gitinore 2025-11-25 16:08:36 +01:00
Stela Augustinova 6398c6d7ce Added new translations 2025-11-25 16:03:55 +01:00
Stela Augustinova 973ce8c3a7 Load OpenAI API key 2025-11-25 16:02:44 +01:00
Stela Augustinova d971869283 Add collapsed left column store for SQL editor settings 2025-11-25 15:17:16 +01:00
SPRINX0\prochazka 987002b8f3 v6.7.1 2025-11-25 14:58:33 +01:00
SPRINX0\prochazka f73fe495a5 changelog 2025-11-25 14:57:56 +01:00
Stela Augustinova fe7b0e2bc7 Add translation script using OpenAI for missing keys (not finished) 2025-11-25 14:38:23 +01:00
Stela Augustinova 23937c54e0 Added checkbox to hide columns/filters panel by default 2025-11-25 13:37:19 +01:00
SPRINX0\prochazka b3d0fd9d2f SYNC: fixed E2e test 2025-11-25 11:09:40 +00:00
SPRINX0\prochazka 497aaf6143 v6.7.1-premium-beta.5 2025-11-25 10:34:09 +01:00
SPRINX0\prochazka 9d6db3a93b v6.7.1-beta.4 2025-11-25 10:33:47 +01:00
CI workflows 8a6f3e6809 chore: auto-update github workflows 2025-11-25 09:22:33 +00:00
CI workflows 6c419716a4 Update pro ref 2025-11-25 09:22:15 +00:00
SPRINX0\prochazka d1a769205c SYNC: support additional objects in team files 2025-11-25 09:22:02 +00:00
Stela Augustinova b784e342c9 Translation - added translation tags for import/export, connection tab 2025-11-25 10:07:41 +01:00
CI workflows 0d82fd51c7 chore: auto-update github workflows 2025-11-25 08:21:06 +00:00
CI workflows 3b4d905485 Update pro ref 2025-11-25 08:20:50 +00:00
SPRINX0\prochazka 53c63f0f4b SYNC: diagrams supported in team files 2025-11-25 08:20:39 +00:00
SPRINX0\prochazka 5553e3cd8d SYNC: Handle error when saving to team files 2025-11-24 16:35:08 +00:00
Stela Augustinova 1e195e07e0 Translation - added translation tags for import/export 2025-11-24 17:02:34 +01:00
Stela Augustinova b6f0e15951 Translation - add translation tags for logs 2025-11-24 16:48:29 +01:00
Stela Augustinova da224303fe Translation - added translation tags for widgets 2025-11-24 16:29:29 +01:00
SPRINX0\prochazka 67ee130a9e japanese localization 2025-11-24 16:20:59 +01:00
CI workflows 18d908fa63 chore: auto-update github workflows 2025-11-24 14:40:35 +00:00
CI workflows c61f58854e Update pro ref 2025-11-24 14:40:16 +00:00
SPRINX0\prochazka f789ecd2f1 SYNC: japanese settings 2025-11-24 14:40:04 +00:00
CI workflows 1fdc30804a chore: auto-update github workflows 2025-11-24 14:31:03 +00:00
CI workflows 5ba10d0acb Update pro ref 2025-11-24 14:30:50 +00:00
SPRINX0\prochazka 12803d8154 SYNC: admin settings 2025-11-24 14:30:38 +00:00
CI workflows 36c391ccff chore: auto-update github workflows 2025-11-24 14:27:45 +00:00
CI workflows 765fb6297c Update pro ref 2025-11-24 14:27:06 +00:00
SPRINX0\prochazka 66255769ad SYNC: support for browser default language 2025-11-24 14:26:54 +00:00
CI workflows 04a8d38641 chore: auto-update github workflows 2025-11-24 14:20:00 +00:00
CI workflows 859d020031 Update pro ref 2025-11-24 14:19:44 +00:00
SPRINX0\prochazka 3c541117d0 SYNC: change language for Team Premium 2025-11-24 14:19:33 +00:00
SPRINX0\prochazka 80ca2e5215 Add the LANG environment variable for the web version. #1266 2025-11-24 14:26:25 +01:00
SPRINX0\prochazka 19f2aa2997 smaller upgrade button #1244 2025-11-24 10:57:57 +01:00
Jan Prochazka ec657f30c7 Merge pull request #1268 from dbgate/feature/sort-sql
Feature/sort sql
2025-11-24 09:04:06 +01:00
SPRINX0\prochazka 7e84d495f5 sort by fix 2025-11-24 09:02:46 +01:00
SPRINX0\prochazka c3baedd93c sort tables by size/rowCount 2025-11-24 08:58:49 +01:00
SPRINX0\prochazka ae9676f744 sql object sort WIP 2025-11-21 16:37:12 +01:00
SPRINX0\prochazka 7ec156a5d1 table rows, table size in Oracle 2025-11-21 16:19:51 +01:00
SPRINX0\prochazka b80cbea1bc show mongo collection sizes #552 2025-11-21 16:07:27 +01:00
SPRINX0\prochazka 4600fa9f32 Show table size fo MySQL and Postgres #552 2025-11-21 15:56:26 +01:00
SPRINX0\prochazka 6e0b3e5cdc fixed: Check updates option no longer available in 6.7.0 #1263 2025-11-21 15:18:00 +01:00
SPRINX0\prochazka 519ff87f5d Merge branch 'feature/mongo-legacy' 2025-11-21 12:57:31 +01:00
SPRINX0\prochazka d4a363e37e v6.7.1-premium-beta.3 2025-11-21 10:46:26 +01:00
SPRINX0\prochazka a3cfc45fef legacy mongodb optimalization + test fix 2025-11-21 10:44:06 +01:00
SPRINX0\prochazka 60602e02d9 logging app language 2025-11-20 17:31:18 +01:00
SPRINX0\prochazka 44366f7872 v6.7.1-premium-beta.2 2025-11-20 16:26:40 +01:00
SPRINX0\prochazka 2f18d8c204 fixed package version 2025-11-20 16:26:26 +01:00
SPRINX0\prochazka 08efbee52b italian translation 2025-11-20 16:14:56 +01:00
SPRINX0\prochazka eac8d78c5d v6.7.1-premium-beta.1 2025-11-20 15:38:04 +01:00
SPRINX0\prochazka db73673374 Connection to MognoDB legacy 2025-11-20 15:35:09 +01:00
SPRINX0\prochazka 281cdb7264 "Show foreign key hints" showing only in premium 2025-11-20 14:18:51 +01:00
SPRINX0\prochazka 101c80d820 fixed: A MERGE statement must be terminated by a semi-colon (;), but dbgate stripped it. #1257 2025-11-20 12:59:54 +01:00
SPRINX0\prochazka 1e06f65d9e portugese (brasil) translation 2025-11-19 18:16:59 +01:00
SPRINX0\prochazka eea85709ed fixed error in auth login 2025-11-19 17:03:22 +01:00
124 changed files with 6220 additions and 474 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: f27a03d4aff5b00a009643df146a9c17bdbf7801
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
-18
View File
@@ -42,24 +42,6 @@ jobs:
run: |
cd packages/tools
yarn test:ci
- uses: tanmen/jest-reporter@v1
if: always()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-file: integration-tests/result.json
action-name: Integration tests
- uses: tanmen/jest-reporter@v1
if: always()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-file: packages/filterparser/result.json
action-name: Filter parser test results
- uses: tanmen/jest-reporter@v1
if: always()
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
result-file: packages/datalib/result.json
action-name: Datalib (perspectives) test results
services:
postgres-integr:
image: postgres
+1
View File
@@ -24,6 +24,7 @@ docker/plugins
.env.development.local
.env.test.local
.env.production.local
.env.translation
npm-debug.log*
yarn-debug.log*
+13
View File
@@ -8,6 +8,19 @@ Builds:
- linux - application for linux
- win - application for Windows
## 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
- FIXED: Check updates option no longer available in 6.7.0 #1263
- FIXED: A MERGE statement must be terminated by a semi-colon (;), but dbgate stripped it. #1257
- ADDED: Show table size #552
- ADDED: Sort tables by size and by row count
- ADDED: Connect to Legacy MongoDB (Premium) #540
- FIXED: Fixed problems in saving team files in Team Premium edition
- CHANGED: Files are by default saved to team folders in Team Premium edition
- 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
+5 -1
View File
@@ -76,6 +76,8 @@ module.exports = ({ editMenu, isMac }, currentTranslations = null) => [
{ command: 'app.zoomIn', hideDisabled: true },
{ command: 'app.zoomOut', hideDisabled: true },
{ command: 'app.zoomReset', hideDisabled: true },
{ divider: true },
{ command: 'app.showLogs', hideDisabled: true },
],
},
{
@@ -95,6 +97,8 @@ module.exports = ({ editMenu, isMac }, currentTranslations = null) => [
{ divider: true },
{ command: 'app.exportConnections', hideDisabled: true },
{ command: 'app.importConnections', hideDisabled: true },
{ divider: true },
{ command: 'app.managePlugins', hideDisabled: true },
],
},
...(isMac
@@ -118,7 +122,7 @@ module.exports = ({ editMenu, isMac }, currentTranslations = null) => [
{ command: 'tabs.changelog', hideDisabled: true },
{ command: 'about.show', hideDisabled: true },
{ divider: true },
{ command: 'file.checkForUpdates', hideDisabled: true },
{ command: 'app.checkForUpdates', hideDisabled: true },
],
},
];
+132
View File
@@ -0,0 +1,132 @@
require('dotenv').config({ path: '.env.translation' });
const fs = require('fs');
const path = require('path');
const OpenAI = require('openai');
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const translationsDir = path.join(__dirname, '../../translations');
const enFilePath = path.join(translationsDir, 'en.json');
const languageNames = {
'cs.json': 'Czech',
'de.json': 'German',
'es.json': 'Spanish',
'fr.json': 'French',
'it.json': 'Italian',
'ja.json': 'Japanese',
'pt.json': 'Portuguese',
'sk.json': 'Slovak',
'zh.json': 'Chinese'
};
// Read source (english)
const enTranslations = JSON.parse(fs.readFileSync(enFilePath, 'utf8'));
const enKeys = Object.keys(enTranslations);
// Get all translation files
const translationFiles = fs.readdirSync(translationsDir)
.filter(file => file.endsWith('.json') && file !== 'en.json')
.sort();
console.log(`Found ${enKeys.length} keys in en.json\n`);
console.log('='.repeat(80));
async function translateMissingIds({file, translations, missingIds}){
const languageName = languageNames[file];
if (!languageName) {
console.log(`No language name mapping for file: ${file}`);
return;
}
// Build object with only missing translations
const needed = {};
missingIds.forEach(key => {
needed[key] = enTranslations[key];
});
// Get all existing translations as style examples
const existingTranslations = {};
Object.keys(translations).forEach(key => {
if (translations[key] && !translations[key].startsWith('***')) {
existingTranslations[key] = {
en: enTranslations[key],
translated: translations[key]
};
}
});
const prompt = `You are a professional translator for DbGate, a database management application.
Translate the following English UI strings to ${languageName}.
IMPORTANT RULES:
1. Preserve ALL placeholders exactly as they appear: {plugin}, {columnNumber}, {0}, {1}, etc.
2. Maintain technical terminology appropriately for database software
3. Match the translation style, tone, and formality of the existing translations shown below
4. Keep the same level of brevity or verbosity as the existing translations
5. Return ONLY valid JSON - no markdown, no explanations, no code blocks
6. Use the same keys as provided
EXISTING TRANSLATIONS (for style reference):
${JSON.stringify(existingTranslations, null, 2)}
STRINGS TO TRANSLATE:
${JSON.stringify(needed, null, 2)}
Return format: {"key": "translated value", ...}`;
const response = await client.chat.completions.create({
model: 'gpt-5.1',
messages: [
{ role: 'system', content: 'You are a professional translator specializing in software localization. Match the style and tone of existing translations. Return only valid JSON.' },
{ role: 'user', content: prompt }
],
temperature: 0.2
});
let translatedJson = response.choices[0].message.content.trim();
// Remove markdown code blocks if present
translatedJson = translatedJson.replace(/^```json\n?/, '').replace(/\n?```$/, '');
return JSON.parse(translatedJson);
}
(async () => {
for (const file of translationFiles) {
const filePath = path.join(translationsDir, file);
const translations = JSON.parse(fs.readFileSync(filePath, 'utf8'));
const missingIds = enKeys.filter(key => !translations.hasOwnProperty(key) || (typeof translations[key] === 'string' && translations[key].startsWith('***')));
console.log(`\n${file.toUpperCase()}`);
console.log('-'.repeat(80));
if (missingIds.length === 0) {
console.log('✓ All translations complete!');
continue;
} else {
console.log(`Found ${missingIds.length} untranslated IDs\n`);
}
const newTranslations = await translateMissingIds({file, translations, missingIds});
if (!newTranslations) {
console.log(`Skipping file due to translation error: ${file}`);
continue;
}
for (const [key, value] of Object.entries(newTranslations)) {
translations[key] = value;
console.log(`Translated: ${key} => ${value}`);
}
fs.writeFileSync(filePath, JSON.stringify(translations, null, 2) + '\n', 'utf8');
console.log(`\n✓ Updated translations written to ${file}`);
}
console.log('\n' + '='.repeat(80));
console.log('Translation complete!\n');
})();
+1
View File
@@ -4,6 +4,7 @@ const volatilePackages = [
'@clickhouse/client',
'bson', // this package is already bundled and is used in mongodb
'mongodb',
'mongodb-old',
'mongodb-client-encryption',
'tedious',
'msnodesqlv8',
+1 -1
View File
@@ -60,7 +60,7 @@ describe('Data browser data', () => {
cy.contains('MyChinook').click();
cy.testid('SqlObjectList_search').clear().type('album');
cy.contains('Tables (1/11)');
cy.contains('347 rows, InnoDB');
cy.contains('347 rows, 65.5 KB, InnoDB');
cy.testid('SqlObjectList_searchMenuDropDown').click();
cy.contains('Column name').click();
cy.contains('Tables (2/11)');
@@ -12,6 +12,7 @@ const {
} = require('dbgate-tools');
function pickImportantTableInfo(engine, table) {
if (!table) return table;
const props = ['columnName', 'defaultValue'];
if (!engine.skipNullability) props.push('notNull');
if (!engine.skipAutoIncrement) props.push('autoIncrement');
@@ -25,6 +26,13 @@ function pickImportantTableInfo(engine, table) {
.map(props =>
_.omitBy(props, (v, k) => k == 'defaultValue' && v == 'NULL' && engine.setNullDefaultInsteadOfDrop)
),
// foreignKeys: table.foreignKeys
// .sort((a, b) => a.refTableName.localeCompare(b.refTableName))
// .map(fk => ({
// constraintType: fk.constraintType,
// refTableName: fk.refTableName,
// columns: fk.columns.map(col => ({ columnName: col.columnName, refColumnName: col.refColumnName })),
// })),
};
}
@@ -33,7 +41,7 @@ function checkTableStructure(engine, t1, t2) {
expect(pickImportantTableInfo(engine, t1)).toEqual(pickImportantTableInfo(engine, t2));
}
async function testTableDiff(engine, conn, driver, mangle) {
async function testTableDiff(engine, conn, driver, mangle, changedTable = 't1') {
const initQuery = formatQueryWithoutParams(driver, `create table ~t0 (~id int not null primary key)`);
await driver.query(conn, transformSqlForEngine(engine, initQuery));
@@ -68,17 +76,38 @@ async function testTableDiff(engine, conn, driver, mangle) {
await driver.query(conn, transformSqlForEngine(engine, query));
}
const tget = x => x.tables.find(y => y.pureName == 't1');
const structure1 = generateDbPairingId(extendDatabaseInfo(await driver.analyseFull(conn)));
if (!engine.skipReferences) {
const query = formatQueryWithoutParams(
driver,
`create table ~t3 (~id int not null primary key, ~fkval int ${
driver.dialect.implicitNullDeclaration ? '' : 'null'
})`
);
await driver.query(conn, transformSqlForEngine(engine, query));
}
const tget = x => x?.tables?.find(y => y.pureName == changedTable);
const structure1Source = await driver.analyseFull(conn);
const structure1 = generateDbPairingId(extendDatabaseInfo(structure1Source));
let structure2 = _.cloneDeep(structure1);
mangle(tget(structure2));
structure2 = extendDatabaseInfo(structure2);
const { sql } = getAlterTableScript(tget(structure1), tget(structure2), {}, structure1, structure2, driver);
// sleep 1s - some engines have update datetime precision only to seconds
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('RUNNING ALTER SQL', driver.engine, ':', sql);
await driver.script(conn, sql);
// if (!engine.skipIncrementalAnalysis) {
// const structure2RealIncremental = await driver.analyseIncremental(conn, structure1Source);
// checkTableStructure(engine, tget(structure2RealIncremental), tget(structure2));
// }
const structure2Real = extendDatabaseInfo(await driver.analyseFull(conn));
checkTableStructure(engine, tget(structure2Real), tget(structure2));
@@ -214,6 +243,48 @@ describe('Alter table', () => {
})
);
test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))(
'Drop FK - %s',
testWrapper(async (conn, driver, engine) => {
await testTableDiff(
engine,
conn,
driver,
tbl => {
tbl.foreignKeys = [];
},
't2'
);
})
);
test.each(engines.filter(x => !x.skipReferences).map(engine => [engine.label, engine]))(
'Create FK - %s',
testWrapper(async (conn, driver, engine) => {
await testTableDiff(
engine,
conn,
driver,
tbl => {
tbl.foreignKeys = [
{
constraintType: 'foreignKey',
pureName: 't3',
refTableName: 't1',
columns: [
{
columnName: 'fkval',
refColumnName: 'col_ref',
},
],
},
];
},
't3'
);
})
);
// test.each(engines.map(engine => [engine.label, engine]))(
// 'Change autoincrement - %s',
// testWrapper(async (conn, driver, engine) => {
+4 -3
View File
@@ -187,6 +187,7 @@ const mariaDbEngine = {
/** @type {import('dbgate-types').TestEngineInfo} */
const postgreSqlEngine = {
label: 'PostgreSQL',
skipIncrementalAnalysis: true,
connection: {
engine: 'postgres@dbgate-plugin-postgres',
password: 'Pwd2020Db',
@@ -757,11 +758,11 @@ const enginesOnCi = [
const enginesOnLocal = [
// all engines, which would be run on local test
// cassandraEngine,
//mysqlEngine,
// mysqlEngine,
// mariaDbEngine,
//postgreSqlEngine,
postgreSqlEngine,
//sqlServerEngine,
sqliteEngine,
// sqliteEngine,
// cockroachDbEngine,
// clickhouseEngine,
// libsqlFileEngine,
+1
View File
@@ -1,3 +1,4 @@
module.exports = {
setupFilesAfterEnv: ['<rootDir>/setupTests.js'],
reporters: ['default', 'github-actions'],
};
+1 -1
View File
@@ -18,7 +18,7 @@
},
"devDependencies": {
"cross-env": "^7.0.3",
"jest": "^27.0.1",
"jest": "^28.1.3",
"pino-pretty": "^11.2.2",
"tmp": "^0.2.3"
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "6.7.0",
"version": "6.7.2-alpha.1",
"name": "dbgate-all",
"workspaces": [
"packages/*",
+1 -1
View File
@@ -31,7 +31,7 @@
"cors": "^2.8.5",
"cross-env": "^6.0.3",
"dbgate-datalib": "^6.0.0-alpha.1",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-sqltree": "^6.0.0-alpha.1",
"dbgate-tools": "^6.0.0-alpha.1",
"debug": "^4.3.4",
+2 -2
View File
@@ -35,8 +35,8 @@ module.exports = {
},
refreshPublicFiles_meta: true,
async refreshPublicFiles({ isRefresh }) {
await refreshPublicFiles(isRefresh);
async refreshPublicFiles({ isRefresh }, req) {
await refreshPublicFiles(isRefresh, req?.headers?.['x-ui-language']);
return {
status: 'ok',
};
+2
View File
@@ -71,6 +71,7 @@ module.exports = {
const isLicenseValid = checkedLicense?.status == 'ok';
const logoutUrl = storageConnectionError ? null : await authProvider.getLogoutUrl();
const adminConfig = storageConnectionError ? null : await storage.readConfig({ group: 'admin' });
const settingsConfig = storageConnectionError ? null : await storage.readConfig({ group: 'settings' });
storage.startRefreshLicense();
@@ -121,6 +122,7 @@ module.exports = {
allowPrivateCloud: platformInfo.isElectron || !!process.env.ALLOW_DBGATE_PRIVATE_CLOUD,
...currentVersion,
redirectToDbGateCloudLogin: !!process.env.REDIRECT_TO_DBGATE_CLOUD_LOGIN,
preferrendLanguage: settingsConfig?.['storage.language'] || process.env.LANGUAGE || null,
};
return configResult;
+14 -3
View File
@@ -14,7 +14,11 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
const processArgs = require('../utility/processArgs');
const { safeJsonParse, getLogger, extractErrorLogData } = require('dbgate-tools');
const platformInfo = require('../utility/platformInfo');
const { connectionHasPermission, testConnectionPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
const {
connectionHasPermission,
testConnectionPermission,
loadPermissionsFromRequest,
} = require('../utility/hasPermission');
const pipeForkLogs = require('../utility/pipeForkLogs');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { getAuthProviderById } = require('../auth/authProvider');
@@ -116,7 +120,10 @@ function getPortalCollections() {
}
}
logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'DBGM-00005 Using connections from ENV variables');
logger.info(
{ connections: connections.map(pickSafeConnectionInfo) },
'DBGM-00005 Using connections from ENV variables'
);
const noengine = connections.filter(x => !x.engine);
if (noengine.length > 0) {
logger.warn(
@@ -502,7 +509,11 @@ module.exports = {
state,
client: 'web',
});
res.redirect(authResp.url);
if (authResp?.url) {
res.redirect(authResp.url);
return;
}
res.json({ error: 'No URL returned from auth provider' });
},
dbloginApp_meta: true,
+38 -1
View File
@@ -1533,6 +1533,12 @@ module.exports = {
"columnName": "name",
"dataType": "varchar(250)",
"notNull": true
},
{
"pureName": "team_file_types",
"columnName": "format",
"dataType": "varchar(50)",
"notNull": false
}
],
"foreignKeys": [],
@@ -1549,7 +1555,38 @@ module.exports = {
"preloadedRows": [
{
"id": -1,
"name": "sql"
"name": "sql",
"format": "text"
},
{
"id": -2,
"name": "diagrams",
"format": "json"
},
{
"id": -3,
"name": "query",
"format": "json"
},
{
"id": -4,
"name": "perspectives",
"format": "json"
},
{
"id": -5,
"name": "impexp",
"format": "json"
},
{
"id": -6,
"name": "shell",
"format": "text"
},
{
"id": -7,
"name": "dbcompare",
"format": "json"
}
]
},
+10 -5
View File
@@ -193,7 +193,7 @@ async function getCloudSigninHeaders(holder = null) {
return null;
}
async function updateCloudFiles(isRefresh) {
async function updateCloudFiles(isRefresh, language) {
let lastCloudFilesTags;
try {
lastCloudFilesTags = await fs.readFile(path.join(datadir(), 'cloud-files-tags.txt'), 'utf-8');
@@ -218,6 +218,7 @@ async function updateCloudFiles(isRefresh) {
...getLicenseHttpHeaders(),
...(await getCloudInstanceHeaders()),
'x-app-version': currentVersion.version,
'x-app-language': language || 'en',
},
}
);
@@ -274,7 +275,7 @@ async function ensurePromoWidgetDataLoaded() {
promoWidgetDataLoaded = true;
}
async function updatePremiumPromoWidget() {
async function updatePremiumPromoWidget(language) {
await ensurePromoWidgetDataLoaded();
const tags = (await collectCloudFilesSearchTags()).join(',');
@@ -286,6 +287,7 @@ async function updatePremiumPromoWidget() {
...getLicenseHttpHeaders(),
...(await getCloudInstanceHeaders()),
'x-app-version': currentVersion.version,
'x-app-language': language || 'en',
},
}
);
@@ -300,18 +302,21 @@ async function updatePremiumPromoWidget() {
socket.emitChanged(`promo-widget-changed`);
}
async function refreshPublicFiles(isRefresh) {
async function refreshPublicFiles(isRefresh, uiLanguage) {
const language = platformInfo.isElectron
? (await config.getCachedSettings())?.['localization.language'] || 'en'
: uiLanguage;
if (!cloudFiles) {
await loadCloudFiles();
}
try {
await updateCloudFiles(isRefresh);
await updateCloudFiles(isRefresh, language);
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00166 Error updating cloud files');
}
const configSettings = await config.get();
if (!isProApp() || configSettings?.trialDaysLeft != null) {
await updatePremiumPromoWidget();
await updatePremiumPromoWidget(language);
}
}
+1
View File
@@ -2,4 +2,5 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
reporters: ['default', 'github-actions'],
};
+14 -1
View File
@@ -35,6 +35,12 @@ program
.option('-u, --user <user>', 'user name')
.option('-p, --password <password>', 'password')
.option('-d, --database <database>', 'database name')
.option('--url <url>', 'database url')
.option('--file <file>', 'database file')
.option('--socket-path <socketPath>', 'socket path')
.option('--service-name <serviceName>', 'service name (for Oracle)')
.option('--auth-type <authType>', 'authentication type')
.option('--use-ssl', 'use SSL connection')
.option('--auto-index-foreign-keys', 'automatically adds indexes to all foreign keys')
.option(
'--load-data-condition <condition>',
@@ -48,7 +54,7 @@ program
.command('deploy <modelFolder>')
.description('Deploys model to database')
.action(modelFolder => {
const { engine, server, user, password, database, transaction } = program.opts();
const { engine, server, user, password, database, url, file, transaction } = program.opts();
// const hooks = [];
// if (program.autoIndexForeignKeys) hooks.push(dbmodel.hooks.autoIndexForeignKeys);
@@ -60,6 +66,13 @@ program
user,
password,
database,
databaseUrl: url,
useDatabaseUrl: !!url,
databaseFile: file,
socketPath: program.socketPath,
serviceName: program.serviceName,
authType: program.authType,
useSsl: program.useSsl,
},
modelFolder,
useTransaction: transaction,
+1
View File
@@ -2,4 +2,5 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
reporters: ['default', 'github-actions'],
};
+5 -2
View File
@@ -1,7 +1,7 @@
import type { SqlDumper } from 'dbgate-types';
import { Command, Select, Update, Delete, Insert } from './types';
import { dumpSqlExpression } from './dumpSqlExpression';
import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
import { dumpSqlFromDefinition, dumpSqlSourceDef, dumpSqlSourceRef } from './dumpSqlSource';
import { dumpSqlCondition } from './dumpSqlCondition';
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
@@ -115,7 +115,10 @@ export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
cmd.fields.map(x => x.targetColumn)
);
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
if (dmp.dialect.requireFromDual) {
if (cmd.whereNotExistsSource) {
dmp.put(' ^from ');
dumpSqlSourceDef(dmp, cmd.whereNotExistsSource);
} else if (dmp.dialect.requireFromDual) {
dmp.put(' ^from ^dual ');
}
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
@@ -2,6 +2,7 @@ import _ from 'lodash';
import type { SqlDumper } from 'dbgate-types';
import { Expression, ColumnRefExpression } from './types';
import { dumpSqlSourceRef } from './dumpSqlSource';
import { dumpSqlSelect } from './dumpSqlCommand';
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
switch (expr.exprType) {
@@ -67,5 +68,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
});
dmp.put(')');
break;
case 'select':
dmp.put('(');
dumpSqlSelect(dmp, expr.select);
dmp.put(')');
break;
}
}
+8 -1
View File
@@ -44,6 +44,7 @@ export interface Insert {
fields: UpdateField[];
targetTable: NamedObjectInfo;
insertWhereNotExistsCondition?: Condition;
whereNotExistsSource?: Source;
}
export interface AllowIdentityInsert {
@@ -226,6 +227,11 @@ export interface RowNumberExpression {
orderBy: OrderByExpression[];
}
export interface SelectExpression {
exprType: 'select';
select: Select;
}
export type Expression =
| ColumnRefExpression
| ValueExpression
@@ -235,7 +241,8 @@ export type Expression =
| CallExpression
| MethodCallExpression
| TranformExpression
| RowNumberExpression;
| RowNumberExpression
| SelectExpression;
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
export type ResultField = Expression & { alias?: string };
+1
View File
@@ -2,4 +2,5 @@ module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
reporters: ['default', 'github-actions'],
};
+1 -1
View File
@@ -33,7 +33,7 @@
},
"dependencies": {
"blueimp-md5": "^2.19.0",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-sqltree": "^6.0.0-alpha.1",
"debug": "^4.3.4",
"json-stable-stringify": "^1.0.1",
+1
View File
@@ -16,6 +16,7 @@ export interface SqlDumper extends AlterProcessor {
transform(type: TransformType, dumpExpr: () => void);
createDatabase(name: string);
dropDatabase(name: string);
comment(value: string);
callableTemplate(func: CallableObjectInfo);
+1 -1
View File
@@ -32,7 +32,7 @@
"chartjs-plugin-datalabels": "^2.2.0",
"cross-env": "^7.0.3",
"dbgate-datalib": "^6.0.0-alpha.1",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-sqltree": "^6.0.0-alpha.1",
"dbgate-tools": "^6.0.0-alpha.1",
"dbgate-types": "^6.0.0-alpha.1",
+1 -1
View File
@@ -61,7 +61,7 @@
initializeAppUpdates();
installCloudListeners();
refreshPublicCloudFiles();
saveSelectedLanguageToCache();
saveSelectedLanguageToCache(config.preferrendLanguage);
const electron = getElectron();
if (electron) {
@@ -1,6 +1,7 @@
<script lang="ts" context="module">
import { copyTextToClipboard } from '../utility/clipboard';
import { _t, _tval, DefferedTranslationResult } from '../translations';
import sqlFormatter from 'sql-formatter';
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const createMatcher =
@@ -88,7 +89,8 @@
isRename?: boolean;
isTruncate?: boolean;
isCopyTableName?: boolean;
isDuplicateTable?: boolean;
isTableBackup?: boolean;
isTableRestore?: boolean;
isDiagram?: boolean;
functionName?: string;
isExport?: boolean;
@@ -106,6 +108,8 @@
}
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
switch (objectTypeField) {
case 'tables':
return [
@@ -175,11 +179,18 @@
isCopyTableName: true,
requiresWriteAccess: false,
},
hasPermission('dbops/table/backup') && {
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
isDuplicateTable: true,
requiresWriteAccess: true,
},
hasPermission('dbops/table/backup') &&
!backupMatch && {
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
isTableBackup: true,
requiresWriteAccess: true,
},
hasPermission('dbops/table/restore') &&
backupMatch && {
label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
isTableRestore: true,
requiresWriteAccess: true,
},
hasPermission('dbops/model/view') && {
label: _t('dbObject.showDiagram', { defaultMessage: 'Show diagram' }),
isDiagram: true,
@@ -396,17 +407,17 @@
functionName: 'tableReader',
},
hasPermission('dbops/model/edit') && {
label: _t('dbObject.dropCollection', { defaultMessage: 'Drop collection/container'}),
label: _t('dbObject.dropCollection', { defaultMessage: 'Drop collection/container' }),
isDropCollection: true,
requiresWriteAccess: true,
},
hasPermission('dbops/table/rename') && {
label: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container'}),
label: _t('dbObject.renameCollection', { defaultMessage: 'Rename collection/container' }),
isRenameCollection: true,
requiresWriteAccess: true,
},
hasPermission('dbops/table/backup') && {
label: _t('dbObject.createCollectionBackup', { defaultMessage: 'Create collection/container backup'}),
label: _t('dbObject.createCollectionBackup', { defaultMessage: 'Create collection/container backup' }),
isDuplicateCollection: true,
requiresWriteAccess: true,
},
@@ -590,7 +601,10 @@
});
} else if (menu.isDropCollection) {
showModal(ConfirmModal, {
message: _t('dbObject.confirmDropCollection', { defaultMessage: 'Really drop collection {name}?', values: { name: data.pureName } }),
message: _t('dbObject.confirmDropCollection', {
defaultMessage: 'Really drop collection {name}?',
values: { name: data.pureName },
}),
onConfirm: async () => {
const dbid = _.pick(data, ['conid', 'database']);
runOperationOnDatabase(dbid, {
@@ -621,7 +635,10 @@
const driver = await getDriver();
showModal(ConfirmModal, {
message: _t('dbObject.confirmCloneCollection', { defaultMessage: 'Really create collection/container copy named {name}?', values: { name: newName } }),
message: _t('dbObject.confirmCloneCollection', {
defaultMessage: 'Really create collection/container copy named {name}?',
values: { name: newName },
}),
onConfirm: async () => {
const dbid = _.pick(data, ['conid', 'database']);
runOperationOnDatabase(dbid, {
@@ -631,7 +648,7 @@
});
},
});
} else if (menu.isDuplicateTable) {
} else if (menu.isTableBackup) {
const driver = await getDriver();
const dmp = driver.createDumper();
const newTable = _.cloneDeep(data);
@@ -665,6 +682,25 @@
},
engine: driver.engine,
});
} else if (menu.isTableRestore) {
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
const driver = await getDriver();
const dmp = driver.createDumper();
const db = await getDatabaseInfo(data);
if (db) {
const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
if (originalTable) {
createTableRestoreScript(data, originalTable, dmp);
newQuery({
title: _t('dbObject.restoreScript', {
defaultMessage: 'Restore {name} #',
values: { name: backupMatch[1] },
}),
initialData: sqlFormatter.format(dmp.s),
});
}
}
} else if (menu.isImport) {
const { conid, database } = data;
openImportExportTab({
@@ -721,9 +757,7 @@
if (!item.submenu) {
if (!item) return item;
return {...item,
label: _tval(item.label)
};
return { ...item, label: _tval(item.label) };
}
return {
...item,
@@ -773,7 +807,9 @@
openNewTab(
{
// title: getObjectTitle(connection, schemaName, pureName),
title: tabComponent ? getObjectTitle(connection, schemaName, pureName) : _t('dbObject.query', { defaultMessage: 'Query #' }),
title: tabComponent
? getObjectTitle(connection, schemaName, pureName)
: _t('dbObject.query', { defaultMessage: 'Query #' }),
focused: tabComponent == null,
tooltip,
icon:
@@ -1002,6 +1038,8 @@
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
}
export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
</script>
<script lang="ts">
@@ -1019,7 +1057,7 @@
} from '../stores';
import openNewTab from '../utility/openNewTab';
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import { showModal } from '../modals/modalTools';
import { findEngineDriver } from 'dbgate-tools';
@@ -1040,6 +1078,9 @@
import { getSupportedScriptTemplates } from '../utility/applyScriptTemplate';
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
import { isProApp } from '../utility/proTools';
import formatFileSize from '../utility/formatFileSize';
import { createTableRestoreScript } from '../utility/tableRestoreScript';
import newQuery from '../query/newQuery';
export let data;
export let passProps;
@@ -1068,6 +1109,9 @@
if (data.tableRowCount != null) {
res.push(`${formatRowCount(data.tableRowCount)} rows`);
}
if (data.sizeBytes) {
res.push(formatFileSize(data.sizeBytes));
}
if (data.tableEngine) {
res.push(data.tableEngine);
}
@@ -1076,14 +1120,21 @@
}
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
$: backupParsed = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
$: backupTitle =
backupParsed != null
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
: null;
</script>
<AppObjectCore
{...$$restProps}
module={$$props.module}
{data}
title={data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
icon={databaseObjectIcons[data.objectTypeField]}
title={backupTitle ??
(data.schemaName && !passProps?.hideSchemaName ? `${data.schemaName}.${data.pureName}` : data.pureName)}
icon={backupParsed ? 'img table-backup' : databaseObjectIcons[data.objectTypeField]}
menu={createMenu}
showPinnedInsteadOfUnpin={passProps?.showPinnedInsteadOfUnpin}
onPin={passProps?.ingorePin ? null : isPinned ? null : () => pinnedTables.update(list => [...list, data])}
@@ -5,6 +5,7 @@
import getElectron from '../utility/getElectron';
import InlineButtonLabel from '../buttons/InlineButtonLabel.svelte';
import resolveApi, { resolveApiHeaders } from '../utility/resolveApi';
import { _t } from '../translations';
import uuidv1 from 'uuid/v1';
@@ -49,11 +50,11 @@
</script>
{#if electron}
<InlineButton on:click={handleOpenElectronFile} title="Open file" data-testid={$$props['data-testid']}>
<InlineButton on:click={handleOpenElectronFile} title={_t('files.openFile', { defaultMessage: "Open file" })} data-testid={$$props['data-testid']}>
<FontIcon {icon} />
</InlineButton>
{:else}
<InlineButtonLabel on:click={() => {}} title="Upload file" data-testid={$$props['data-testid']} htmlFor={inputId}>
<InlineButtonLabel on:click={() => {}} title={_t('files.uploadFile', { defaultMessage: "Upload file" })} data-testid={$$props['data-testid']} htmlFor={inputId}>
<FontIcon {icon} />
</InlineButtonLabel>
{/if}
+2 -1
View File
@@ -1,6 +1,7 @@
<script lang="ts">
import FormStyledButtonLikeLabel from '../buttons/FormStyledButtonLikeLabel.svelte';
import uploadFiles from '../utility/uploadFiles';
import { _t } from '../translations';
const handleChange = e => {
const files = [...e.target.files];
@@ -9,6 +10,6 @@
</script>
<div class="m-1">
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">Upload file</FormStyledButtonLikeLabel>
<FormStyledButtonLikeLabel htmlFor="uploadFileButton">{_t('files.uploadFile', { defaultMessage: "Upload file" })}</FormStyledButtonLikeLabel>
<input type="file" id="uploadFileButton" hidden on:change={handleChange} />
</div>
@@ -3,7 +3,7 @@ import { currentDatabase, getCurrentDatabase } from '../stores';
import getElectron from '../utility/getElectron';
import registerCommand from './registerCommand';
import { apiCall } from '../utility/api';
import { switchCurrentDatabase } from '../utility/common';
import { getDatabasStatusMenu, switchCurrentDatabase } from '../utility/common';
import { __t } from '../translations';
registerCommand({
@@ -18,33 +18,7 @@ registerCommand({
conid: connection._id,
database: name,
};
return [
{
text: 'Sync model (incremental)',
onClick: () => {
apiCall('database-connections/sync-model', dbid);
},
},
{
text: 'Sync model (full)',
onClick: () => {
apiCall('database-connections/sync-model', { ...dbid, isFullRefresh: true });
},
},
{
text: 'Reopen',
onClick: () => {
apiCall('database-connections/refresh', dbid);
},
},
{
text: 'Disconnect',
onClick: () => {
const electron = getElectron();
if (electron) apiCall('database-connections/disconnect', dbid);
switchCurrentDatabase(null);
},
},
];
return getDatabasStatusMenu(dbid);
},
});
+68 -17
View File
@@ -11,6 +11,7 @@ import {
promoWidgetPreview,
visibleToolbar,
visibleWidgetSideBar,
selectedWidget,
} from '../stores';
import registerCommand from './registerCommand';
import { get } from 'svelte/store';
@@ -115,13 +116,13 @@ registerCommand({
toolbar: true,
icon: 'icon new-connection',
toolbarName: __t('command.new.connection', { defaultMessage: 'Add connection' }),
category: __t('command.new', { defaultMessage: 'New'}),
category: __t('command.new', { defaultMessage: 'New' }),
toolbarOrder: 1,
name: __t('command.new.connection', { defaultMessage: 'Connection' }),
testEnabled: () => !getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase,
onClick: () => {
openNewTab({
title: 'New Connection',
title: _t('common.newConnection', { defaultMessage: 'New Connection' }),
icon: 'img connection',
tabComponent: 'ConnectionTab',
});
@@ -140,7 +141,7 @@ registerCommand({
!getCurrentConfig()?.runAsPortal && !getCurrentConfig()?.storageDatabase && !!getCloudSigninTokenHolder(),
onClick: () => {
openNewTab({
title: 'New Connection on Cloud',
title: _t('common.newConnectionCloud', { defaultMessage: 'New Connection on Cloud' }),
icon: 'img cloud-connection',
tabComponent: 'ConnectionTab',
props: {
@@ -561,7 +562,10 @@ registerCommand({
testEnabled: () => true,
onClick: () => {
showModal(ConfirmModal, {
message: _t('command.file.resetLayoutConfirm', { defaultMessage: 'Really reset layout data? All opened tabs, settings and layout data will be lost. Connections and saved files will be preserved. After this, restart DbGate for applying changes.' }),
message: _t('command.file.resetLayoutConfirm', {
defaultMessage:
'Really reset layout data? All opened tabs, settings and layout data will be lost. Connections and saved files will be preserved. After this, restart DbGate for applying changes.',
}),
onConfirm: async () => {
await apiCall('config/delete-settings');
localStorage.clear();
@@ -665,7 +669,9 @@ registerCommand({
'currentArchive',
];
for (const key of keys) removeLocalStorage(key);
showSnackbarSuccess(_t('command.view.restart', { defaultMessage: 'Restart DbGate (or reload on web) for applying changes' }));
showSnackbarSuccess(
_t('command.view.restart', { defaultMessage: 'Restart DbGate (or reload on web) for applying changes' })
);
},
});
@@ -762,6 +768,19 @@ if (isProApp()) {
}
if (hasPermission('settings/change')) {
registerCommand({
id: 'settings.settingsTab',
category: __t('command.settings', { defaultMessage: 'Settings' }),
name: __t('command.settings.settingsTab', { defaultMessage: 'Settings tab' }),
onClick: () => {
openNewTab({
title: _t('command.settings.settingsTab', { defaultMessage: 'Settings tab' }),
icon: 'icon settings',
tabComponent: 'SettingsTab',
props: {},
});
},
});
registerCommand({
id: 'settings.commands',
category: __t('command.settings', { defaultMessage: 'Settings' }),
@@ -777,14 +796,14 @@ if (hasPermission('settings/change')) {
testEnabled: () => hasPermission('settings/change'),
});
registerCommand({
id: 'settings.show',
category: __t('command.settings', { defaultMessage: 'Settings' }),
name: __t('command.settings.change', { defaultMessage: 'Change' }),
toolbarName: __t('command.settings', { defaultMessage: 'Settings' }),
onClick: () => showModal(SettingsModal),
testEnabled: () => hasPermission('settings/change'),
});
// registerCommand({
// id: 'settings.show',
// category: __t('command.settings', { defaultMessage: 'Settings' }),
// name: __t('command.settings.change', { defaultMessage: 'Change' }),
// toolbarName: __t('command.settings', { defaultMessage: 'Settings' }),
// onClick: () => showModal(SettingsModal),
// testEnabled: () => hasPermission('settings/change'),
// });
}
registerCommand({
@@ -799,7 +818,9 @@ registerCommand({
registerCommand({
id: 'file.exit',
category: __t('command.file', { defaultMessage: 'File' }),
name: isMac() ? __t('command.file.quit', { defaultMessage: 'Quit' }) : __t('command.file.exit', { defaultMessage: 'Exit' }),
name: isMac()
? __t('command.file.quit', { defaultMessage: 'Quit' })
: __t('command.file.exit', { defaultMessage: 'Exit' }),
// keyText: isMac() ? 'Command+Q' : null,
testEnabled: () => getElectron() != null,
onClick: () => getElectron().send('quit-app'),
@@ -862,6 +883,7 @@ export function registerFileCommands({
undoRedo = false,
executeAdditionalCondition = null,
copyPaste = false,
defaultTeamFolder = false,
}) {
if (save) {
registerCommand({
@@ -874,7 +896,7 @@ export function registerFileCommands({
toolbar: true,
isRelatedToTab: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), 'save', folder, format, fileExtension),
onClick: () => saveTabFile(getCurrentEditor(), 'save', folder, format, fileExtension, defaultTeamFolder),
});
registerCommand({
id: idPrefix + '.saveAs',
@@ -882,14 +904,14 @@ export function registerFileCommands({
category,
name: __t('command.saveAs', { defaultMessage: 'Save As' }),
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), 'save-as', folder, format, fileExtension),
onClick: () => saveTabFile(getCurrentEditor(), 'save-as', folder, format, fileExtension, defaultTeamFolder),
});
registerCommand({
id: idPrefix + '.saveToDisk',
category,
name: __t('command.saveToDisk', { defaultMessage: 'Save to disk' }),
testEnabled: () => getCurrentEditor() != null && getElectron() != null,
onClick: () => saveTabFile(getCurrentEditor(), 'save-to-disk', folder, format, fileExtension),
onClick: () => saveTabFile(getCurrentEditor(), 'save-to-disk', folder, format, fileExtension, defaultTeamFolder),
});
}
@@ -1202,6 +1224,35 @@ registerCommand({
},
});
if ( hasPermission('application-log'))
{
registerCommand({
id: 'app.showLogs',
category: __t('command.application', { defaultMessage: 'Application' }),
name: __t('command.application.showLogs', { defaultMessage: 'View application logs' }),
onClick: () => {
openNewTab({
title: 'Application log',
icon: 'img applog',
tabComponent: 'AppLogTab',
});
},
});
}
if (hasPermission('widgets/plugins'))
{
registerCommand({
id: 'app.managePlugins',
category: __t('command.application', { defaultMessage: 'Application' }),
name: __t('command.application.managePlugins', { defaultMessage: 'Manage plugins' }),
onClick: () => {
selectedWidget.set('plugins');
visibleWidgetSideBar.set(true);
},
});
}
const electron = getElectron();
if (electron) {
electron.addEventListener('run-command', (e, commandId) => runCommand(commandId));
@@ -17,6 +17,7 @@
import moveDrag from '../utility/moveDrag';
import ColumnLine from './ColumnLine.svelte';
import DomTableRef from './DomTableRef';
import { _t } from '../translations';
export let conid;
export let database;
@@ -185,8 +186,8 @@
const handleSetTableAlias = () => {
showModal(InputTextModal, {
value: alias || '',
label: 'New alias',
header: 'Set table alias',
label: _t('designerTable.newAlias', { defaultMessage: 'New alias' }),
header: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }),
onConfirm: newAlias => {
onChangeTable({
...table,
@@ -210,13 +211,13 @@
return settings?.tableMenu({ designer, designerId, onRemoveTable });
}
return [
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
{ text: _t('common.remove', { defaultMessage: 'Remove' }), onClick: () => onRemoveTable({ designerId }) },
{ divider: true },
settings?.allowTableAlias &&
!isMultipleTableSelection && [
{ text: 'Set table alias', onClick: handleSetTableAlias },
{ text: _t('designerTable.setTableAlias', { defaultMessage: 'Set table alias' }), onClick: handleSetTableAlias },
alias && {
text: 'Remove table alias',
text: _t('designerTable.removeTableAlias', { defaultMessage: 'Remove table alias' }),
onClick: () =>
onChangeTable({
...table,
@@ -225,11 +226,11 @@
},
],
settings?.allowAddAllReferences &&
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
!isMultipleTableSelection && { text: _t('designerTable.addReferences', { defaultMessage: 'Add references' }), onClick: () => onAddAllReferences(table) },
settings?.allowChangeColor && { text: _t('designerTable.changeColor', { defaultMessage: 'Change color' }), onClick: () => onChangeTableColor(table) },
settings?.allowDefineVirtualReferences &&
!isMultipleTableSelection && {
text: 'Define virtual foreign key',
text: _t('designerTable.defineVirtualForeignKey', { defaultMessage: 'Define virtual foreign key' }),
onClick: () => handleDefineVirtualForeignKey(table),
},
settings?.appendTableSystemMenu &&
@@ -0,0 +1,165 @@
<script lang="ts">
import _ from 'lodash';
import HorizontalSplitter from './HorizontalSplitter.svelte';
interface MenuItemDef {
label: string;
slot?: number;
component?: any;
props?: any;
testid?: string;
identifier?: string;
}
export let items: MenuItemDef[];
export let value: string | number = 0;
export let containerMaxWidth = undefined;
export let containerMaxHeight = undefined;
export let flex1 = true;
export let flexColContainer = false;
export let maxHeight100 = false;
export let scrollableContentContainer = false;
export let contentTestId = undefined;
export let onUserChange = null;
export function setValue(index) {
value = index;
}
export function getValue() {
return value;
}
</script>
<div class="main" class:maxHeight100 class:flex1>
<HorizontalSplitter initialValue="20%">
<svelte:fragment slot="1">
<div class="menu">
{#each _.compact(items) as item, index}
<div
class="menu-item"
class:selected={value == (item.identifier ?? index)}
on:click={() => {
value = item.identifier ?? index;
onUserChange?.(item.identifier ?? index);
}}
data-testid={item.testid}
>
<span class="ml-2 noselect">
{item.label}
</span>
</div>
{/each}
</div>
</svelte:fragment>
<svelte:fragment slot="2">
<div
class="content-container"
class:scrollableContentContainer
style:max-height={containerMaxHeight}
data-testid={contentTestId}
>
{#each _.compact(items) as item, index}
<div
class="container"
class:flexColContainer
class:maxHeight100
class:itemVisible={(item.identifier ?? index) == value}
style:max-width={containerMaxWidth}
>
<svelte:component
this={item.component}
{...item.props}
itemVisible={(item.identifier ?? index) == value}
menuControlHiddenItem={(item.identifier ?? index) != value}
/>
</div>
{/each}
</div>
</svelte:fragment>
</HorizontalSplitter>
</div>
<style>
.main {
display: flex;
flex: 1;
}
.main.flex1 {
flex: 1;
max-height: 100%;
}
.main.maxHeight100 {
max-height: 100%;
}
.menu {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
background-color: var(--theme-bg-2);
overflow-x: auto;
}
.menu::-webkit-scrollbar {
width: 7px;
}
.menu-item {
white-space: nowrap;
padding: 12px 20px;
display: flex;
align-items: center;
cursor: pointer;
border-bottom: 1px solid var(--theme-border);
transition: background-color 0.2s ease;
}
.menu-item:hover {
background-color: var(--theme-bg-hover);
}
.menu-item.selected {
background-color: var(--theme-bg-1);
font-weight: 600;
border-left: 3px solid var(--theme-font-link);
}
.content-container {
flex: 1;
position: relative;
overflow: hidden;
height: 100%;
}
.scrollableContentContainer {
overflow-y: auto;
}
.container.maxHeight100 {
max-height: 100%;
}
.container.flexColContainer {
display: flex;
flex-direction: column;
}
.container {
position: absolute;
display: flex;
left: 0;
right: 0;
top: 0;
bottom: 0;
overflow-y: auto;
padding: 20px;
}
.container:not(.itemVisible) {
visibility: hidden;
}
</style>
@@ -7,6 +7,7 @@
import { getFormContext } from './FormProviderCore.svelte';
import FormSelectField from './FormSelectField.svelte';
import { _t } from '../translations';
export let folderName;
export let name;
@@ -28,10 +29,10 @@
<div>
<FormStyledButton
type="button"
value="All files"
value={_t('common.allFiles', { defaultMessage: "All files" })}
on:click={() => setFieldValue(name, _.uniq([...($values[name] || []), ...($files && $files.map(x => x.name))]))}
/>
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
<FormStyledButton type="button" value={_t('common.removeAll', { defaultMessage: "Remove all" })} on:click={() => setFieldValue(name, [])} />
</div>
</div>
+1
View File
@@ -353,6 +353,7 @@
'img data-deploy': 'mdi mdi-database-settings color-icon-green',
'img arrow-start-here': 'mdi mdi-arrow-down-bold-circle color-icon-green',
'img team-file': 'mdi mdi-account-file color-icon-red',
'img table-backup': 'mdi mdi-cube color-icon-yellow',
};
</script>
+3 -2
View File
@@ -23,6 +23,7 @@
import ElectronFilesInput from './ElectronFilesInput.svelte';
import { addFilesToSourceList } from './ImportExportConfigurator.svelte';
import UploadButton from '../buttons/UploadButton.svelte';
import { _t } from '../translations';
export let setPreviewSource = undefined;
@@ -55,10 +56,10 @@
{:else}
<UploadButton />
{/if}
<FormStyledButton value="Add web URL" on:click={handleAddUrl} />
<FormStyledButton value={_t('importExport.addWebUrl', { defaultMessage: "Add web URL" })} on:click={handleAddUrl} />
</div>
<div class="wrapper">Drag &amp; drop imported files here</div>
<div class="wrapper">{_t('importExport.dragDropImportedFilesHere', { defaultMessage: "Drag & drop imported files here" })}</div>
</div>
<style>
@@ -6,6 +6,7 @@
import { getFormContext } from '../forms/FormProviderCore.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { _t } from '../translations';
export let conidName;
export let databaseName;
@@ -41,7 +42,7 @@
{#if $dbinfo && $dbinfo[field]?.length > 0}
<FormStyledButton
type="button"
value={`All ${field}`}
value={_t('common.allFields', { defaultMessage: 'All {field}', values: { field } })}
data-testid={`FormTablesSelect_buttonAll_${field}`}
on:click={() =>
setFieldValue(
@@ -52,7 +53,7 @@
{/if}
{/each}
<FormStyledButton type="button" value="Remove all" on:click={() => setFieldValue(name, [])} />
<FormStyledButton type="button" value={_t('common.removeAll', { defaultMessage: "Remove all" })} on:click={() => setFieldValue(name, [])} />
</div>
</div>
@@ -77,6 +77,7 @@
import createRef from '../utility/createRef';
import DropDownButton from '../buttons/DropDownButton.svelte';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import { _t } from '../translations';
// export let uploadedFile = undefined;
// export let openedFile = undefined;
@@ -211,7 +212,7 @@
</div>
<div class="m-2">
<div class="title"><FontIcon icon="icon tables" /> Map source tables/files</div>
<div class="title"><FontIcon icon="icon tables" /> {_t('importExport.mapSourceTablesFiles', { defaultMessage: "Map source tables/files" })}</div>
{#key targetEditKey}
{#key progressHolder}
@@ -220,34 +221,34 @@
columns={[
{
fieldName: 'source',
header: 'Source',
header: _t('importExport.source', { defaultMessage: "Source" }),
component: SourceName,
getProps: row => ({ name: row }),
},
{
fieldName: 'action',
header: 'Action',
header: _t('importExport.action', { defaultMessage: "Action" }),
component: SourceAction,
getProps: row => ({ name: row, targetDbinfo }),
},
{
fieldName: 'target',
header: 'Target',
header: _t('importExport.target', { defaultMessage: "Target" }),
slot: 1,
},
supportsPreview && {
fieldName: 'preview',
header: 'Preview',
header: _t('importExport.preview', { defaultMessage: "Preview" }),
slot: 0,
},
!!progressHolder && {
fieldName: 'status',
header: 'Status',
header: _t('importExport.status', { defaultMessage: "Status" }),
slot: 3,
},
{
fieldName: 'columns',
header: 'Columns',
header: _t('importExport.columns', { defaultMessage: "Columns" }),
slot: 2,
},
]}
@@ -73,19 +73,19 @@
<div class="column">
{#if direction == 'source'}
<div class="title">
<FontIcon icon="icon import" /> Source configuration
<FontIcon icon="icon import" /> {_t('importExport.sourceConfiguration', { defaultMessage: 'Source configuration' })}
</div>
{/if}
{#if direction == 'target'}
<div class="title">
<FontIcon icon="icon export" /> Target configuration
<FontIcon icon="icon export" /> {_t('importExport.targetConfiguration', { defaultMessage: 'Target configuration' })}
</div>
{/if}
<div class="buttons">
{#if $currentDatabase}
<FormStyledButton
value="Current DB"
value={_t('importExport.currentDatabase', { defaultMessage: "Current DB" })}
on:click={() => {
values.update(x => ({
...x,
@@ -97,7 +97,7 @@
/>
{/if}
<FormStyledButton
value="Current archive"
value={_t('importExport.currentArchive', { defaultMessage: "Current archive" })}
data-testid={direction == 'source'
? 'SourceTargetConfig_buttonCurrentArchive_source'
: 'SourceTargetConfig_buttonCurrentArchive_target'}
@@ -111,7 +111,7 @@
/>
{#if direction == 'target'}
<FormStyledButton
value="New archive"
value={_t('importExport.newArchive', { defaultMessage: "New archive" })}
on:click={() => {
showModal(InputTextModal, {
header: 'Archive',
@@ -133,7 +133,7 @@
<FormSelectField
options={types.filter(x => x.directions.includes(direction))}
name={storageTypeField}
label="Storage type"
label={_t('importExport.storageType', { defaultMessage: "Storage type" })}
/>
{#if format && isProApp()}
@@ -172,9 +172,9 @@
{/if}
{#if storageType == 'database' || storageType == 'query'}
<FormConnectionSelect name={connectionIdField} label="Server" {direction} />
<FormConnectionSelect name={connectionIdField} label={_t('common.server', { defaultMessage: 'Server' })} {direction} />
{#if !$connectionInfo?.singleDatabase}
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label="Database" />
<FormDatabaseSelect conidName={connectionIdField} name={databaseNameField} label={_t('common.database', { defaultMessage: 'Database' })} />
{/if}
{/if}
{#if storageType == 'database'}
@@ -210,7 +210,7 @@
{#if storageType == 'archive'}
<FormArchiveFolderSelect
label="Archive folder"
label={_t('importExport.archiveFolder', { defaultMessage: "Archive folder" })}
name={archiveFolderField}
additionalFolders={_.compact([$values[archiveFolderField]])}
allowCreateNew={direction == 'target'}
+28 -6
View File
@@ -86,14 +86,26 @@
submenuKey += 1;
return;
}
if (item.switchStore && item.switchValue) {
item.switchStore.update(x => ({
...x,
[item.switchValue]: !x[item.switchValue],
}));
if (item.switchStore) {
if (item.switchValue) {
item.switchStore.update(x => ({
...x,
[item.switchValue]: !x[item.switchValue],
}));
}
if (item.switchOption && item.switchOptionValue) {
item.switchStore.update(x => ({
...x,
[item.switchOption]: item.switchOptionValue,
}));
}
switchIndex++;
return;
if (!item.closeOnSwitchClick) {
return;
}
}
dispatchClose();
if (onCloseParent) onCloseParent();
if (item.onClick) item.onClick();
@@ -163,6 +175,16 @@
{/if}
{/key}
{/if}
{#if item.switchOption && item.switchStoreGetter}
{@const optionValue = item.switchStoreGetter()[item.switchOption]}
{#key switchIndex}
{#if optionValue === item.switchOptionValue || (item.switchOptionIsDefault && !optionValue)}
<FontIcon icon="icon check" padRight />
{:else}
<FontIcon icon="icon invisible-box" padRight />
{/if}
{/key}
{/if}
{item.text || item.label}
</span>
{#if item.keyText}
+28 -13
View File
@@ -14,6 +14,8 @@
import { closeCurrentModal, showModal } from './modalTools';
import FormCloudFolderSelect from '../forms/FormCloudFolderSelect.svelte';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import { useConfig } from '../utility/metadataLoaders';
import { showSnackbarError } from '../utility/snackbar';
export let data;
export let name;
@@ -24,26 +26,39 @@
export let onSave = undefined;
export let folid;
export let skipLocal = false;
export let defaultTeamFolder = false;
// export let cntid;
const values = writable({ name, cloudFolder: folid ?? '__local' });
const configValue = useConfig();
const values = writable({
name,
cloudFolder: folid ?? '__local',
saveToTeamFolder: !!(getCurrentConfig()?.storageDatabase && defaultTeamFolder),
});
const electron = getElectron();
const handleSubmit = async e => {
const { name, cloudFolder } = e.detail;
if ($values['saveToTeamFolder']) {
const { teamFileId } = await apiCall('team-files/create-new', { fileType: folder, file: name, data });
closeCurrentModal();
if (onSave) {
onSave(name, {
savedFile: name,
savedFolder: folder,
savedFilePath: null,
savedCloudFolderId: null,
savedCloudContentId: null,
savedTeamFileId: teamFileId,
});
const resp = await apiCall('team-files/create-new', { fileType: folder, file: name, data });
if (resp?.apiErrorMessage) {
showSnackbarError(resp.apiErrorMessage);
} else if (resp?.teamFileId) {
closeCurrentModal();
if (onSave) {
onSave(name, {
savedFile: name,
savedFolder: folder,
savedFilePath: null,
savedCloudFolderId: null,
savedCloudContentId: null,
savedTeamFileId: resp.teamFileId,
});
}
} else {
showSnackbarError('Failed to save to team folder.');
}
} else if (cloudFolder === '__local') {
await apiCall('files/save', { folder, file: name, data, format });
@@ -124,7 +139,7 @@
]}
/>
{/if}
{#if getCurrentConfig().storageDatabase}
{#if $configValue?.storageDatabase}
<FormCheckboxField label="Save to team folder" name="saveToTeamFolder" />
{/if}
@@ -8,6 +8,7 @@
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
import PluginsList from './PluginsList.svelte';
import { filterName } from 'dbgate-tools';
import { _t } from '../translations';
let filter = '';
// let search = '';
@@ -20,7 +21,7 @@
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search extensions on web" {filter} bind:value={filter} />
<SearchInput placeholder={_t('plugins.searchExtensionsOnWeb', { defaultMessage: 'Search extensions on web' })} {filter} bind:value={filter} />
</SearchBoxWrapper>
<WidgetsInnerContainer>
{#if $plugins?.errorMessage}
@@ -0,0 +1,70 @@
<script lang="ts">
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import { _t } from "../translations";
import FontIcon from '../icons/FontIcon.svelte';
import FormValues from "../forms/FormValues.svelte";
</script>
<div class="wrapper">
<FormValues let:values>
<div class="heading">{_t('settings.behaviour', { defaultMessage: 'Behaviour' })}</div>
<FormCheckboxField
name="behaviour.useTabPreviewMode"
label={_t('settings.behaviour.useTabPreviewMode', { defaultMessage: 'Use tab preview mode' })}
defaultValue={true}
/>
<FormCheckboxField
name="behaviour.jsonPreviewWrap"
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap JSON in preview' })}
defaultValue={false}
/>
<div class="tip">
<FontIcon icon="img tip" />
{_t('settings.behaviour.singleClickPreview', {
defaultMessage:
'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.',
})}
</div>
<FormCheckboxField
name="behaviour.openDetailOnArrows"
label={_t('settings.behaviour.openDetailOnArrows', {
defaultMessage: 'Open detail on keyboard navigation',
})}
defaultValue={true}
disabled={values['behaviour.useTabPreviewMode'] === false}
/>
<div class="heading">{_t('settings.confirmations', { defaultMessage: 'Confirmations' })}</div>
<FormCheckboxField
name="skipConfirm.tableDataSave"
label={_t('settings.confirmations.skipConfirm.tableDataSave', {
defaultMessage: 'Skip confirmation when saving table data (SQL)',
})}
/>
<FormCheckboxField
name="skipConfirm.collectionDataSave"
label={_t('settings.confirmations.skipConfirm.collectionDataSave', {
defaultMessage: 'Skip confirmation when saving collection data (NoSQL)',
})}
/>
</FormValues>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
.tip {
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,87 @@
<script lang="ts">
import CheckboxField from "../forms/CheckboxField.svelte";
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import FormFieldTemplateLarge from "../forms/FormFieldTemplateLarge.svelte";
import FormSelectField from "../forms/FormSelectField.svelte";
import FormTextField from "../forms/FormTextField.svelte";
import FormValues from "../forms/FormValues.svelte";
import { lockedDatabaseMode } from "../stores";
import { _t } from "../translations";
</script>
<div class="wrapper">
<FormValues let:values>
<div class="heading">{_t('settings.connection', { defaultMessage: 'Connection' })}</div>
<FormFieldTemplateLarge
label={_t('settings.connection.showOnlyTabsFromSelectedDatabase', {
defaultMessage: 'Show only tabs from selected database',
})}
type="checkbox"
labelProps={{
onClick: () => {
$lockedDatabaseMode = !$lockedDatabaseMode;
},
}}
>
<CheckboxField checked={$lockedDatabaseMode} on:change={e => ($lockedDatabaseMode = e.target.checked)} />
</FormFieldTemplateLarge>
<FormCheckboxField
name="connection.autoRefresh"
label={_t('settings.connection.autoRefresh', {
defaultMessage: 'Automatic refresh of database model on background',
})}
defaultValue={false}
/>
<FormTextField
name="connection.autoRefreshInterval"
label={_t('settings.connection.autoRefreshInterval', {
defaultMessage: 'Interval between automatic DB structure reloads in seconds',
})}
defaultValue="30"
disabled={values['connection.autoRefresh'] === false}
/>
<FormSelectField
label={_t('settings.connection.sshBindHost', { defaultMessage: 'Local host address for SSH connections' })}
name="connection.sshBindHost"
isNative
defaultValue="127.0.0.1"
options={[
{ value: '127.0.0.1', label: '127.0.0.1 (IPv4)' },
{ value: '::1', label: '::1 (IPv6)' },
{ value: 'localhost', label: 'localhost (domain name)' },
]}
/>
<div class="heading">{_t('settings.session', { defaultMessage: 'Query sessions' })}</div>
<FormCheckboxField
name="session.autoClose"
label={_t('settings.session.autoClose', {
defaultMessage: 'Automatic close query sessions after period without any activity',
})}
defaultValue={true}
/>
<FormTextField
name="session.autoCloseTimeout"
label={_t('settings.session.autoCloseTimeout', {
defaultMessage: 'Interval, after which query session without activity is closed (in minutes)',
})}
defaultValue="15"
disabled={values['session.autoClose'] === false}
/>
</FormValues>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,100 @@
<script lang="ts">
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import FormSelectField from "../forms/FormSelectField.svelte";
import FormTextField from "../forms/FormTextField.svelte";
import { _t } from "../translations";
import { isProApp } from "../utility/proTools";
</script>
<div class="wrapper">
<div class="heading">{_t('settings.dataGrid.title', { defaultMessage: 'Data grid' })}</div>
<FormTextField
name="dataGrid.pageSize"
label={_t('settings.dataGrid.pageSize', {
defaultMessage: 'Page size (number of rows for incremental loading, must be between 5 and 50000)',
})}
defaultValue="100"
/>
{#if isProApp()}
<FormCheckboxField
name="dataGrid.showHintColumns"
label={_t('settings.dataGrid.showHintColumns', { defaultMessage: 'Show foreign key hints' })}
defaultValue={true}
/>
{/if}
<!-- <FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} /> -->
<FormCheckboxField
name="dataGrid.thousandsSeparator"
label={_t('settings.dataGrid.thousandsSeparator', {
defaultMessage: 'Use thousands separator for numbers',
})}
/>
<FormTextField
name="dataGrid.defaultAutoRefreshInterval"
label={_t('settings.dataGrid.defaultAutoRefreshInterval', {
defaultMessage: 'Default grid auto refresh interval in seconds',
})}
defaultValue="10"
/>
<FormCheckboxField
name="dataGrid.alignNumbersRight"
label={_t('settings.dataGrid.alignNumbersRight', { defaultMessage: 'Align numbers to right' })}
defaultValue={false}
/>
<FormTextField
name="dataGrid.collectionPageSize"
label={_t('settings.dataGrid.collectionPageSize', {
defaultMessage: 'Collection page size (for MongoDB JSON view, must be between 5 and 1000)',
})}
defaultValue="50"
/>
<FormSelectField
label={_t('settings.dataGrid.coloringMode', { defaultMessage: 'Row coloring mode' })}
name="dataGrid.coloringMode"
isNative
defaultValue="36"
options={[
{
value: '36',
label: _t('settings.dataGrid.coloringMode.36', { defaultMessage: 'Every 3rd and 6th row' }),
},
{
value: '2-primary',
label: _t('settings.dataGrid.coloringMode.2-primary', {
defaultMessage: 'Every 2-nd row, primary color',
}),
},
{
value: '2-secondary',
label: _t('settings.dataGrid.coloringMode.2-secondary', {
defaultMessage: 'Every 2-nd row, secondary color',
}),
},
{ value: 'none', label: _t('settings.dataGrid.coloringMode.none', { defaultMessage: 'None' }) },
]}
/>
<FormCheckboxField
name="dataGrid.showAllColumnsWhenSearch"
label={_t('settings.dataGrid.showAllColumnsWhenSearch', {
defaultMessage: 'Show all columns when searching',
})}
defaultValue={false}
/>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,103 @@
<script lang="ts">
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import FormSelectField from "../forms/FormSelectField.svelte";
import FormValues from "../forms/FormValues.svelte";
import { _t } from "../translations";
import FormDefaultActionField from "./FormDefaultActionField.svelte";
</script>
<div class="wrapper">
<FormValues let:values>
<div class="heading">{_t('settings.defaultActions', { defaultMessage: 'Default actions' })}</div>
<FormSelectField
label={_t('settings.defaultActions.connectionClick', { defaultMessage: 'Connection click' })}
name="defaultAction.connectionClick"
isNative
defaultValue="connect"
options={[
{
value: 'openDetails',
label: _t('settings.defaultActions.connectionClick.openDetails', {
defaultMessage: 'Edit / open details',
}),
},
{
value: 'connect',
label: _t('settings.defaultActions.connectionClick.connect', { defaultMessage: 'Connect' }),
},
{
value: 'none',
label: _t('settings.defaultActions.connectionClick.none', { defaultMessage: 'Do nothing' }),
},
]}
/>
<FormSelectField
label={_t('settings.defaultActions.databaseClick', { defaultMessage: 'Database click' })}
name="defaultAction.databaseClick"
isNative
defaultValue="switch"
options={[
{
value: 'switch',
label: _t('settings.defaultActions.databaseClick.switch', { defaultMessage: 'Switch database' }),
},
{
value: 'none',
label: _t('settings.defaultActions.databaseClick.none', { defaultMessage: 'Do nothing' }),
},
]}
/>
<FormCheckboxField
name="defaultAction.useLastUsedAction"
label={_t('settings.defaultActions.useLastUsedAction', { defaultMessage: 'Use last used action' })}
defaultValue={true}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.tableClick', { defaultMessage: 'Table click' })}
objectTypeField="tables"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.viewClick', { defaultMessage: 'View click' })}
objectTypeField="views"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.materializedViewClick', { defaultMessage: 'Materialized view click' })}
objectTypeField="matviews"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.procedureClick', { defaultMessage: 'Procedure click' })}
objectTypeField="procedures"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.functionClick', { defaultMessage: 'Function click' })}
objectTypeField="functions"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
<FormDefaultActionField
label={_t('settings.defaultActions.collectionClick', { defaultMessage: 'NoSQL collection click' })}
objectTypeField="collections"
disabled={values['defaultAction.useLastUsedAction'] !== false}
/>
</FormValues>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,50 @@
<script lang="ts">
import FormTextField from "../forms/FormTextField.svelte";
import { _t } from "../translations";
</script>
<div class="wrapper">
<div class="heading">{_t('settings.externalTools', { defaultMessage: 'External tools' })}</div>
<FormTextField
name="externalTools.mysqldump"
label={_t('settings.other.externalTools.mysqldump', {
defaultMessage: 'mysqldump (backup MySQL database)',
})}
defaultValue="mysqldump"
/>
<FormTextField
name="externalTools.mysql"
label={_t('settings.other.externalTools.mysql', { defaultMessage: 'mysql (restore MySQL database)' })}
defaultValue="mysql"
/>
<FormTextField
name="externalTools.mysqlPlugins"
label={_t('settings.other.externalTools.mysqlPlugins', {
defaultMessage:
'Folder with mysql plugins (for example for authentication). Set only in case of problems',
})}
defaultValue=""
/>
<FormTextField
name="externalTools.pg_dump"
label={_t('settings.other.externalTools.pg_dump', {
defaultMessage: 'pg_dump (backup PostgreSQL database)',
})}
defaultValue="pg_dump"
/>
<FormTextField
name="externalTools.psql"
label={_t('settings.other.externalTools.psql', { defaultMessage: 'psql (restore PostgreSQL database)' })}
defaultValue="psql"
/>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,100 @@
<script lang="ts">
import { internalRedirectTo } from '../clientAuth';
import CheckboxField from '../forms/CheckboxField.svelte';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import SelectField from '../forms/SelectField.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { showModal } from '../modals/modalTools';
import { EDITOR_KEYBINDINGS_MODES } from '../query/AceEditor.svelte';
import { currentEditorKeybindigMode, currentEditorWrapEnabled } from '../stores';
import { _t, getSelectedLanguage, setSelectedLanguage } from '../translations';
import { isMac } from '../utility/common';
import getElectron from '../utility/getElectron';
import { isProApp } from '../utility/proTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
const electron = getElectron();
let restartWarning = false;
</script>
<div class="wrapper">
<div class="heading">{_t('settings.general', { defaultMessage: 'General' })}</div>
{#if electron}
<div class="heading">{_t('settings.appearance', { defaultMessage: 'Appearance' })}</div>
<FormCheckboxField
name="app.useNativeMenu"
label={isMac()
? _t('settings.useNativeWindowTitle', { defaultMessage: 'Use native window title' })
: _t('settings.useSystemNativeMenu', { defaultMessage: 'Use system native menu' })}
on:change={() => {
restartWarning = true;
}}
/>
{#if restartWarning}
<div class="ml-5 mb-3">
<FontIcon icon="img warn" />
{_t('settings.nativeMenuRestartWarning', {
defaultMessage: 'Native menu settings will be applied after app restart',
})}
</div>
{/if}
{/if}
<FormCheckboxField
name="tabGroup.showServerName"
label={_t('settings.tabGroup.showServerName', {
defaultMessage: 'Show server name alongside database name in title of the tab group',
})}
defaultValue={false}
/>
<div class="heading">{_t('settings.localization', { defaultMessage: 'Localization' })}</div>
<FormFieldTemplateLarge
label={_t('settings.localization.language', { defaultMessage: 'Language' })}
type="combo"
>
<SelectField
isNative
data-testid="SettingsModal_languageSelect"
options={[
{ value: 'cs', label: 'Čeština' },
{ value: 'de', label: 'Deutsch' },
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português (Brasil)' },
{ value: 'sk', label: 'Slovenčina' },
{ value: 'ja', label: '日本語' },
{ value: 'zh', label: '中文' },
]}
defaultValue={getSelectedLanguage()}
value={getSelectedLanguage()}
on:change={e => {
setSelectedLanguage(e.detail);
showModal(ConfirmModal, {
message: _t('settings.localization.reloadWarning', {
defaultMessage: 'Application will be reloaded to apply new language settings',
}),
onConfirm: () => {
setTimeout(() => {
internalRedirectTo(electron ? '/index.html' : '/');
}, 100);
},
});
}}
/>
</FormFieldTemplateLarge>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,91 @@
<script lang="ts">
import { safeFormatDate } from "dbgate-tools";
import FormStyledButton from "../buttons/FormStyledButton.svelte";
import FormTextAreaField from "../forms/FormTextAreaField.svelte";
import FontIcon from "../icons/FontIcon.svelte";
import { _t } from "../translations";
import { apiCall } from "../utility/api";
import { useSettings } from "../utility/metadataLoaders";
import { derived } from "svelte/store";
const settings = useSettings();
const settingsValues = derived(settings, $settings => {
if (!$settings) {
return {};
}
return $settings;
});
let licenseKeyCheckResult = null;
$: licenseKey = $settingsValues['other.licenseKey'];
</script>
<div class="heading">{_t('settings.other.license', { defaultMessage: 'License' })}</div>
<FormTextAreaField
name="other.licenseKey"
label={_t('settings.other.licenseKey', { defaultMessage: 'License key' })}
rows={7}
onChange={async value => {
licenseKeyCheckResult = await apiCall('config/check-license', { licenseKey: value });
}}
/>
{#if licenseKeyCheckResult}
<div class="m-3 ml-5">
{#if licenseKeyCheckResult.status == 'ok'}
<div>
<FontIcon icon="img ok" />
{_t('settings.other.licenseKey.valid', { defaultMessage: 'License key is valid' })}
</div>
{#if licenseKeyCheckResult.validTo}
<div>
{_t('settings.other.licenseKey.validTo', { defaultMessage: 'License valid to:' })}
{licenseKeyCheckResult.validTo}
</div>
{/if}
{#if licenseKeyCheckResult.expiration}
<div>
{_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
<b>{safeFormatDate(licenseKeyCheckResult.expiration)}</b>
</div>
{/if}
{:else if licenseKeyCheckResult.status == 'error'}
<div>
<FontIcon icon="img error" />
{licenseKeyCheckResult.errorMessage ??
_t('settings.other.licenseKey.invalid', { defaultMessage: 'License key is invalid' })}
{#if licenseKeyCheckResult.expiration}
<div>
{_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
<b>{safeFormatDate(licenseKeyCheckResult.expiration)}</b>
</div>
{/if}
</div>
{#if licenseKeyCheckResult.isExpired}
<div class="mt-2">
<FormStyledButton
value={_t('settings.other.licenseKey.checkForNew', {
defaultMessage: 'Check for new license key',
})}
skipWidth
on:click={async () => {
licenseKeyCheckResult = await apiCall('config/get-new-license', { oldLicenseKey: licenseKey });
if (licenseKeyCheckResult.licenseKey) {
apiCall('config/update-settings', { 'other.licenseKey': licenseKeyCheckResult.licenseKey });
}
}}
/>
</div>
{/if}
{/if}
</div>
{/if}
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,64 @@
<script lang="ts">
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import FormSelectField from "../forms/FormSelectField.svelte";
import FormTextField from "../forms/FormTextField.svelte";
import { _t } from "../translations";
import { isProApp } from "../utility/proTools";
</script>
<div class="wrapper">
<div class="heading">{_t('settings.other', { defaultMessage: 'Other' })}</div>
<FormTextField
name="other.gistCreateToken"
label={_t('settings.other.gistCreateToken', { defaultMessage: 'API token for creating error gists' })}
defaultValue=""
/>
<FormSelectField
label={_t('settings.other.autoUpdateApplication', { defaultMessage: 'Auto update application' })}
name="app.autoUpdateMode"
isNative
defaultValue=""
options={[
{
value: 'skip',
label: _t('settings.other.autoUpdateApplication.skip', {
defaultMessage: 'Do not check for new versions',
}),
},
{
value: '',
label: _t('settings.other.autoUpdateApplication.check', { defaultMessage: 'Check for new versions' }),
},
{
value: 'download',
label: _t('settings.other.autoUpdateApplication.download', {
defaultMessage: 'Check and download new versions',
}),
},
]}
/>
{#if isProApp()}
<FormCheckboxField
name="ai.allowSendModels"
label={_t('settings.other.ai.allowSendModels', {
defaultMessage: 'Allow to send DB models and query snippets to AI service',
})}
defaultValue={false}
/>
{/if}
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
@@ -0,0 +1,100 @@
<script lang="ts">
import CheckboxField from "../forms/CheckboxField.svelte";
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
import FormFieldTemplateLarge from "../forms/FormFieldTemplateLarge.svelte";
import FormSelectField from "../forms/FormSelectField.svelte";
import FormTextField from "../forms/FormTextField.svelte";
import SelectField from "../forms/SelectField.svelte";
import { EDITOR_KEYBINDINGS_MODES } from "../query/AceEditor.svelte";
import { currentEditorKeybindigMode, currentEditorWrapEnabled } from "../stores";
import { _t } from "../translations";
</script>
<div class="wrapper">
<div class="heading">{_t('settings.sqlEditor', { defaultMessage: 'SQL editor' })}</div>
<div class="flex">
<div class="col-3">
<FormSelectField
label={_t('settings.sqlEditor.sqlCommandsCase', { defaultMessage: 'SQL commands case' })}
name="sqlEditor.sqlCommandsCase"
isNative
defaultValue="upperCase"
options={[
{ value: 'upperCase', label: 'UPPER CASE' },
{ value: 'lowerCase', label: 'lower case' },
]}
/>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.editor.keybinds', { defaultMessage: 'Editor keybinds' })}
type="combo"
>
<SelectField
isNative
defaultValue="default"
options={EDITOR_KEYBINDINGS_MODES.map(mode => ({ label: mode.label, value: mode.value }))}
value={$currentEditorKeybindigMode}
on:change={e => ($currentEditorKeybindigMode = e.detail)}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.editor.wordWrap', { defaultMessage: 'Enable word wrap' })}
type="combo"
>
<CheckboxField
checked={$currentEditorWrapEnabled}
on:change={e => ($currentEditorWrapEnabled = e.target.checked)}
/>
</FormFieldTemplateLarge>
</div>
</div>
<FormTextField
name="sqlEditor.limitRows"
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
/>
<FormCheckboxField
name="sqlEditor.showTableAliasesInCodeCompletion"
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
defaultMessage: 'Show table aliases in code completion',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableSplitByEmptyLine"
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableExecuteCurrentLine"
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
defaultMessage: 'Disable current line execution (Execute current)',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.hideColumnsPanel"
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
defaultValue={false}
/>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
</style>
+17 -5
View File
@@ -194,7 +194,10 @@ ORDER BY
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'it', label: 'Italiano' },
{ value: 'pt', label: 'Português (Brasil)' },
{ value: 'sk', label: 'Slovenčina' },
{ value: 'ja', label: '日本語' },
{ value: 'zh', label: '中文' },
]}
defaultValue={getSelectedLanguage()}
@@ -223,11 +226,13 @@ ORDER BY
})}
defaultValue="100"
/>
<FormCheckboxField
name="dataGrid.showHintColumns"
label={_t('settings.dataGrid.showHintColumns', { defaultMessage: 'Show foreign key hints' })}
defaultValue={true}
/>
{#if isProApp()}
<FormCheckboxField
name="dataGrid.showHintColumns"
label={_t('settings.dataGrid.showHintColumns', { defaultMessage: 'Show foreign key hints' })}
defaultValue={true}
/>
{/if}
<!-- <FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} /> -->
<FormCheckboxField
@@ -362,6 +367,13 @@ ORDER BY
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.hideColumnsPanel"
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
defaultValue={false}
/>
</svelte:fragment>
<svelte:fragment slot="2">
<div class="heading">{_t('settings.connection', { defaultMessage: 'Connection' })}</div>
@@ -0,0 +1,164 @@
<script lang="ts">
import Link from "../elements/Link.svelte";
import CheckboxField from "../forms/CheckboxField.svelte";
import FormFieldTemplateLarge from "../forms/FormFieldTemplateLarge.svelte";
import SelectField from "../forms/SelectField.svelte";
import { currentEditorFontSize, currentEditorTheme, currentTheme, extensions, getSystemTheme, selectedWidget, visibleWidgetSideBar } from "../stores";
import { _t } from "../translations";
import ThemeSkeleton from "./ThemeSkeleton.svelte";
import { EDITOR_THEMES, FONT_SIZES } from '../query/AceEditor.svelte';
import { closeCurrentModal } from "../modals/modalTools";
import TextField from "../forms/TextField.svelte";
import FormTextField from "../forms/FormTextField.svelte";
import SqlEditor from "../query/SqlEditor.svelte";
function openThemePlugins() {
closeCurrentModal();
$selectedWidget = 'plugins';
$visibleWidgetSideBar = true;
}
const sqlPreview = `-- example query
SELECT
MAX(Album.AlbumId) AS max_album,
MAX(Album.Title) AS max_title,
Artist.ArtistId,
'album' AS test_string,
123 AS test_number
FROM
Album
INNER JOIN Artist ON Album.ArtistId = Artist.ArtistId
GROUP BY
Artist.ArtistId
ORDER BY
Artist.Name ASC
`;
</script>
<div class="wrapper">
<div class="heading">{_t('settings.appearance', { defaultMessage: 'Application theme' })}</div>
<FormFieldTemplateLarge
label={_t('settings.appearance.useSystemTheme', { defaultMessage: 'Use system theme' })}
type="checkbox"
labelProps={{
onClick: () => {
if ($currentTheme) {
$currentTheme = null;
} else {
$currentTheme = getSystemTheme();
}
},
}}
>
<CheckboxField
checked={!$currentTheme}
on:change={e => {
if (e.target['checked']) {
$currentTheme = null;
} else {
$currentTheme = getSystemTheme();
}
}}
/>
</FormFieldTemplateLarge>
<div class="themes">
{#each $extensions.themes as theme}
<ThemeSkeleton {theme} />
{/each}
</div>
<div class="m-5">
{_t('settings.appearance.moreThemes', { defaultMessage: 'More themes are available as' })}
<Link onClick={openThemePlugins}>plugins</Link>
<br />
{_t('settings.appearance.afterInstalling', {
defaultMessage:
'After installing theme plugin (try search "theme" in available extensions) new themes will be available here.',
})}
</div>
<div class="heading">{_t('settings.appearance.editorTheme', { defaultMessage: 'Editor theme' })}</div>
<div class="flex">
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.appearance.editorTheme', { defaultMessage: 'Theme' })}
type="combo"
>
<SelectField
isNative
notSelected={_t('settings.appearance.editorTheme.default', { defaultMessage: '(use theme default)' })}
options={EDITOR_THEMES.map(theme => ({ label: theme, value: theme }))}
value={$currentEditorTheme}
on:change={e => ($currentEditorTheme = e.detail)}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.appearance.fontSize', { defaultMessage: 'Font size' })}
type="combo"
>
<SelectField
isNative
notSelected="(default)"
options={FONT_SIZES}
value={FONT_SIZES.find(x => x.value == $currentEditorFontSize) ? $currentEditorFontSize : 'custom'}
on:change={e => ($currentEditorFontSize = e.detail)}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.appearance.customSize', { defaultMessage: 'Custom size' })}
type="text"
>
<TextField
value={$currentEditorFontSize == 'custom' ? '' : $currentEditorFontSize}
on:change={e => ($currentEditorFontSize = e.target['value'])}
disabled={!!FONT_SIZES.find(x => x.value == $currentEditorFontSize) &&
$currentEditorFontSize != 'custom'}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormTextField
name="editor.fontFamily"
label={_t('settings.appearance.fontFamily', { defaultMessage: 'Editor font family' })}
/>
</div>
</div>
<div class="editor">
<SqlEditor value={sqlPreview} readOnly />
</div>
</div>
<style>
.heading {
font-size: 20px;
margin: 5px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
}
.themes {
display: flex;
flex-wrap: wrap;
margin-left: var(--dim-large-form-margin);
}
.editor {
position: relative;
height: 250px;
width: 400px;
margin-left: var(--dim-large-form-margin);
margin-top: var(--dim-large-form-margin);
margin-bottom: var(--dim-large-form-margin);
}
</style>
+1
View File
@@ -200,6 +200,7 @@ export const DEFAULT_OBJECT_SEARCH_SETTINGS = {
sqlObjectText: false,
tableEngine: false,
tablesWithRows: false,
sortBy: undefined as string
};
export const DEFAULT_CONNECTION_SEARCH_SETTINGS = {
@@ -60,7 +60,11 @@
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">{constraintInfo ? _t('foreignKeyEditor.editForeignKey', { defaultMessage: 'Edit foreign key' }) : _t('foreignKeyEditor.addForeignKey', { defaultMessage: 'Add foreign key' })}</svelte:fragment>
<svelte:fragment slot="header"
>{constraintInfo
? _t('foreignKeyEditor.editForeignKey', { defaultMessage: 'Edit foreign key' })
: _t('foreignKeyEditor.addForeignKey', { defaultMessage: 'Add foreign key' })}</svelte:fragment
>
<div class="largeFormMarker">
<div class="row">
@@ -92,6 +96,19 @@
const name = fullNameFromString(e.detail);
refTableName = name.pureName;
refSchemaName = name.schemaName;
if (!columns?.find(x => x.columnName)) {
const refTable = dbInfo?.tables?.find(
x => x.pureName == refTableName && x.schemaName == refSchemaName
);
if (refTable?.primaryKey) {
columns = refTable.primaryKey.columns.map(col => ({
refColumnName: col.columnName,
}));
} else {
columns = [];
}
}
}
}}
/>
@@ -135,7 +152,8 @@
{_t('foreignKeyEditor.baseColumn', { defaultMessage: 'Base column - ' })}{tableInfo.pureName}
</div>
<div class="col-5 ml-1">
{_t('foreignKeyEditor.refColumn', { defaultMessage: 'Ref column - ' })}{refTableName || _t('foreignKeyEditor.tableNotSet', { defaultMessage: '(table not set)' })}
{_t('foreignKeyEditor.refColumn', { defaultMessage: 'Ref column - ' })}{refTableName ||
_t('foreignKeyEditor.tableNotSet', { defaultMessage: '(table not set)' })}
</div>
</div>
@@ -217,7 +235,11 @@
}}
/>
<FormStyledButton type="button" value={_t('common.close', { defaultMessage: 'Close' })} on:click={closeCurrentModal} />
<FormStyledButton
type="button"
value={_t('common.close', { defaultMessage: 'Close' })}
on:click={closeCurrentModal}
/>
{#if constraintInfo}
<FormStyledButton
type="button"
+64 -25
View File
@@ -3,8 +3,8 @@
registerCommand({
id: 'tableEditor.addColumn',
category: __t('tableEditor', { defaultMessage: 'Table editor'}),
name: __t('tableEditor.addColumn', { defaultMessage: 'Add column'}),
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addColumn', { defaultMessage: 'Add column' }),
icon: 'icon add-column',
toolbar: true,
isRelatedToTab: true,
@@ -14,8 +14,8 @@
registerCommand({
id: 'tableEditor.addPrimaryKey',
category: __t('tableEditor', { defaultMessage: 'Table editor'}),
name: __t('tableEditor.addPrimaryKey', { defaultMessage: 'Add primary key'}),
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addPrimaryKey', { defaultMessage: 'Add primary key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
@@ -25,8 +25,8 @@
registerCommand({
id: 'tableEditor.addForeignKey',
category: __t('tableEditor', { defaultMessage: 'Table editor'}),
name: __t('tableEditor.addForeignKey', { defaultMessage: 'Add foreign key'}),
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addForeignKey', { defaultMessage: 'Add foreign key' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
@@ -36,8 +36,8 @@
registerCommand({
id: 'tableEditor.addIndex',
category: __t('tableEditor', { defaultMessage: 'Table editor'}),
name: __t('tableEditor.addIndex', { defaultMessage: 'Add index'}),
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addIndex', { defaultMessage: 'Add index' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
@@ -47,8 +47,8 @@
registerCommand({
id: 'tableEditor.addUnique',
category: __t('tableEditor', { defaultMessage: 'Table editor'}),
name: __t('tableEditor.addUnique', { defaultMessage: 'Add unique'}),
category: __t('tableEditor', { defaultMessage: 'Table editor' }),
name: __t('tableEditor.addUnique', { defaultMessage: 'Add unique' }),
icon: 'icon add-key',
toolbar: true,
isRelatedToTab: true,
@@ -188,7 +188,10 @@
<ObjectListControl
collection={columns?.map((x, index) => ({ ...x, ordinal: index + 1 }))}
title={_t('tableEditor.columns', { defaultMessage: 'Columns ({columnCount})', values: { columnCount: columns?.length || 0 } })}
title={_t('tableEditor.columns', {
defaultMessage: 'Columns ({columnCount})',
values: { columnCount: columns?.length || 0 },
})}
emptyMessage={_t('tableEditor.nocolumnsdefined', { defaultMessage: 'No columns defined' })}
clickable
on:clickrow={e => showModal(ColumnEditorModal, { columnInfo: e.detail, tableInfo, setTableInfo, driver })}
@@ -217,9 +220,7 @@
text: _t('tableEditor.copydefinitions', { defaultMessage: 'Copy definitions' }),
icon: 'icon copy',
onClick: selected => {
const names = selected
.map(x => `${x.columnName} ${x.dataType}${x.notNull ? ' NOT NULL' : ''}`)
.join(',\n');
const names = selected.map(x => `${x.columnName} ${x.dataType}${x.notNull ? ' NOT NULL' : ''}`).join(',\n');
navigator.clipboard.writeText(names);
},
},
@@ -288,9 +289,21 @@
: null,
]}
>
<svelte:fragment slot="0" let:row>{row?.notNull ? _t('tableEditor.notnull', { defaultMessage: 'NOT NULL' }) : _t('tableEditor.null', { defaultMessage: 'NULL' })}</svelte:fragment>
<svelte:fragment slot="1" let:row>{row?.isSparse ? _t('tableEditor.yes', { defaultMessage: 'YES' }) : _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment>
<svelte:fragment slot="2" let:row>{row?.isPersisted ? _t('tableEditor.yes', { defaultMessage: 'YES' }) : _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment>
<svelte:fragment slot="0" let:row
>{row?.notNull
? _t('tableEditor.notnull', { defaultMessage: 'NOT NULL' })
: _t('tableEditor.null', { defaultMessage: 'NULL' })}</svelte:fragment
>
<svelte:fragment slot="1" let:row
>{row?.isSparse
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="2" let:row
>{row?.isPersisted
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="3" let:row
><Link
onClick={e => {
@@ -299,8 +312,16 @@
}}>{_t('tableEditor.remove', { defaultMessage: 'Remove' })}</Link
></svelte:fragment
>
<svelte:fragment slot="4" let:row>{row?.isUnsigned ? _t('tableEditor.yes', { defaultMessage: 'YES' }) : _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment>
<svelte:fragment slot="5" let:row>{row?.isZerofill ? _t('tableEditor.yes', { defaultMessage: 'YES' }) : _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment>
<svelte:fragment slot="4" let:row
>{row?.isUnsigned
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="5" let:row
>{row?.isZerofill
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="name" let:row><ColumnLabel {...row} forceIcon /></svelte:fragment>
</ObjectListControl>
@@ -321,7 +342,10 @@
<ObjectListControl
collection={indexes}
onAddNew={isWritable && columns?.length > 0 ? addIndex : null}
title={_t('tableEditor.indexes', { defaultMessage: 'Indexes ({indexCount})', values: { indexCount: indexes?.length || 0 } })}
title={_t('tableEditor.indexes', {
defaultMessage: 'Indexes ({indexCount})',
values: { indexCount: indexes?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.noindexdefined', { defaultMessage: 'No index defined' }) : null}
clickable
on:clickrow={e => showModal(IndexEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, driver })}
@@ -348,7 +372,11 @@
>
<svelte:fragment slot="name" let:row><ConstraintLabel {...row} /></svelte:fragment>
<svelte:fragment slot="0" let:row>{row?.columns.map(x => x.columnName).join(', ')}</svelte:fragment>
<svelte:fragment slot="1" let:row>{row?.isUnique ? _t('tableEditor.yes', { defaultMessage: 'YES' }) : _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment>
<svelte:fragment slot="1" let:row
>{row?.isUnique
? _t('tableEditor.yes', { defaultMessage: 'YES' })
: _t('tableEditor.no', { defaultMessage: 'NO' })}</svelte:fragment
>
<svelte:fragment slot="2" let:row
><Link
onClick={e => {
@@ -364,7 +392,10 @@
<ObjectListControl
collection={uniques}
onAddNew={isWritable && columns?.length > 0 ? addUnique : null}
title={_t('tableEditor.uniqueConstraints', { defaultMessage: 'Unique constraints ({constraintCount})', values: { constraintCount: uniques?.length || 0 } })}
title={_t('tableEditor.uniqueConstraints', {
defaultMessage: 'Unique constraints ({constraintCount})',
values: { constraintCount: uniques?.length || 0 },
})}
emptyMessage={isWritable ? _t('tableEditor.nouniquedefined', { defaultMessage: 'No unique defined' }) : null}
clickable
on:clickrow={e => showModal(UniqueEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo })}
@@ -401,13 +432,21 @@
<ForeignKeyObjectListControl
collection={foreignKeys}
onAddNew={isWritable && columns?.length > 0 ? addForeignKey : null}
title={_t('tableEditor.foreignKeys', { defaultMessage: 'Foreign keys ({foreignKeyCount})', values: { foreignKeyCount: foreignKeys?.length || 0 } })}
emptyMessage={isWritable ? _t('tableEditor.noforeignkeydefined', { defaultMessage: 'No foreign key defined' }) : null}
title={_t('tableEditor.foreignKeys', {
defaultMessage: 'Foreign keys ({foreignKeyCount})',
values: { foreignKeyCount: foreignKeys?.length || 0 },
})}
emptyMessage={isWritable
? _t('tableEditor.noforeignkeydefined', { defaultMessage: 'No foreign key defined' })
: null}
clickable
onRemove={row => setTableInfo(tbl => editorDeleteConstraint(tbl, row))}
on:clickrow={e => showModal(ForeignKeyEditorModal, { constraintInfo: e.detail, tableInfo, setTableInfo, dbInfo })}
/>
<ForeignKeyObjectListControl collection={dependencies} title={_t('tableEditor.dependencies', { defaultMessage: 'Dependencies' })} />
<ForeignKeyObjectListControl
collection={dependencies}
title={_t('tableEditor.dependencies', { defaultMessage: 'Dependencies' })}
/>
{/if}
</div>
+5 -2
View File
@@ -758,7 +758,7 @@
title="Upgrade to Premium"
data-testid="TabsPanel_buttonUpgrade"
>
<FontIcon icon="icon premium" padRight /> Upgrade
<FontIcon icon="icon premium" /> Upgrade
</div>
{/if}
@@ -803,6 +803,9 @@
cursor: pointer;
font-size: 10pt;
padding: 5px;
margin-top: 3px;
margin-right: 3px;
font-size: 8pt;
}
.upgrade-button:hover {
background: linear-gradient(135deg, #0f5a85, #5c1870);
@@ -822,7 +825,7 @@
}
.tabs-upgrade-button {
right: 120px;
right: 110px;
}
.tabs.can-split {
right: 60px;
+32 -31
View File
@@ -24,6 +24,7 @@
createQuickExportHandlerRef,
registerQuickExportHandler,
} from '../buttons/ToolStripExportButton.svelte';
import { _t } from '../translations';
let loadedRows = [];
let loadedAll = false;
@@ -191,8 +192,8 @@
<SelectField
isNative
options={[
{ label: 'Recent logs', value: 'recent' },
{ label: 'Choose date', value: 'date' },
{ label: _t('logs.recentLogs', { defaultMessage: 'Recent logs' }), value: 'recent' },
{ label: _t('logs.chooseDate', { defaultMessage: 'Choose date' }), value: 'date' },
]}
value={mode}
on:change={e => {
@@ -202,7 +203,7 @@
/>
{#if mode === 'recent'}
<div class="filter-label ml-2">Auto-scroll</div>
<div class="filter-label ml-2">{_t('logs.autoScroll', { defaultMessage: 'Auto-scroll' })}</div>
<input
type="checkbox"
checked={autoScroll}
@@ -213,7 +214,7 @@
{/if}
{#if mode === 'date'}
<div class="filter-label">Date:</div>
<div class="filter-label">{_t('logs.date', { defaultMessage: 'Date:' })}</div>
<DateRangeSelector
onChange={value => {
dateFilter = value;
@@ -225,12 +226,12 @@
data-testid="AdminAuditLogTab_addFilter"
icon="icon filter"
menu={[
{ text: 'Connection ID', onClick: () => filterBy('conid') },
{ text: 'Database', onClick: () => filterBy('database') },
{ text: 'Engine', onClick: () => filterBy('engine') },
{ text: 'Message code', onClick: () => filterBy('msgcode') },
{ text: 'Caller', onClick: () => filterBy('caller') },
{ text: 'Name', onClick: () => filterBy('name') },
{ text: _t('logs.connectionId', { defaultMessage: 'Connection ID' }), onClick: () => filterBy('conid') },
{ text: _t('logs.database', { defaultMessage: 'Database' }), onClick: () => filterBy('database') },
{ text: _t('logs.engine', { defaultMessage: 'Engine' }), onClick: () => filterBy('engine') },
{ text: _t('logs.messageCode', { defaultMessage: 'Message code' }), onClick: () => filterBy('msgcode') },
{ text: _t('logs.caller', { defaultMessage: 'Caller' }), onClick: () => filterBy('caller') },
{ text: _t('logs.name', { defaultMessage: 'Name' }), onClick: () => filterBy('name') },
]}
/>
</div>
@@ -259,15 +260,15 @@
<table>
<thead>
<tr>
<th style="width:80px">Date</th>
<th>Time</th>
<th>Code</th>
<th>Message</th>
<th>Connection</th>
<th>Database</th>
<th>Engine</th>
<th>Caller</th>
<th>Name</th>
<th style="width:80px">{_t('logs.dateTab', { defaultMessage: 'Date' })}</th>
<th>{_t('logs.timeTab', { defaultMessage: 'Time' })}</th>
<th>{_t('logs.codeTab', { defaultMessage: 'Code' })}</th>
<th>{_t('logs.messageTab', { defaultMessage: 'Message' })}</th>
<th>{_t('logs.connectionTab', { defaultMessage: 'Connection' })}</th>
<th>{_t('logs.databaseTab', { defaultMessage: 'Database' })}</th>
<th>{_t('logs.engineTab', { defaultMessage: 'Engine' })}</th>
<th>{_t('logs.callerTab', { defaultMessage: 'Caller' })}</th>
<th>{_t('logs.nameTab', { defaultMessage: 'Name' })}</th>
</tr>
</thead>
<tbody>
@@ -299,14 +300,14 @@
<TabControl
isInline
tabs={_.compact([
{ label: 'Details', slot: 1 },
{ label: _t('logs.details', { defaultMessage: 'Details' }), slot: 1 },
{ label: 'JSON', slot: 2 },
])}
>
<svelte:fragment slot="1">
<div class="details-wrap">
<div class="row">
<div>Message code:</div>
<div>{_t('logs.messageCode', { defaultMessage: 'Message code:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('msgcode', [row.msgcode])}>{row.msgcode || 'N/A'}</Link>
{:else}
@@ -314,15 +315,15 @@
{/if}
</div>
<div class="row">
<div>Message:</div>
<div>{_t('logs.message', { defaultMessage: 'Message:' })}</div>
{row.msg}
</div>
<div class="row">
<div>Time:</div>
<div>{_t('logs.time', { defaultMessage: 'Time:' })}</div>
<b>{row.time ? format(new Date(parseInt(row.time)), 'yyyy-MM-dd HH:mm:ss') : ''}</b>
</div>
<div class="row">
<div>Caller:</div>
<div>{_t('logs.caller', { defaultMessage: 'Caller:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('caller', [row.caller])}>{row.caller || 'N/A'}</Link>
{:else}
@@ -330,7 +331,7 @@
{/if}
</div>
<div class="row">
<div>Name:</div>
<div>{_t('logs.name', { defaultMessage: 'Name:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('name', [row.name])}>{row.name || 'N/A'}</Link>
{:else}
@@ -339,7 +340,7 @@
</div>
{#if row.conid}
<div class="row">
<div>Connection ID:</div>
<div>{_t('logs.connectionId', { defaultMessage: 'Connection ID:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('conid', [row.conid])}
>{formatPossibleUuid(row.conid)}</Link
@@ -351,7 +352,7 @@
{/if}
{#if row.database}
<div class="row">
<div>Database:</div>
<div>{_t('logs.database', { defaultMessage: 'Database:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('database', [row.database])}>{row.database}</Link>
{:else}
@@ -361,7 +362,7 @@
{/if}
{#if row.engine}
<div class="row">
<div>Engine:</div>
<div>{_t('logs.engine', { defaultMessage: 'Engine:' })}</div>
{#if mode == 'date'}
<Link onClick={() => doSetFilter('engine', [row.engine])}>{row.engine}</Link>
{:else}
@@ -381,13 +382,13 @@
{/each}
{#if !loadedRows?.length && mode === 'date'}
<tr>
<td colspan="6">No data for selected date</td>
<td colspan="6">{_t('logs.noDataForSelectedDate', { defaultMessage: "No data for selected date" })}</td>
</tr>
{/if}
{#if !loadedAll && mode === 'date'}
{#key loadedRows}
<tr>
<td colspan="6" bind:this={domLoadNext}>Loading next rows... </td>
<td colspan="6" bind:this={domLoadNext}>{_t('logs.loadingNextRows', { defaultMessage: "Loading next rows..." })}</td>
</tr>
{/key}
{/if}
@@ -402,7 +403,7 @@
data-testid="AdminAuditLogTab_refreshButton"
on:click={() => {
reloadData();
}}>Refresh</ToolStripButton
}}>{_t('logs.refresh', { defaultMessage: 'Refresh' })}</ToolStripButton
>
<ToolStripExportButton {quickExportHandlerRef} />
</svelte:fragment>
+4 -1
View File
@@ -13,6 +13,7 @@
import CommandModal from '../modals/CommandModal.svelte';
import { showModal } from '../modals/modalTools';
import { commandsCustomized } from '../stores';
import { _tval } from '../translations';
$: commandList = _.sortBy(_.values($commandsCustomized), ['category', 'name']);
let filter;
@@ -27,7 +28,9 @@
<div class="table-wrapper">
<TableControl
clickable
rows={commandList.filter(cmd => filterName(filter, cmd['category'], cmd['name'], cmd['keyText'], cmd['id']))}
rows={commandList.filter(cmd =>
filterName(filter, _tval(cmd['category']), _tval(cmd['name']), _tval(cmd['keyText']), cmd['id'])
)}
columns={[
{ header: 'Category', fieldName: 'category' },
{ header: 'Name', fieldName: 'name' },
+1
View File
@@ -8,6 +8,7 @@
folder: 'diagrams',
format: 'json',
fileExtension: 'diagram',
defaultTeamFolder: true,
undoRedo: true,
});
+12 -10
View File
@@ -10,6 +10,7 @@
fileExtension: 'impexp',
// undoRedo: true,
defaultTeamFolder: true,
});
</script>
@@ -53,6 +54,7 @@
import uuidv1 from 'uuid/v1';
import { tick } from 'svelte';
import { showSnackbarError } from '../utility/snackbar';
import { _t } from '../translations';
let busy = false;
let executeNumber = 0;
@@ -288,21 +290,21 @@
/>
{#if busy}
<LoadingInfo wrapper message="Processing import/export ..." />
<LoadingInfo wrapper message={_t('importExport.processingImportExport', { defaultMessage: "Processing import/export ..." })} />
{/if}
</div>
<svelte:fragment slot="2">
<WidgetColumnBar>
<WidgetColumnBarItem
title="Output files"
title={_t('importExport.outputFiles', { defaultMessage: "Output files" })}
name="output"
height="20%"
data-testid="ImportExportTab_outputFiles"
>
<RunnerOutputFiles {runnerId} {executeNumber} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Messages" name="messages">
<WidgetColumnBarItem title={_t('importExport.messages', { defaultMessage: "Messages" })} name="messages">
<SocketMessageView
eventName={runnerId ? `runner-info-${runnerId}` : null}
{executeNumber}
@@ -311,16 +313,16 @@
/>
</WidgetColumnBarItem>
<WidgetColumnBarItem
title="Preview"
title={_t('importExport.preview', { defaultMessage: "Preview" })}
name="preview"
skip={!$previewReaderStore}
data-testid="ImportExportTab_preview"
>
<PreviewDataGrid reader={$previewReaderStore} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Advanced configuration" name="config" collapsed>
<FormTextField label="Schedule" name="schedule" />
<FormTextField label="Start variable index" name="startVariableIndex" />
<WidgetColumnBarItem title={_t('importExport.advancedConfiguration', { defaultMessage: "Advanced configuration" })} name="config" collapsed>
<FormTextField label={_t('importExport.schedule', { defaultMessage: "Schedule" })} name="schedule" />
<FormTextField label={_t('importExport.startVariableIndex', { defaultMessage: "Start variable index" })} name="startVariableIndex" />
</WidgetColumnBarItem>
</WidgetColumnBar>
</svelte:fragment>
@@ -329,15 +331,15 @@
<svelte:fragment slot="toolstrip">
{#if busy}
<ToolStripButton icon="icon stop" on:click={handleCancel} data-testid="ImportExportTab_stopButton"
>Stop</ToolStripButton
>{_t('importExport.stop', { defaultMessage: "Stop" })}</ToolStripButton
>
{:else}
<ToolStripButton on:click={handleExecute} icon="icon run" data-testid="ImportExportTab_executeButton"
>Run</ToolStripButton
>{_t('importExport.run', { defaultMessage: "Run" })}</ToolStripButton
>
{/if}
<ToolStripButton icon="img shell" on:click={handleGenerateScript} data-testid="ImportExportTab_generateScriptButton"
>Generate script</ToolStripButton
>{_t('importExport.generateScript', { defaultMessage: "Generate script" })}</ToolStripButton
>
<ToolStripSaveButton idPrefix="impexp" />
</svelte:fragment>
+7 -1
View File
@@ -52,6 +52,7 @@
findReplace: true,
executeAdditionalCondition: () => getCurrentEditor()?.hasConnection() && hasPermission('dbops/query'),
copyPaste: true,
defaultTeamFolder: true,
});
registerCommand({
id: 'query.executeCurrent',
@@ -123,8 +124,9 @@
</script>
<script lang="ts">
import { getContext, onDestroy, onMount, tick } from 'svelte';
import { getContext, onDestroy, onMount, setContext, tick } from 'svelte';
import sqlFormatter from 'sql-formatter';
import { writable } from 'svelte/store';
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
import SqlEditor from '../query/SqlEditor.svelte';
@@ -166,6 +168,7 @@
import FontIcon from '../icons/FontIcon.svelte';
import hasPermission from '../utility/hasPermission';
import QueryAiAssistant from '../ai/QueryAiAssistant.svelte';
import { getCurrentSettings } from '../stores';
export let tabid;
export let conid;
@@ -175,6 +178,9 @@
export const activator = createActivator('QueryTab', false);
const collapsedLeftColumnStore = writable(getCurrentSettings()['sqlEditor.hideColumnsPanel'] ?? false);
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
const QUERY_PARAMETER_STYLES = [
{
value: '',
+121
View File
@@ -0,0 +1,121 @@
<script lang="ts" context="module">
export const matchingProps = [];
</script>
<script lang="ts">
import SettingsMenuControl from "../elements/SettingsMenuControl.svelte";
import GeneralSettings from "../settings/GeneralSettings.svelte";
import SettingsFormProvider from "../forms/SettingsFormProvider.svelte";
import ConnectionSettings from "../settings/ConnectionSettings.svelte";
import ThemeSettings from "../settings/ThemeSettings.svelte";
import DefaultActionsSettings from "../settings/DefaultActionsSettings.svelte";
import BehaviourSettings from "../settings/BehaviourSettings.svelte";
import ExternalToolsSettings from "../settings/ExternalToolsSettings.svelte";
import OtherSettings from "../settings/OtherSettings.svelte";
import LicenseSettings from "../settings/LicenseSettings.svelte";
import { isProApp } from "../utility/proTools";
import { _t } from "../translations";
import CommandListTab from "./CommandListTab.svelte";
import DataGridSettings from "../settings/DataGridSettings.svelte";
import SQLEditorSettings from "../settings/SQLEditorSettings.svelte";
import AiSettingsTab from "../settings/AiSettingsTab.svelte";
const menuItems = [
{
label: _t('settings.general', { defaultMessage: 'General' }),
identifier: 'general',
component: GeneralSettings,
props: {},
testid: 'settings-general',
},
{
label: _t('settings.connection', { defaultMessage: 'Connection' }),
identifier: 'connection',
component: ConnectionSettings,
props: {},
testid: 'settings-connection',
},
{
label: _t('settings.dataGrid.title', { defaultMessage: 'Data grid' }),
identifier: 'data-grid',
component: DataGridSettings,
props: {},
testid: 'settings-data-grid',
},
{
label: _t('settings.sqlEditor.title', { defaultMessage: 'SQL Editor' }),
identifier: 'sql-editor',
component: SQLEditorSettings,
props: {},
testid: 'settings-sql-editor',
},
{
label: _t('settings.theme', { defaultMessage: 'Themes' }),
identifier: 'theme',
component: ThemeSettings,
props: {},
testid: 'settings-themes',
},
{
label: _t('settings.defaultActions', { defaultMessage: 'Default Actions' }),
identifier: 'default-actions',
component: DefaultActionsSettings,
props: {},
testid: 'settings-default-actions',
},
{
label: _t('settings.behaviour', { defaultMessage: 'Behaviour' }),
identifier: 'behaviour',
component: BehaviourSettings,
props: {},
testid: 'settings-behaviour',
},
{
label: _t('settings.externalTools', { defaultMessage: 'External Tools' }),
identifier: 'external-tools',
component: ExternalToolsSettings,
props: {},
testid: 'settings-external-tools',
},
{
label: _t('command.settings.shortcuts', { defaultMessage: 'Keyboard shortcuts' }),
identifier: 'shortcuts',
component: CommandListTab,
props: {},
testid: 'settings-shortcuts',
},
isProApp() && {
label: _t('settings.license', { defaultMessage: 'License' }),
identifier: 'license',
component: LicenseSettings,
props: {},
testid: 'settings-license',
},
isProApp() && {
label: _t('settings.AI', { defaultMessage: 'AI'}),
identifier: 'ai',
component: AiSettingsTab,
props: {},
testid: 'settings-ai',
},
{
label: _t('settings.other', { defaultMessage: 'Other' }),
identifier: 'other',
component: OtherSettings,
props: {},
testid: 'settings-other',
},
];
let selectedItem = 'general';
</script>
<SettingsFormProvider>
<SettingsMenuControl
items={menuItems}
bind:value={selectedItem}
flex1={true}
flexColContainer={true}
scrollableContentContainer={true}
/>
</SettingsFormProvider>
+1
View File
@@ -12,6 +12,7 @@
execute: true,
toggleComment: true,
findReplace: true,
defaultTeamFolder: true,
executeAdditionalCondition: () => getCurrentConfig().allowShellScripting,
});
+2
View File
@@ -25,6 +25,7 @@ import * as ServerSummaryTab from './ServerSummaryTab.svelte';
import * as ImportExportTab from './ImportExportTab.svelte';
import * as SqlObjectTab from './SqlObjectTab.svelte';
import * as AppLogTab from './AppLogTab.svelte';
import * as SettingsTab from './SettingsTab.svelte';
import protabs from './index-pro';
@@ -56,5 +57,6 @@ export default {
ImportExportTab,
SqlObjectTab,
AppLogTab,
SettingsTab,
...protabs,
};
+16 -7
View File
@@ -4,6 +4,9 @@ import de from '../../../translations/de.json';
import fr from '../../../translations/fr.json';
import es from '../../../translations/es.json';
import zh from '../../../translations/zh.json';
import pt from '../../../translations/pt.json';
import it from '../../../translations/it.json';
import ja from '../../../translations/ja.json';
import MessageFormat, { MessageFunction } from '@messageformat/core';
import { getStringSettingsValue } from './settings/settingsTools';
@@ -16,8 +19,11 @@ const translations = {
sk,
de,
fr,
es,
zh,
es,
pt,
it,
ja,
};
const supportedLanguages = Object.keys(translations);
@@ -27,13 +33,16 @@ const defaultLanguage = 'en';
let selectedLanguageCache: string | null = null;
export function getSelectedLanguage(): string {
export function getSelectedLanguage(preferrendLanguage?: string): string {
if (selectedLanguageCache) return selectedLanguageCache;
// const browserLanguage = getBrowserLanguage();
if (preferrendLanguage == 'auto') {
preferrendLanguage = getBrowserLanguage();
}
const selectedLanguage = getElectron()
? getStringSettingsValue('localization.language', null)
: localStorage.getItem('selectedLanguage');
? getStringSettingsValue('localization.language', preferrendLanguage)
: localStorage.getItem('selectedLanguage') ?? preferrendLanguage;
if (!selectedLanguage || !supportedLanguages.includes(selectedLanguage)) return defaultLanguage;
return selectedLanguage;
@@ -47,8 +56,8 @@ export async function setSelectedLanguage(language: string) {
}
}
export function saveSelectedLanguageToCache() {
selectedLanguageCache = getSelectedLanguage();
export function saveSelectedLanguageToCache(preferrendLanguage?: string) {
selectedLanguageCache = getSelectedLanguage(preferrendLanguage);
}
export function getBrowserLanguage(): string {
+1
View File
@@ -186,6 +186,7 @@ export async function apiCall(
headers: {
'Content-Type': 'application/json',
'x-api-session-id': getApiSessionId(),
'x-ui-language': localStorage.getItem('selectedLanguage') || 'en',
...resolveApiHeaders(),
},
body: JSON.stringify(args, serializeJsTypesReplacer),
+41 -1
View File
@@ -4,6 +4,8 @@ import _ from 'lodash';
import { getSchemaList } from './metadataLoaders';
import { showSnackbarError } from './snackbar';
import { _t } from '../translations';
import { apiCall } from './api';
import getElectron from './getElectron';
export class LoadingToken {
isCanceled = false;
@@ -63,7 +65,8 @@ export function getObjectTypeFieldLabel(objectTypeField, driver?) {
if (objectTypeField == 'procedures') return _t('dbObject.procedures', { defaultMessage: 'Procedures' });
if (objectTypeField == 'functions') return _t('dbObject.functions', { defaultMessage: 'Functions' });
if (objectTypeField == 'triggers') return _t('dbObject.triggers', { defaultMessage: 'Triggers' });
if (objectTypeField == 'schedulerEvents') return _t('dbObject.schedulerEvents', { defaultMessage: 'Scheduler Events' });
if (objectTypeField == 'schedulerEvents')
return _t('dbObject.schedulerEvents', { defaultMessage: 'Scheduler Events' });
if (objectTypeField == 'matviews') return _t('dbObject.matviews', { defaultMessage: 'Materialized Views' });
if (objectTypeField == 'collections') return _t('dbObject.collections', { defaultMessage: 'Collections/Containers' });
return _.startCase(objectTypeField);
@@ -151,3 +154,40 @@ export function getKeyTextFromEvent(e) {
keyText += e.key;
return keyText;
}
export function getDatabasStatusMenu(dbid) {
function callSchemalListChanged() {
apiCall('database-connections/dispatch-database-changed-event', { event: 'schema-list-changed', ...dbid });
}
return [
{
text: _t('command.database.refreshIncremental', { defaultMessage: 'Refresh DB structure (incremental)' }),
onClick: () => {
apiCall('database-connections/sync-model', dbid);
callSchemalListChanged();
},
},
{
text: _t('command.database.refreshFull', { defaultMessage: 'Refresh DB structure (full)' }),
onClick: () => {
apiCall('database-connections/sync-model', { ...dbid, isFullRefresh: true });
callSchemalListChanged();
},
},
{
text: _t('command.database.reopenConnection', { defaultMessage: 'Reopen connection' }),
onClick: () => {
apiCall('database-connections/refresh', dbid);
callSchemalListChanged();
},
},
{
text: _t('command.database.disconnect', { defaultMessage: 'Disconnect' }),
onClick: () => {
const electron = getElectron();
if (electron) apiCall('database-connections/disconnect', dbid);
switchCurrentDatabase(null);
},
},
];
}
+2 -1
View File
@@ -11,7 +11,7 @@ import getElectron from './getElectron';
// return derived(editorStore, editor => editor != null);
// }
export default async function saveTabFile(editor, saveMode, folder, format, fileExtension) {
export default async function saveTabFile(editor, saveMode, folder, format, fileExtension, defaultTeamFolder) {
const tabs = get(openedTabs);
const tabid = editor.activator.tabid;
const data = editor.getData();
@@ -94,6 +94,7 @@ export default async function saveTabFile(editor, saveMode, folder, format, file
filePath: savedFilePath,
onSave,
folid: savedCloudFolderId,
defaultTeamFolder,
// cntid: savedCloudContentId,
});
}
@@ -0,0 +1,132 @@
import _ from 'lodash';
import { Condition, dumpSqlInsert, dumpSqlUpdate, Insert, Update, Delete, dumpSqlDelete } from 'dbgate-sqltree';
import { TableInfo, SqlDumper } from 'dbgate-types';
export function createTableRestoreScript(backupTable: TableInfo, originalTable: TableInfo, dmp: SqlDumper) {
const bothColumns = _.intersection(
backupTable.columns.map(x => x.columnName),
originalTable.columns.map(x => x.columnName)
);
const keyColumns = _.intersection(
originalTable.primaryKey?.columns?.map(x => x.columnName) || [],
backupTable.columns.map(x => x.columnName)
);
const valueColumns = _.difference(bothColumns, keyColumns);
function makeColumnCond(colName: string, operator: '=' | '<>' | '<' | '>' | '<=' | '>=' = '='): Condition {
return {
conditionType: 'binary',
operator,
left: {
exprType: 'column',
columnName: colName,
source: { name: originalTable },
},
right: {
exprType: 'column',
columnName: colName,
source: { alias: 'bak' },
},
};
}
function putTitle(title: string) {
dmp.putRaw('\n\n');
dmp.comment(`******************** ${title} ********************`);
dmp.putRaw('\n');
}
dmp.comment(`Restoring data into table ${originalTable.pureName} from backup table ${backupTable.pureName}`);
dmp.putRaw('\n');
dmp.comment(`Key columns: ${keyColumns.join(', ')}`);
dmp.putRaw('\n');
dmp.comment(`Value columns: ${valueColumns.join(', ')}`);
dmp.putRaw('\n');
dmp.comment(`Follows UPDATE, DELETE, INSERT statements to restore data`);
dmp.putRaw('\n');
const update: Update = {
commandType: 'update',
from: { name: originalTable },
fields: valueColumns.map(colName => ({
exprType: 'select',
select: {
commandType: 'select',
from: { name: backupTable, alias: 'bak' },
columns: [
{
exprType: 'column',
columnName: colName,
source: { alias: 'bak' },
},
],
where: {
conditionType: 'and',
conditions: keyColumns.map(colName => makeColumnCond(colName)),
},
},
targetColumn: colName,
})),
where: {
conditionType: 'exists',
subQuery: {
commandType: 'select',
from: { name: backupTable, alias: 'bak' },
selectAll: true,
where: {
conditionType: 'and',
conditions: [
...keyColumns.map(keyColName => makeColumnCond(keyColName)),
{
conditionType: 'or',
conditions: valueColumns.map(colName => makeColumnCond(colName, '<>')),
},
],
},
},
},
};
putTitle('UPDATE');
dumpSqlUpdate(dmp, update);
dmp.endCommand();
const delcmd: Delete = {
commandType: 'delete',
from: { name: originalTable },
where: {
conditionType: 'notExists',
subQuery: {
commandType: 'select',
from: { name: backupTable, alias: 'bak' },
selectAll: true,
where: {
conditionType: 'and',
conditions: keyColumns.map(colName => makeColumnCond(colName)),
},
},
},
};
putTitle('DELETE');
dumpSqlDelete(dmp, delcmd);
dmp.endCommand();
const insert: Insert = {
commandType: 'insert',
targetTable: originalTable,
fields: bothColumns.map(colName => ({
targetColumn: colName,
exprType: 'column',
columnName: colName,
source: { alias: 'bak' },
})),
whereNotExistsSource: { name: backupTable, alias: 'bak' },
insertWhereNotExistsCondition: {
conditionType: 'and',
conditions: keyColumns.map(colName => makeColumnCond(colName)),
},
};
putTitle('INSERT');
dumpSqlInsert(dmp, insert);
dmp.endCommand();
}
@@ -101,6 +101,7 @@
import WidgetTitle from './WidgetTitle.svelte';
import JsonExpandedCellView from '../celldata/JsonExpandedCellView.svelte';
import XmlCellView from '../celldata/XmlCellView.svelte';
import { _t } from '../translations';
let selectedFormatType = 'autodetect';
@@ -116,7 +117,7 @@
</script>
<div class="wrapper">
<WidgetTitle>Cell data view</WidgetTitle>
<WidgetTitle>{_t('cellDataWidget.title', { defaultMessage: "Cell data view" })}</WidgetTitle>
<div class="main">
<div class="toolbar">
Format:<span>&nbsp;</span>
@@ -126,18 +127,18 @@
on:change={e => (selectedFormatType = e.detail)}
data-testid="CellDataWidget_selectFormat"
options={[
{ value: 'autodetect', label: `Autodetect - ${autodetectFormat.title}` },
{ value: 'autodetect', label: _t('cellDataWidget.autodetect', { defaultMessage: "Autodetect - {autoDetectTitle}", values : { autoDetectTitle: autodetectFormat.title } }) },
...formats.map(fmt => ({ label: fmt.title, value: fmt.type })),
]}
/>
</div>
<div class="data">
{#if usedFormat.single && selection?.length != 1}
<ErrorInfo message="Must be selected one cell" alignTop />
<ErrorInfo message={_t('cellDataWidget.mustSelectOneCell', { defaultMessage: "Must be selected one cell" })} alignTop />
{:else if usedFormat == null}
<ErrorInfo message="Format not selected" alignTop />
<ErrorInfo message={_t('cellDataWidget.formatNotSelected', { defaultMessage: "Format not selected" })} alignTop />
{:else if !selection || selection.length == 0}
<ErrorInfo message="No data selected" alignTop />
<ErrorInfo message={_t('cellDataWidget.noDataSelected', { defaultMessage: "No data selected" })} alignTop />
{:else}
<svelte:component this={usedFormat?.component} {selection} />
{/if}
+2 -2
View File
@@ -19,12 +19,12 @@
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Saved files" name="files" height="70%" storageName="savedFilesWidget">
<WidgetColumnBarItem title={_t('files.savedFiles', { defaultMessage: "Saved files" })} name="files" height="70%" storageName="savedFilesWidget">
<SavedFilesList />
</WidgetColumnBarItem>
{#if hasPermission('files/favorites/read')}
<WidgetColumnBarItem title="Favorites" name="favorites" storageName="favoritesWidget">
<WidgetColumnBarItem title={_t('files.favorites', { defaultMessage: "Favorites" })} name="favorites" storageName="favoritesWidget">
<WidgetsInnerContainer>
<AppObjectList list={$favorites || []} module={favoriteFileAppObject} />
</WidgetsInnerContainer>
@@ -13,13 +13,14 @@
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { _t } from '../translations';
$: favorites = useFavorites();
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" storageName='closedTabsWidget'>
<WidgetColumnBarItem title={_t('history.recentlyClosedTabs', { defaultMessage: "Recently closed tabs" })} name="closedTabs" storageName='closedTabsWidget'>
<WidgetsInnerContainer>
<AppObjectList
list={_.sortBy(
@@ -30,7 +31,7 @@
/>
</WidgetsInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Query history" name="queryHistory" storageName='queryHistoryWidget'>
<WidgetColumnBarItem title={_t('history.queryHistory', { defaultMessage: "Query history" })} name="queryHistory" storageName='queryHistoryWidget'>
<QueryHistoryList />
</WidgetColumnBarItem>
</WidgetColumnBar>
@@ -4,13 +4,15 @@
import WidgetColumnBar from './WidgetColumnBar.svelte';
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
import { _t } from '../translations';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Installed extensions" name="installed" height="50%" storageName='installedPluginsWidget'>
<WidgetColumnBarItem title={_t('widgets.installedExtensions', { defaultMessage: 'Installed extensions' })} name="installed" height="50%" storageName='installedPluginsWidget'>
<InstalledPluginsList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Available extensions" name="all" storageName='allPluginsWidget'>
<WidgetColumnBarItem title={_t('widgets.availableExtensions', { defaultMessage: 'Available extensions' })} name="all" storageName='allPluginsWidget'>
<AvailablePluginsList />
</WidgetColumnBarItem>
</WidgetColumnBar>
@@ -30,11 +30,11 @@
<WidgetColumnBarItem title="Public Knowledge Base" name="publicCloud" storageName="publicCloudItems">
<WidgetsInnerContainer>
<SearchBoxWrapper>
<SearchInput placeholder="Search public files" bind:value={filter} />
<SearchInput placeholder={_t('publicCloudWidget.searchPublicFiles', { defaultMessage: "Search public files" })} bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineButton
on:click={handleRefreshPublic}
title="Refresh files"
title={_t('publicCloudWidget.refreshFiles', { defaultMessage: "Refresh files" })}
data-testid="CloudItemsWidget_buttonRefreshPublic"
>
<FontIcon icon="icon refresh" />
@@ -52,9 +52,9 @@
<ErrorInfo message="No files found for your configuration" />
<div class="error-info">
<div class="m-1">
Only files relevant for your connections, platform and DbGate edition are listed. Please define connections at first.
{_t('publicCloudWidget.onlyRelevantFilesListed', { defaultMessage: "Only files relevant for your connections, platform and DbGate edition are listed. Please define connections at first." })}
</div>
<FormStyledButton value={`Refresh list`} skipWidth on:click={handleRefreshPublic} />
<FormStyledButton value={_t('publicCloudWidget.refreshList', { defaultMessage: "Refresh list" })} skipWidth on:click={handleRefreshPublic} />
</div>
{/if}
</WidgetsInnerContainer>
@@ -9,6 +9,7 @@
import openNewTab from '../utility/openNewTab';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { apiCall, apiOff, apiOn } from '../utility/api';
import { _t } from '../translations';
let filter = '';
let search = '';
@@ -38,7 +39,7 @@
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search query history" {filter} bind:value={filter} />
<SearchInput placeholder={_t('history.searchQueryHistory', { defaultMessage: "Search query history" })} {filter} bind:value={filter} />
<CloseSearchButton
bind:filter
on:click={() => {
@@ -54,7 +55,7 @@
on:click={() => {
openNewTab(
{
title: 'Query #',
title: _t('database.queryDesigner', { defaultMessage: "Query #" }),
icon: 'icon sql-file',
tabComponent: 'QueryTab',
focused: true,
@@ -13,6 +13,7 @@
import { isProApp } from '../utility/proTools';
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
import { _t } from '../translations';
let filter = '';
@@ -65,19 +66,19 @@
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search saved files" bind:value={filter} />
<SearchInput placeholder={_t('files.searchSavedFiles', { defaultMessage: "Search saved files" })} bind:value={filter} />
<CloseSearchButton bind:filter />
<InlineUploadButton
filters={[
{
name: `All supported files`,
name: _t('files.allSupportedFiles', { defaultMessage: "All supported files" }),
extensions: ['sql'],
},
{ name: `SQL files`, extensions: ['sql'] },
{ name: _t('files.sqlFiles', { defaultMessage: "SQL files" }), extensions: ['sql'] },
]}
onProcessFile={handleUploadedFile}
/>
<InlineButton on:click={handleRefreshFiles} title="Refresh files" data-testid="SavedFileList_buttonRefresh">
<InlineButton on:click={handleRefreshFiles} title={_t('files.refreshFiles', { defaultMessage: "Refresh files" })} data-testid="SavedFileList_buttonRefresh">
<FontIcon icon="icon refresh" />
</InlineButton>
</SearchBoxWrapper>
@@ -86,7 +87,7 @@
<AppObjectList
list={files}
module={savedFileAppObject}
groupFunc={data => (data.teamFileId ? 'Team files' : dataFolderTitle(data.folder))}
groupFunc={data => (data.teamFileId ? _t('files.teamFiles', { defaultMessage: "Team files" }) : dataFolderTitle(data.folder))}
{filter}
/>
</WidgetsInnerContainer>
+121 -27
View File
@@ -31,7 +31,7 @@
import { chevronExpandIcon } from '../icons/expandIcons';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import { getObjectTypeFieldLabel } from '../utility/common';
import { getDatabasStatusMenu, getObjectTypeFieldLabel } from '../utility/common';
import DropDownButton from '../buttons/DropDownButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
@@ -79,12 +79,30 @@
// $: console.log('OBJECTS', $objects);
$: sortArgs =
$databaseObjectAppObjectSearchSettings.sortBy == 'rowCount'
? [
['rowCount', 'sizeBytes', 'schemaName', 'pureName'],
['desc', 'desc', 'asc', 'asc'],
]
: $databaseObjectAppObjectSearchSettings.sortBy == 'sizeBytes'
? [
['sizeBytes', 'rowCount', 'schemaName', 'pureName'],
['desc', 'desc', 'asc', 'asc'],
]
: [
['schemaName', 'pureName'],
['asc', 'asc'],
];
$: objectList = _.flatten([
...['tables', 'collections', 'views', 'matviews', 'procedures', 'functions', 'triggers', 'schedulerEvents'].map(
objectTypeField =>
_.sortBy(
_.orderBy(
(($objects || {})[objectTypeField] || []).map(obj => ({ ...obj, objectTypeField })),
['schemaName', 'pureName']
sortArgs[0],
// @ts-ignore
sortArgs[1]
)
),
...appsForDb.map(app =>
@@ -102,11 +120,6 @@
// setInterval(() => (generateIndex += 1), 2000);
// $: objectList = generateObjectList(generateIndex);
const handleRefreshDatabase = () => {
apiCall('database-connections/refresh', { conid, database });
apiCall('database-connections/dispatch-database-changed-event', { event: 'schema-list-changed', conid, database });
};
function createAddMenu() {
const res = [];
if (driver?.databaseEngineTypes?.includes('document')) {
@@ -129,23 +142,75 @@
return res;
}
function createRefreshDatabaseMenu() {
return getDatabasStatusMenu({ conid, database });
}
function handleFullRefreshDatabase() {
apiCall('database-connections/sync-model', { conid, database, isFullRefresh: true });
apiCall('database-connections/dispatch-database-changed-event', { event: 'schema-list-changed', conid, database });
}
function createSearchMenu() {
const res = [];
res.push({ label: _t('sqlObject.searchBy', { defaultMessage: 'Search by:' }), isBold: true, disabled: true });
if (driver?.databaseEngineTypes?.includes('document')) {
res.push({ label: _t('sqlObject.collectionName', { defaultMessage: 'Collection name' }), switchValue: 'pureName' });
res.push({
label: _t('sqlObject.collectionName', { defaultMessage: 'Collection name' }),
switchValue: 'pureName',
});
}
if (driver?.databaseEngineTypes?.includes('sql')) {
res.push({ label: _t('sqlObject.tableViewProcedureName', { defaultMessage: 'Table/view/procedure name' }), switchValue: 'pureName' });
res.push({
label: _t('sqlObject.tableViewProcedureName', { defaultMessage: 'Table/view/procedure name' }),
switchValue: 'pureName',
});
res.push({ label: _t('sqlObject.schemaName', { defaultMessage: 'Schema' }), switchValue: 'schemaName' });
res.push({ label: _t('sqlObject.columnName', { defaultMessage: 'Column name' }), switchValue: 'columnName' });
res.push({ label: _t('sqlObject.columnDataType', { defaultMessage: 'Column data type' }), switchValue: 'columnDataType' });
res.push({ label: _t('sqlObject.tableComment', { defaultMessage: 'Table comment' }), switchValue: 'tableComment' });
res.push({ label: _t('sqlObject.columnComment', { defaultMessage: 'Column comment' }), switchValue: 'columnComment' });
res.push({ label: _t('sqlObject.viewProcedureTriggerText', { defaultMessage: 'View/procedure/trigger text' }), switchValue: 'sqlObjectText' });
res.push({
label: _t('sqlObject.columnDataType', { defaultMessage: 'Column data type' }),
switchValue: 'columnDataType',
});
res.push({
label: _t('sqlObject.tableComment', { defaultMessage: 'Table comment' }),
switchValue: 'tableComment',
});
res.push({
label: _t('sqlObject.columnComment', { defaultMessage: 'Column comment' }),
switchValue: 'columnComment',
});
res.push({
label: _t('sqlObject.viewProcedureTriggerText', { defaultMessage: 'View/procedure/trigger text' }),
switchValue: 'sqlObjectText',
});
res.push({ label: _t('sqlObject.tableEngine', { defaultMessage: 'Table engine' }), switchValue: 'tableEngine' });
res.push({ label: _t('sqlObject.tablesWithRows', { defaultMessage: 'Only tables with rows' }), switchValue: 'tablesWithRows' });
res.push({
label: _t('sqlObject.tablesWithRows', { defaultMessage: 'Only tables with rows' }),
switchValue: 'tablesWithRows',
});
}
res.push({ label: _t('sqlObject.sortBy', { defaultMessage: 'Sort by:' }), isBold: true, disabled: true });
res.push({
label: _t('sqlObject.name', { defaultMessage: 'Name' }),
switchOption: 'sortBy',
switchOptionValue: 'name',
switchOptionIsDefault: true,
closeOnSwitchClick: true,
});
res.push({
label: _t('sqlObject.rowCount', { defaultMessage: 'Row count' }),
switchOption: 'sortBy',
switchOptionValue: 'rowCount',
closeOnSwitchClick: true,
});
res.push({
label: _t('sqlObject.sizeBytes', { defaultMessage: 'Size (bytes)' }),
switchOption: 'sortBy',
switchOptionValue: 'sizeBytes',
closeOnSwitchClick: true,
});
return res.map(item => ({
...item,
switchStore: databaseObjectAppObjectSearchSettings,
@@ -167,6 +232,15 @@
$focusedConnectionOrDatabase?.database != extractDbNameFromComposite(database)));
// $: console.log('STATUS', $status);
function getAppObjectGroup(data) {
if (data.objectTypeField == 'tables') {
if (data.pureName.match(databaseObjectAppObject.TABLE_BACKUP_REGEX)) {
return _t('dbObject.tableBackups', { defaultMessage: 'Table Backups' });
}
}
return getObjectTypeFieldLabel(data.objectTypeField, driver);
}
</script>
{#if $status && $status.name == 'error'}
@@ -176,7 +250,18 @@
<WidgetsInnerContainer hideContent={differentFocusedDb}>
<ErrorInfo message={$status.message} icon="img error" />
<InlineButton on:click={handleRefreshDatabase}>{_t('common.refresh', { defaultMessage: 'Refresh' })}</InlineButton>
<InlineButton on:click={handleFullRefreshDatabase}
>{_t('common.refresh', { defaultMessage: 'Refresh' })}</InlineButton
>
<DropDownButton
menu={createRefreshDatabaseMenu}
title={_t('sqlObjectList.refreshDatabase', { defaultMessage: 'Refresh database connection and object list' })}
square
narrow={false}
data-testid="SqlObjectList_refreshButton"
icon="icon dots-vertical"
/>
</WidgetsInnerContainer>
{:else if objectList.length == 0 && $status && $status.name != 'pending' && $status.name != 'checkStructure' && $status.name != 'loadStructure' && $objects}
<SchemaSelector
@@ -193,19 +278,25 @@
<WidgetsInnerContainer hideContent={differentFocusedDb}>
<ErrorInfo
message={_t('sqlObject.databaseEmpty', { defaultMessage: 'Database {database} is empty or structure is not loaded, press Refresh button to reload structure', values: { database } })}
message={_t('sqlObject.databaseEmpty', {
defaultMessage:
'Database {database} is empty or structure is not loaded, press Refresh button to reload structure',
values: { database },
})}
icon="img alert"
/>
<div class="m-1" />
<InlineButton on:click={handleRefreshDatabase}>{_t('common.refresh', { defaultMessage: 'Refresh' })}</InlineButton>
{#if driver?.databaseEngineTypes?.includes('sql')}
<div class="m-1" />
<InlineButton on:click={() => runCommand('new.table')}>{_t('database.newTable', { defaultMessage: 'New table' })}</InlineButton>
<InlineButton on:click={() => runCommand('new.table')}
>{_t('database.newTable', { defaultMessage: 'New table' })}</InlineButton
>
{/if}
{#if driver?.databaseEngineTypes?.includes('document')}
<div class="m-1" />
<InlineButton on:click={() => runCommand('new.collection')}
>{_t('sqlObject.newCollection', { defaultMessage: 'New collection/container'})}</InlineButton
>{_t('sqlObject.newCollection', { defaultMessage: 'New collection/container' })}</InlineButton
>
{/if}
</WidgetsInnerContainer>
@@ -231,14 +322,14 @@
{#if !filter}
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
{/if}
<InlineButton
on:click={handleRefreshDatabase}
title={_t('sqlObjectList.refreshDatabase', { defaultMessage: "Refresh database connection and object list" })}
<DropDownButton
menu={createRefreshDatabaseMenu}
title={_t('sqlObjectList.refreshDatabase', { defaultMessage: 'Refresh database connection and object list' })}
square
narrow={false}
data-testid="SqlObjectList_refreshButton"
>
<FontIcon icon="icon refresh" />
</InlineButton>
icon="icon dots-vertical"
/>
</SearchBoxWrapper>
<SchemaSelector
schemaList={_.isArray($schemaList) ? $schemaList : null}
@@ -260,7 +351,10 @@
data-testid="SqlObjectList_container"
>
{#if ($status && ($status.name == 'pending' || $status.name == 'checkStructure' || $status.name == 'loadStructure') && $objects) || !$objects}
<LoadingInfo message={$status?.feedback?.analysingMessage || _t('sqlObject.loadingStructure', { defaultMessage: 'Loading database structure' })} />
<LoadingInfo
message={$status?.feedback?.analysingMessage ||
_t('sqlObject.loadingStructure', { defaultMessage: 'Loading database structure' })}
/>
{:else}
<AppObjectListHandler
bind:this={domListHandler}
@@ -286,7 +380,7 @@
.filter(x => x.schemaName == null || ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
.map(x => ({ ...x, conid, database }))}
module={databaseObjectAppObject}
groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)}
groupFunc={getAppObjectGroup}
subItemsComponent={(data, { isExpandedBySearch }) =>
data.objectTypeField == 'procedures' || data.objectTypeField == 'functions'
? isExpandedBySearch
+16 -36
View File
@@ -35,16 +35,16 @@
getCurrentConfig().storageDatabase && {
icon: 'icon admin',
name: 'admin',
title: 'Administration',
title: _t('widgets.administration', { defaultMessage: 'Administration' }),
},
{
icon: 'icon database',
name: 'database',
title: 'Database connections',
title: _t('widgets.databaseConnections', { defaultMessage: 'Database connections' }),
},
getCurrentConfig().allowPrivateCloud && {
name: 'cloud-private',
title: 'DbGate Cloud',
title: _t('widgets.dbgateCloud', { defaultMessage: 'DbGate Cloud' }),
icon: 'icon cloud-private',
},
@@ -55,17 +55,17 @@
{
icon: 'icon file',
name: 'file',
title: 'Favorites & Saved files',
title: _t('widgets.favoritesAndSavedFiles', { defaultMessage: 'Favorites & Saved files' }),
},
{
icon: 'icon history',
name: 'history',
title: 'Query history & Closed tabs',
title: _t('widgets.queryHistoryAndClosedTabs', { defaultMessage: 'Query history & Closed tabs' }),
},
isProApp() && {
icon: 'icon archive',
name: 'archive',
title: 'Archive (saved tabular data)',
title: _t('widgets.archive', { defaultMessage: 'Archive (saved tabular data)' }),
},
// {
// icon: 'icon plugin',
@@ -75,17 +75,17 @@
{
icon: 'icon cell-data',
name: 'cell-data',
title: 'Selected cell data detail view',
title: _t('widgets.selectedCellDataDetailView', { defaultMessage: 'Selected cell data detail view' }),
},
{
name: 'cloud-public',
title: 'DbGate Cloud',
title: _t('widgets.dbgateCloud', { defaultMessage: 'DbGate Cloud' }),
icon: 'icon cloud-public',
},
{
icon: 'icon premium',
name: 'premium',
title: 'Premium promo',
title: _t('widgets.premiumPromo', { defaultMessage: 'Premium promo' }),
isPremiumPromo: true,
},
// {
@@ -113,32 +113,12 @@
//const handleChangeWidget= e => (selectedWidget.set(item.name))
function handleSettingsMenu() {
const rect = domSettings.getBoundingClientRect();
const left = rect.right;
const top = rect.bottom;
const items = [
hasPermission('settings/change') && { command: 'settings.show' },
{ command: 'theme.changeTheme' },
hasPermission('settings/change') && { command: 'settings.commands' },
hasPermission('widgets/plugins') && {
text: _t('widgets.managePlugins', { defaultMessage: 'Manage plugins' }),
onClick: () => {
$selectedWidget = 'plugins';
$visibleWidgetSideBar = true;
},
},
hasPermission('application-log') && {
text: _t('widgets.viewApplicationLogs', { defaultMessage: 'View application logs' }),
onClick: () => {
openNewTab({
title: 'Application log',
icon: 'img applog',
tabComponent: 'AppLogTab',
});
},
},
];
currentDropDownMenu.set({ left, top, items });
openNewTab({
title: 'Settings',
icon: 'icon settings',
tabComponent: 'SettingsTab',
props: {},
});
}
function handleCloudAccountMenu() {
@@ -213,7 +193,7 @@
class="wrapper"
on:click={() => showModal(NewObjectModal)}
data-testid="WidgetIconPanel_addButton"
title="Add New"
title={_t('widgets.addNew', { defaultMessage: 'Add New' })}
>
<FontIcon icon="icon add" />
</div>
+1 -1
View File
@@ -37,7 +37,7 @@
"dependencies": {
"dbgate-tools": "^6.0.0-alpha.1",
"lodash": "^4.17.21",
"dbgate-query-splitter": "^4.11.7"
"dbgate-query-splitter": "^4.11.9"
},
"optionalDependencies": {
"@duckdb/node-api": "^1.2.1-alpha.16"
+1 -1
View File
@@ -38,7 +38,7 @@
"wkx": "^0.5.0",
"pg-copy-streams": "^6.0.6",
"node-firebird": "^1.1.9",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-tools": "^6.0.0-alpha.1",
"lodash": "^4.17.21",
"pg": "^8.11.5"
+2 -1
View File
@@ -37,11 +37,12 @@
},
"dependencies": {
"bson": "^6.8.0",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-tools": "^6.0.0-alpha.1",
"is-promise": "^4.0.0",
"lodash": "^4.17.21",
"mongodb": "^6.3.0",
"mongodb-old": "npm:mongodb@6.16.0",
"@mongosh/browser-runtime-electron": "^3.16.4",
"@mongosh/service-provider-node-driver": "^3.10.2"
},
@@ -16,12 +16,16 @@ class Analyser extends DatabaseAnalyser {
collections
.filter((x) => x.type == 'collection')
.map((x) =>
this.dbhan
.getDatabase()
.collection(x.name)
.aggregate([{ $collStats: { count: {} } }])
.toArray()
.then((resp) => ({ name: x.name, count: resp[0].count }))
this.dbhan
.getDatabase()
.collection(x.name)
.aggregate([{ $collStats: { count: {}, storageStats: {} } }])
.toArray()
.then((resp) => ({
name: x.name,
count: resp[0].count,
size: resp[0].storageStats?.size
}))
)
);
} catch (e) {
@@ -29,11 +33,13 @@ class Analyser extends DatabaseAnalyser {
stats = {};
}
const res = this.mergeAnalyseResult({
collections: [
...collections.map((x, index) => ({
pureName: x.name,
tableRowCount: stats[index]?.count,
sizeBytes: stats[index]?.size,
uniqueKey: [{ columnName: '_id' }],
partitionKey: [{ columnName: '_id' }],
clusterKey: [{ columnName: '_id' }],
@@ -1,10 +1,11 @@
const _ = require('lodash');
const { EventEmitter } = require('events');
const stream = require('stream');
const driverBase = require('../frontend/driver');
const driverBases = require('../frontend/drivers');
const Analyser = require('./Analyser');
const isPromise = require('is-promise');
const { MongoClient, ObjectId, AbstractCursor, Long } = require('mongodb');
const mongodb = require('mongodb');
const { ObjectId } = require('mongodb');
const { EJSON } = require('bson');
const { serializeJsTypesForJsonStringify, deserializeJsTypesFromJsonParse, getLogger } = require('dbgate-tools');
const createBulkInsertStream = require('./createBulkInsertStream');
@@ -18,7 +19,8 @@ let isProApp;
const logger = getLogger('mongoDriver');
function serializeMongoData(row) {
function serializeMongoData(row, driverBase) {
const { Long } = driverBase.useLegacyDriver ? require('mongodb-old') : mongodb;
return EJSON.serialize(
serializeJsTypesForJsonStringify(row, (value) => {
if (value instanceof Long) {
@@ -33,10 +35,10 @@ function serializeMongoData(row) {
);
}
async function readCursor(cursor, options) {
async function readCursor(cursor, options, driverBase) {
options.recordset({ __isDynamicStructure: true });
await cursor.forEach((row) => {
options.row(serializeMongoData(row));
options.row(serializeMongoData(row, driverBase));
});
}
@@ -86,8 +88,8 @@ async function getScriptableDb(dbhan) {
// }
// }
/** @type {import('dbgate-types').EngineDriver<MongoClient, import('mongodb').Db>} */
const driver = {
/** @type {import('dbgate-types').EngineDriver<import('mongodb').MongoClient, import('mongodb').Db>} */
const drivers = driverBases.map((driverBase) => ({
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, useDatabaseUrl, databaseUrl, ssl, useSshTunnel }) {
@@ -120,6 +122,8 @@ const driver = {
options.tlsInsecure = !ssl.rejectUnauthorized;
}
const { MongoClient } = driverBase.useLegacyDriver ? require('mongodb-old') : mongodb;
const client = new MongoClient(mongoUrl, options);
await client.connect();
return {
@@ -314,8 +318,10 @@ const driver = {
return;
}
const { AbstractCursor } = driverBase.useLegacyDriver ? require('mongodb-old') : mongodb;
if (exprValue instanceof AbstractCursor) {
await readCursor(exprValue, options);
await readCursor(exprValue, options, driverBase);
} else if (isPromise(exprValue)) {
try {
const resValue = await exprValue;
@@ -427,7 +433,7 @@ const driver = {
const cursorStream = exprValue.stream();
cursorStream.on('data', (row) => {
pass.write(serializeMongoData(row));
pass.write(serializeMongoData(row, driverBase));
});
// propagate error
@@ -487,10 +493,12 @@ const driver = {
let cursor = await collection.aggregate(deserializeMongoData(convertToMongoAggregate(options.aggregate)));
const rows = await cursor.toArray();
return {
rows: rows.map(serializeMongoData).map((x) => ({
...x._id,
..._.omit(x, ['_id']),
})),
rows: rows
.map((row) => serializeMongoData(row, driverBase))
.map((x) => ({
...x._id,
..._.omit(x, ['_id']),
})),
};
} else {
// console.log('options.condition', JSON.stringify(options.condition, undefined, 2));
@@ -500,7 +508,7 @@ const driver = {
if (options.limit) cursor = cursor.limit(options.limit);
const rows = await cursor.toArray();
return {
rows: rows.map(serializeMongoData),
rows: rows.map((row) => serializeMongoData(row, driverBase)),
};
}
} catch (err) {
@@ -613,10 +621,12 @@ const driver = {
]);
const rows = await cursor.toArray();
return _.uniqBy(
rows.map(serializeMongoData).map(({ _id }) => {
if (_.isArray(_id) || _.isPlainObject(_id)) return { value: null };
return { value: _id };
}),
rows
.map((row) => serializeMongoData(row, driverBase))
.map(({ _id }) => {
if (_.isArray(_id) || _.isPlainObject(_id)) return { value: null };
return { value: _id };
}),
(x) => x.value
);
} catch (err) {
@@ -753,10 +763,10 @@ const driver = {
return result;
},
};
}));
driver.initialize = (dbgateEnv) => {
drivers.initialize = (dbgateEnv) => {
isProApp = dbgateEnv.isProApp;
};
module.exports = driver;
module.exports = drivers;
@@ -1,4 +1,4 @@
const driver = require('./driver');
const drivers = require('./drivers');
const {
formatProfilerEntry,
extractProfileTimestamp,
@@ -7,13 +7,13 @@ const {
module.exports = {
packageName: 'dbgate-plugin-mongo',
drivers: [driver],
drivers,
functions: {
formatProfilerEntry,
extractProfileTimestamp,
aggregateProfileChartEntry,
},
initialize(dbgateEnv) {
driver.initialize(dbgateEnv);
drivers.initialize(dbgateEnv);
},
};
@@ -16,9 +16,12 @@ function jsonStringifyWithObjectId(obj) {
return JSON.stringify(obj, mongoReplacer, 2)
.replace(/\{\s*\"\$oid\"\s*\:\s*\"([0-9a-f]+)\"\s*\}/g, (m, id) => `ObjectId("${id}")`)
.replace(/\{\s*\"\$bigint\"\s*\:\s*\"([0-9]+)\"\s*\}/g, (m, num) => `${num}n`)
.replace(/\{\s*"\$binary"\s*:\s*\{\s*"base64"\s*:\s*"([^"]+)"(?:\s*,\s*"subType"\s*:\s*"([0-9a-fA-F]{2})")?\s*\}\s*\}/g, (m, base64, subType) => {
return `BinData(${parseInt(subType || "00", 16)}, "${base64}")`;
});
.replace(
/\{\s*"\$binary"\s*:\s*\{\s*"base64"\s*:\s*"([^"]+)"(?:\s*,\s*"subType"\s*:\s*"([0-9a-fA-F]{2})")?\s*\}\s*\}/g,
(m, base64, subType) => {
return `BinData(${parseInt(subType || '00', 16)}, "${base64}")`;
}
);
}
/** @type {import('dbgate-types').SqlDialect} */
@@ -34,7 +37,7 @@ const dialect = {
};
/** @type {import('dbgate-types').EngineDriver} */
const driver = {
const mongoDriverBase = {
...driverBase,
dumperClass: Dumper,
databaseEngineTypes: ['document'],
@@ -193,4 +196,16 @@ const driver = {
},
};
module.exports = driver;
const mongoDriver = {
...mongoDriverBase,
};
const legacyMongoDriver = {
...mongoDriverBase,
engine: 'mongo-legacy@dbgate-plugin-mongo',
title: 'MongoDB 4 - Legacy',
premiumOnly: true,
useLegacyDriver: true,
};
module.exports = [mongoDriver, legacyMongoDriver];
@@ -1,9 +1,9 @@
import driver from './driver';
import drivers from './drivers';
import { formatProfilerEntry, extractProfileTimestamp, aggregateProfileChartEntry } from './profilerFunctions';
export default {
packageName: 'dbgate-plugin-mongo',
drivers: [driver],
drivers,
functions: {
formatProfilerEntry,
extractProfileTimestamp,
+1 -1
View File
@@ -38,7 +38,7 @@
"dependencies": {
"@azure/identity": "^4.6.0",
"async-lock": "^1.2.6",
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-tools": "^6.0.0-alpha.1",
"lodash": "^4.17.21",
"tedious": "^18.6.1"
+1 -1
View File
@@ -36,7 +36,7 @@
"webpack-cli": "^5.1.4"
},
"dependencies": {
"dbgate-query-splitter": "^4.11.7",
"dbgate-query-splitter": "^4.11.9",
"dbgate-tools": "^6.0.0-alpha.1",
"lodash": "^4.17.21",
"mysql2": "^3.11.3"
@@ -3,6 +3,7 @@ select
TABLE_NAME as pureName,
TABLE_TYPE as objectType,
TABLE_ROWS as tableRowCount,
DATA_LENGTH + INDEX_LENGTH as sizeBytes,
case when ENGINE='InnoDB' then CREATE_TIME else coalesce(UPDATE_TIME, CREATE_TIME) end as modifyDate
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#'
@@ -4,6 +4,7 @@ select
TABLE_ROWS as tableRowCount,
ENGINE as tableEngine,
TABLE_COMMENT as objectComment,
DATA_LENGTH + INDEX_LENGTH as sizeBytes,
case when ENGINE='InnoDB' then CREATE_TIME else coalesce(UPDATE_TIME, CREATE_TIME) end as modifyDate
from information_schema.tables
where TABLE_SCHEMA = '#DATABASE#' and (TABLE_TYPE='BASE TABLE' or TABLE_TYPE='SYSTEM VERSIONED') and TABLE_NAME =OBJECT_ID_CONDITION;

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