Compare commits
84 Commits
feature/xm
...
feature/li
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7db9ef481 | ||
|
|
fb709c9eb0 | ||
|
|
dfe4d2811a | ||
|
|
6e4d16749b | ||
|
|
75b2debf0a | ||
|
|
ba9e124527 | ||
|
|
d684ab0e5e | ||
|
|
2e8205f458 | ||
|
|
35855297eb | ||
|
|
d365ecce46 | ||
|
|
f4edc45507 | ||
|
|
d165d81df0 | ||
|
|
ba0eba7132 | ||
|
|
2b7f4281c2 | ||
|
|
0046575a4f | ||
|
|
58b88d66be | ||
|
|
57f1019e51 | ||
|
|
b40168182a | ||
|
|
0ece662d8c | ||
|
|
9265e52d68 | ||
|
|
3a04166747 | ||
|
|
75bf58359c | ||
|
|
e7f63e0460 | ||
|
|
1fdd1c6e88 | ||
|
|
391b9e91e9 | ||
|
|
1e67ca3794 | ||
|
|
ceebf6dbe1 | ||
|
|
8d4f9fd953 | ||
|
|
1c3032068e | ||
|
|
7b4b72166f | ||
|
|
707e5bb8b0 | ||
|
|
ad5d364c57 | ||
|
|
138fadf672 | ||
|
|
82eabc41fe | ||
|
|
3e6aab6b00 | ||
|
|
5396b3f1fb | ||
|
|
b1ba887922 | ||
|
|
93a1c593fe | ||
|
|
b7044248cb | ||
|
|
ea5e2f660b | ||
|
|
e9779a3d2f | ||
|
|
1c6ec0f8e3 | ||
|
|
84bd81e525 | ||
|
|
a84cbee9db | ||
|
|
97b16c8c0c | ||
|
|
0a6a35b022 | ||
|
|
6565b4101b | ||
|
|
fbbcc1172d | ||
|
|
53dc50c0dd | ||
|
|
7b56485c74 | ||
|
|
cfc9b809fc | ||
|
|
4015e2566e | ||
|
|
1d474a967c | ||
|
|
bffc34485a | ||
|
|
3bf22a8606 | ||
|
|
257ffa3cc4 | ||
|
|
0c104d5d29 | ||
|
|
2a59faec17 | ||
|
|
00534f7edd | ||
|
|
4006f03444 | ||
|
|
beca5c6e45 | ||
|
|
69f781d3de | ||
|
|
3f3160406f | ||
|
|
8067cff9bd | ||
|
|
7d776bb2af | ||
|
|
880bb0d7cb | ||
|
|
3aea01fb78 | ||
|
|
7025f4701d | ||
|
|
9d98b06132 | ||
|
|
47cb83c1ff | ||
|
|
6ff8847251 | ||
|
|
9d9367d127 | ||
|
|
ae7c1ae666 | ||
|
|
44fe3cb7bd | ||
|
|
bc783eb511 | ||
|
|
0491bd5364 | ||
|
|
cf47cccc97 | ||
|
|
c65ab35107 | ||
|
|
b8ea16f4d4 | ||
|
|
b40b5f0c1c | ||
|
|
b193e29fdb | ||
|
|
1b602c134f | ||
|
|
e462c6d412 | ||
|
|
d0c83f3c96 |
2
.github/workflows/build-app-pro-beta.yaml
vendored
2
.github/workflows/build-app-pro-beta.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-app-pro.yaml
vendored
2
.github/workflows/build-app-pro.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-cloud-pro.yaml
vendored
2
.github/workflows/build-cloud-pro.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-docker-pro.yaml
vendored
2
.github/workflows/build-docker-pro.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-npm-pro.yaml
vendored
2
.github/workflows/build-npm-pro.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/e2e-pro.yaml
vendored
2
.github/workflows/e2e-pro.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 4ae9129958c3f2f09cb75ec338b52fcb95f12358
|
||||
ref: 21048330597124a88fa1b8447e0bc18666eb69c5
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -32,4 +32,6 @@ packages/api/src/packagedPluginsContent.js
|
||||
.VSCodeCounter
|
||||
|
||||
packages/web/public/*.html
|
||||
e2e-tests/screenshots/*.png
|
||||
e2e-tests/screenshots/*.png
|
||||
my_guitar_shop.db
|
||||
.aider*
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@@ -8,6 +8,18 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
### 6.2.1
|
||||
- ADDED: Commit/rollback and autocommit in scripts #1039
|
||||
- FIXED: Doesn't import all the records from MongoDB #1044
|
||||
- ADDED: Show server name alongside database name in title of the tab group #1041
|
||||
- ADDED: Can't open Sqlite through web #956
|
||||
- FIXED: Crashed after text input at columns search #1049
|
||||
- FIXED: Incorrect autojoin for foreign keys with more columns #1051
|
||||
- FIXED: Scroll in XML cell view, XML view respect themes
|
||||
- REMOVED: armv7l build for Linux (because of problems with glibc compatibility)
|
||||
- CHANGED: Upgraded to node:22 for docker builds
|
||||
- CHANGED: Upgraded SQLite engine version (better-sqlite3@11.8.1)
|
||||
|
||||
### 6.2.0
|
||||
- ADDED: Query AI Assistant (Premium)
|
||||
- ADDED: Cassandra database support
|
||||
|
||||
43
common/translations-cli/addMissing.js
Normal file
43
common/translations-cli/addMissing.js
Normal file
@@ -0,0 +1,43 @@
|
||||
//@ts-check
|
||||
const { getDefaultTranslations, getLanguageTranslations } = require('./helpers');
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
*/
|
||||
function getMissingTranslations(language) {
|
||||
const source = getDefaultTranslations();
|
||||
/** @type {Record<string, string>} */
|
||||
let target;
|
||||
|
||||
try {
|
||||
target = getLanguageTranslations(language);
|
||||
} catch {
|
||||
console.log(`Language ${language} not found, creating a new one`);
|
||||
target = {};
|
||||
}
|
||||
|
||||
let added = 0;
|
||||
let removed = 0;
|
||||
|
||||
for (const key in source) {
|
||||
if (!target[key]) {
|
||||
target[key] = `*** ${source[key]}`;
|
||||
added++;
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in target) {
|
||||
if (!source[key]) {
|
||||
delete target[key];
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
const newLength = Object.keys(target).length;
|
||||
|
||||
return { result: target, stats: { added, removed, newLength } };
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getMissingTranslations,
|
||||
};
|
||||
16
common/translations-cli/constants.js
Normal file
16
common/translations-cli/constants.js
Normal file
@@ -0,0 +1,16 @@
|
||||
// @ts-check
|
||||
//
|
||||
const defaultLanguage = 'en';
|
||||
|
||||
/** @typedef {{ extensions: string[], directories: string[] }} ExtractConfig
|
||||
|
||||
/** @type {ExtractConfig} */
|
||||
const defaultExtractConfig = {
|
||||
extensions: ['.js', '.ts', '.svelte'],
|
||||
directories: ['app', 'packages/web'],
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
defaultLanguage,
|
||||
defaultExtractConfig,
|
||||
};
|
||||
84
common/translations-cli/extract.js
Normal file
84
common/translations-cli/extract.js
Normal file
@@ -0,0 +1,84 @@
|
||||
//@ts-check
|
||||
const fs = require('fs');
|
||||
const { promisify } = require('util');
|
||||
|
||||
const { getFiles } = require('./helpers');
|
||||
|
||||
const readFilePromise = promisify(fs.readFile);
|
||||
|
||||
const translationRegex = /_t\(\s*['"]([^'"]+)['"]\s*,\s*\{\s*defaultMessage\s*:\s*['"]([^'"]+)['"]\s*\}/g;
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*
|
||||
* @returns {Promise<Record<string, string>>}
|
||||
*/
|
||||
async function extractTranslationsFromFile(file) {
|
||||
/** @type {Record<string, string>} */
|
||||
const translations = {};
|
||||
const content = await readFilePromise(file, 'utf-8');
|
||||
let match;
|
||||
|
||||
while ((match = translationRegex.exec(content)) !== null) {
|
||||
const [_, key, defaultText] = match;
|
||||
translations[key] = defaultText;
|
||||
}
|
||||
|
||||
return translations;
|
||||
}
|
||||
|
||||
/** @typedef {{ ignoreDuplicates?: boolean }} ExtractOptions */
|
||||
|
||||
/**
|
||||
* @param {string[]} directories
|
||||
* @param {string[]} extensions
|
||||
* @param {ExtractOptions} options
|
||||
*
|
||||
* @returns {Promise<Record<string, string>>}
|
||||
*/
|
||||
async function extractAllTranslations(directories, extensions, options = {}) {
|
||||
const { ignoreDuplicates } = options;
|
||||
|
||||
try {
|
||||
/** @type {Record<string, string>} */
|
||||
const allTranslations = {};
|
||||
/** @type {Record<string, string[]>} */
|
||||
const translationKeyToFiles = {};
|
||||
|
||||
for (const dir of directories) {
|
||||
const files = await getFiles(dir, extensions);
|
||||
|
||||
for (const file of files) {
|
||||
const fileTranslations = await extractTranslationsFromFile(file);
|
||||
|
||||
for (const key in fileTranslations) {
|
||||
if (!translationKeyToFiles[key]) {
|
||||
translationKeyToFiles[key] = [];
|
||||
}
|
||||
|
||||
translationKeyToFiles[key].push(file);
|
||||
|
||||
if (!ignoreDuplicates && allTranslations[key] && allTranslations[key] !== fileTranslations[key]) {
|
||||
console.error(
|
||||
`Different translations for the same key [${key}] found. ${file}: ${
|
||||
fileTranslations[key]
|
||||
}. Previous value: ${allTranslations[key]} was found in ${translationKeyToFiles[key].join(', ')}`
|
||||
);
|
||||
throw new Error(`Duplicate translation key found: ${key}`);
|
||||
}
|
||||
|
||||
allTranslations[key] = fileTranslations[key];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allTranslations;
|
||||
} catch (error) {
|
||||
console.error('Error extracting translations:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
module.exports = {
|
||||
extractTranslationsFromFile,
|
||||
extractAllTranslations,
|
||||
};
|
||||
198
common/translations-cli/helpers.js
Normal file
198
common/translations-cli/helpers.js
Normal file
@@ -0,0 +1,198 @@
|
||||
//@ts-check
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { defaultLanguage } = require('./constants');
|
||||
const sortJsonKeysAlphabetically = require('./sortJsonKeysAlphabetically');
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @param {string[]} extensions
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasValidExtension(file, extensions) {
|
||||
return extensions.includes(path.extname(file).toLowerCase());
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @param {string[]} extensions
|
||||
*
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
async function getFiles(dir, extensions) {
|
||||
const files = await fs.promises.readdir(dir);
|
||||
const allFiles = await Promise.all(
|
||||
files.map(async file => {
|
||||
const filePath = path.join(dir, file);
|
||||
const stats = await fs.promises.stat(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
return getFiles(filePath, extensions);
|
||||
} else if (stats.isFile() && hasValidExtension(file, extensions)) {
|
||||
const slashPath = filePath.replace(/\\/g, '/');
|
||||
if (slashPath.includes('/node_modules/') || slashPath.includes('/build/') || slashPath.includes('/dist/')) {
|
||||
return null;
|
||||
}
|
||||
return filePath;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
);
|
||||
|
||||
const validFiles = /** @type {string[]} */ (allFiles.flat().filter(file => file !== null));
|
||||
|
||||
return validFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string | string[]} value
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatDefaultValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
const scriptDir = getScriptDir();
|
||||
/** @param {string} file
|
||||
*
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveFile(file) {
|
||||
if (path.isAbsolute(file)) {
|
||||
return file;
|
||||
}
|
||||
|
||||
return path.resolve(scriptDir, '..', '..', file);
|
||||
}
|
||||
|
||||
/** @param {string[]} dirs
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function resolveDirs(dirs) {
|
||||
return dirs.map(resolveFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} extensions
|
||||
*
|
||||
* @returns {string[]}
|
||||
*/
|
||||
function resolveExtensions(extensions) {
|
||||
return extensions.map(ext => (ext.startsWith('.') ? ext : `.${ext}`));
|
||||
}
|
||||
|
||||
function getScriptDir() {
|
||||
if (require.main?.filename) {
|
||||
return path.dirname(require.main.filename);
|
||||
}
|
||||
|
||||
if ('pkg' in process && process.pkg) {
|
||||
return path.dirname(process.execPath);
|
||||
}
|
||||
|
||||
return __dirname;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
*/
|
||||
function ensureFileDirExists(file) {
|
||||
const dir = path.dirname(file);
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, string>} existingTranslations - Previously extracted translations
|
||||
* @param {Record<string, string>} newTranslations - Newly extracted translations
|
||||
* @returns {{ added: string[], removed: string[], updated: string[] }} Translation changes
|
||||
*/
|
||||
const getTranslationChanges = (existingTranslations, newTranslations) => {
|
||||
const existingKeys = new Set(Object.keys(existingTranslations || {}));
|
||||
const newKeys = new Set(Object.keys(newTranslations));
|
||||
|
||||
const added = [...newKeys].filter(key => !existingKeys.has(key));
|
||||
const removed = [...existingKeys].filter(key => !newKeys.has(key));
|
||||
const updated = [...newKeys].filter(
|
||||
key => existingKeys.has(key) && existingTranslations[key] !== newTranslations[key]
|
||||
);
|
||||
|
||||
return { added, removed, updated };
|
||||
};
|
||||
|
||||
function getDefaultTranslations() {
|
||||
return getLanguageTranslations(defaultLanguage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
*
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
function getLanguageTranslations(language) {
|
||||
const file = resolveFile(`translations/${language}.json`);
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
* @param {Record<string, string>} translations
|
||||
*/
|
||||
function setLanguageTranslations(language, translations) {
|
||||
const file = resolveFile(`translations/${language}.json`);
|
||||
const sorted = sortJsonKeysAlphabetically(translations);
|
||||
|
||||
fs.writeFileSync(file, JSON.stringify(sorted, null, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
* @param {Record<string, string>} newTranslations
|
||||
*/
|
||||
function updateLanguageTranslations(language, newTranslations) {
|
||||
const translations = getLanguageTranslations(language);
|
||||
const updatedTranslations = { ...translations, ...newTranslations };
|
||||
const sorted = sortJsonKeysAlphabetically(updatedTranslations);
|
||||
|
||||
setLanguageTranslations(language, sorted);
|
||||
}
|
||||
|
||||
function getAllLanguages() {
|
||||
const dir = resolveFile('translations');
|
||||
|
||||
const files = fs.readdirSync(dir);
|
||||
const languages = files.filter(file => file.endsWith('.json')).map(file => file.replace('.json', ''));
|
||||
|
||||
return languages;
|
||||
}
|
||||
|
||||
function getAllNonDefaultLanguages() {
|
||||
return getAllLanguages().filter(language => language !== defaultLanguage);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
hasValidExtension,
|
||||
getFiles,
|
||||
formatDefaultValue,
|
||||
resolveFile,
|
||||
resolveDirs,
|
||||
resolveExtensions,
|
||||
ensureFileDirExists,
|
||||
getTranslationChanges,
|
||||
getDefaultTranslations,
|
||||
getLanguageTranslations,
|
||||
setLanguageTranslations,
|
||||
updateLanguageTranslations,
|
||||
getAllLanguages,
|
||||
getAllNonDefaultLanguages,
|
||||
};
|
||||
3
common/translations-cli/index.js
Normal file
3
common/translations-cli/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
const { program } = require('./program');
|
||||
|
||||
program.parse();
|
||||
163
common/translations-cli/program.js
Normal file
163
common/translations-cli/program.js
Normal file
@@ -0,0 +1,163 @@
|
||||
//@ts-check
|
||||
const fs = require('fs');
|
||||
const { program } = require('commander');
|
||||
const {
|
||||
resolveDirs,
|
||||
resolveExtensions,
|
||||
getTranslationChanges,
|
||||
setLanguageTranslations,
|
||||
getAllNonDefaultLanguages,
|
||||
updateLanguageTranslations,
|
||||
getDefaultTranslations,
|
||||
} = require('./helpers');
|
||||
const { extractAllTranslations } = require('./extract');
|
||||
const { getMissingTranslations } = require('./addMissing');
|
||||
const { defaultLanguage, defaultExtractConfig } = require('./constants');
|
||||
const { removeUnusedAllTranslations, removeUnusedForSignelLanguage } = require('./removeUnused');
|
||||
|
||||
/**
|
||||
* @typedef {import('./constants').ExtractConfig & { verbose?: boolean, ignoreUnused?: boolean }} ExtractOptions
|
||||
*/
|
||||
|
||||
program.name('dbgate-translations-cli').description('CLI tool for managing translation').version('1.0.0');
|
||||
|
||||
program
|
||||
.command('extract')
|
||||
.description('Extract translation keys from source files')
|
||||
.option('-d, --directories <directories...>', 'directories to search', defaultExtractConfig.directories)
|
||||
.option('-e, --extensions <extensions...>', 'file extensions to process', defaultExtractConfig.extensions)
|
||||
.option('-r, --ignoreUnused', 'Ignore unused keys in the output file')
|
||||
.option('-v, --verbose', 'verbose mode')
|
||||
.action(async (/** @type {ExtractOptions} */ options) => {
|
||||
try {
|
||||
const { directories, extensions, verbose, ignoreUnused } = options;
|
||||
|
||||
const resolvedRirectories = resolveDirs(directories);
|
||||
const resolvedExtensions = resolveExtensions(extensions);
|
||||
|
||||
const extractedTranslations = await extractAllTranslations(resolvedRirectories, resolvedExtensions);
|
||||
const defaultTranslations = getDefaultTranslations();
|
||||
|
||||
const { added, removed, updated } = getTranslationChanges(defaultTranslations, extractedTranslations);
|
||||
|
||||
console.log('\nTranslation changes:');
|
||||
console.log(`- Added: ${added.length} keys`);
|
||||
console.log(`- ${ignoreUnused ? 'Unused' : 'Removed'}: ${removed.length} keys`);
|
||||
console.log(`- Updated: ${updated.length} keys`);
|
||||
console.log(`- Total: ${Object.keys(extractedTranslations).length} keys`);
|
||||
|
||||
if (verbose) {
|
||||
if (added.length > 0) {
|
||||
console.log('\nNew keys:');
|
||||
added.forEach(key => console.log(` + ${key}`));
|
||||
}
|
||||
|
||||
if (removed.length > 0) {
|
||||
console.log('\nRemoved keys:');
|
||||
removed.forEach(key => console.log(` - ${key}`));
|
||||
}
|
||||
|
||||
if (updated.length > 0) {
|
||||
console.log('\nUpdated keys:');
|
||||
updated.forEach(key => {
|
||||
console.log(` ~ ${key}`);
|
||||
console.log(` Old: ${defaultLanguage[key]}`);
|
||||
console.log(` New: ${extractedTranslations[key]}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (ignoreUnused) {
|
||||
console.log('New translations were saved. Unused keys are kept.\n');
|
||||
updateLanguageTranslations(defaultLanguage, extractedTranslations);
|
||||
|
||||
if (verbose) {
|
||||
console.log('\nUnused keys:');
|
||||
for (const key of removed) {
|
||||
console.log(`${key}: "${defaultTranslations[key]}"`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.log('Unused keys were removed.\n');
|
||||
setLanguageTranslations(defaultLanguage, extractedTranslations);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Error during extraction:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
const ALL_LANGUAGES = 'all';
|
||||
|
||||
/**
|
||||
* @param {string} target
|
||||
*/
|
||||
function addMissingTranslations(target) {
|
||||
console.log(`Adding missing keys for language: ${target}`);
|
||||
const { result, stats } = getMissingTranslations(target);
|
||||
console.log(`Added: ${stats.added}, Removed: ${stats.removed}, Total: ${stats.newLength}`);
|
||||
setLanguageTranslations(target, result);
|
||||
console.log(`New translations for ${target} were saved.`);
|
||||
}
|
||||
|
||||
program
|
||||
.command('add-missing')
|
||||
.description('Add missing keys for a langauge to the translation file')
|
||||
.option('-t, --target <target>', 'language to add missing translations to', ALL_LANGUAGES)
|
||||
.action(options => {
|
||||
try {
|
||||
const { target } = options;
|
||||
const languages = getAllNonDefaultLanguages();
|
||||
|
||||
if (target === ALL_LANGUAGES) {
|
||||
console.log('Adding missing keys for all languages\n');
|
||||
for (const language of languages) {
|
||||
addMissingTranslations(language);
|
||||
console.log();
|
||||
}
|
||||
} else {
|
||||
addMissingTranslations(target);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Error during add-missing:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('remove-unused')
|
||||
.description('Remove unused keys from the translation files')
|
||||
.option('-t, --target <target>', 'language to add missing translations to', ALL_LANGUAGES)
|
||||
.action(async options => {
|
||||
try {
|
||||
const { target } = options;
|
||||
if (target === ALL_LANGUAGES) {
|
||||
console.log('Removing unused keys from all languages\n');
|
||||
await removeUnusedAllTranslations();
|
||||
} else {
|
||||
await removeUnusedForSignelLanguage(target);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Error during add-missing:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
program
|
||||
.command('check')
|
||||
.description('Check if there are multiple default values for the same key')
|
||||
.action(async () => {
|
||||
try {
|
||||
await extractAllTranslations(defaultExtractConfig.directories, defaultExtractConfig.extensions);
|
||||
console.log('No problems found while extracting translations.');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error('Error during check:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = { program };
|
||||
46
common/translations-cli/removeUnused.js
Normal file
46
common/translations-cli/removeUnused.js
Normal file
@@ -0,0 +1,46 @@
|
||||
// @ts-check
|
||||
const { defaultExtractConfig } = require('./constants');
|
||||
const { extractAllTranslations } = require('./extract');
|
||||
const { getLanguageTranslations, getAllLanguages, setLanguageTranslations } = require('./helpers');
|
||||
|
||||
const { directories, extensions } = defaultExtractConfig;
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
* @param {Record<string, string>} source
|
||||
*/
|
||||
function getUsedTranslations(language, source) {
|
||||
const languageTranslations = getLanguageTranslations(language);
|
||||
|
||||
for (const key in languageTranslations) {
|
||||
if (!(key in source)) {
|
||||
delete languageTranslations[key];
|
||||
}
|
||||
}
|
||||
|
||||
return languageTranslations;
|
||||
}
|
||||
|
||||
async function removeUnusedAllTranslations() {
|
||||
const source = await extractAllTranslations(directories, extensions);
|
||||
const languages = getAllLanguages();
|
||||
|
||||
for (const language of languages) {
|
||||
const newTranslations = getUsedTranslations(language, source);
|
||||
setLanguageTranslations(language, newTranslations);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} language
|
||||
*/
|
||||
async function removeUnusedForSignelLanguage(language) {
|
||||
const source = await extractAllTranslations(directories, extensions);
|
||||
const newTranslations = getUsedTranslations(language, source);
|
||||
setLanguageTranslations(language, newTranslations);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
removeUnusedAllTranslations,
|
||||
removeUnusedForSignelLanguage,
|
||||
};
|
||||
24
common/translations-cli/sortJsonKeysAlphabetically.js
Normal file
24
common/translations-cli/sortJsonKeysAlphabetically.js
Normal file
@@ -0,0 +1,24 @@
|
||||
// @ts-check
|
||||
|
||||
/**
|
||||
* @param {object|string} json
|
||||
* @returns {object}
|
||||
*/
|
||||
function sortJsonKeysAlphabetically(json) {
|
||||
const obj = typeof json === 'string' ? JSON.parse(json) : json;
|
||||
|
||||
if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
const sortedObj = Object.keys(obj)
|
||||
.sort()
|
||||
.reduce((result, key) => {
|
||||
result[key] = obj[key];
|
||||
return result;
|
||||
}, {});
|
||||
|
||||
return sortedObj;
|
||||
}
|
||||
|
||||
module.exports = sortJsonKeysAlphabetically;
|
||||
@@ -4,4 +4,5 @@ module.exports = {
|
||||
mssql: true,
|
||||
oracle: true,
|
||||
sqlite: true,
|
||||
mongo: true
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ function clearTestingData() {
|
||||
if (fs.existsSync(path.join(baseDir, 'archive-e2etests'))) {
|
||||
fs.rmdirSync(path.join(baseDir, 'archive-e2etests'), { recursive: true });
|
||||
}
|
||||
if (fs.existsSync(path.join(__dirname, '../my_guitar_shop.db'))) {
|
||||
fs.unlinkSync(path.join(__dirname, '../my_guitar_shop.db'));
|
||||
if (fs.existsSync(path.join(__dirname, 'tmpdata', 'my_guitar_shop.db'))) {
|
||||
fs.unlinkSync(path.join(__dirname, 'tmpdata', 'my_guitar_shop.db'));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -403,48 +403,4 @@ describe('Data browser data', () => {
|
||||
cy.contains('Novak');
|
||||
cy.contains('Rows: 8');
|
||||
});
|
||||
|
||||
// it('Import', () => {
|
||||
// TBC after Import FIX
|
||||
// cy.contains('MySql-connection').click();
|
||||
// cy.contains('MyChinook').click();
|
||||
// cy.contains('Customer').rightclickclick();
|
||||
// cy.contains('Import').click();
|
||||
// cy.get('input[type=file]').selectFile('cypress/fixtures/Customer_add.csv');
|
||||
// cy.get('table tbody tr').eq(1).within(() => {
|
||||
// cy.get('select').select('Append data');
|
||||
// });
|
||||
|
||||
// });
|
||||
|
||||
it('Backup table', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('Customer').rightclick();
|
||||
cy.contains('backup').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains ('_Customer').should('be.visible');
|
||||
});
|
||||
|
||||
it('Truncate table', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('_Customer').click();
|
||||
cy.contains('Leonie').click();
|
||||
cy.contains('_Customer').rightclick();
|
||||
cy.contains('Truncate table').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('Leonie').click();
|
||||
cy.testid ('TableDataTab_refreshGrid').click();
|
||||
cy.contains('No rows loaded')
|
||||
});
|
||||
|
||||
it('Drop table', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('_Customer').rightclick();
|
||||
cy.contains('Drop table').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('_Customer').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,37 +28,37 @@ beforeEach(() => {
|
||||
cy.viewport(1250, 900);
|
||||
});
|
||||
|
||||
function multiTest(testName, testDefinition) {
|
||||
function multiTest(testProps, testDefinition) {
|
||||
if (localconfig.mysql) {
|
||||
it(testName + ' MySQL', () => testDefinition('MySql-connection', 'mysql@dbgate-plugin-mysql'));
|
||||
it('MySQL', () => testDefinition('MySql-connection', 'my_guitar_shop', 'mysql@dbgate-plugin-mysql'));
|
||||
}
|
||||
if (localconfig.postgres) {
|
||||
it(testName + ' Postgres', () => testDefinition('Postgres-connection', 'postgres@dbgate-plugin-postgres'));
|
||||
it('Postgres', () => testDefinition('Postgres-connection', 'my_guitar_shop', 'postgres@dbgate-plugin-postgres'));
|
||||
}
|
||||
if (localconfig.mssql) {
|
||||
it(testName + ' Mssql', () => testDefinition('Mssql-connection', 'mssql@dbgate-plugin-mssql'));
|
||||
it('Mssql', () => testDefinition('Mssql-connection', 'my_guitar_shop', 'mssql@dbgate-plugin-mssql'));
|
||||
}
|
||||
if (localconfig.oracle) {
|
||||
it(testName + ' Oracle', () =>
|
||||
testDefinition('Oracle-connection', 'oracle@dbgate-plugin-oracle', {
|
||||
databaseName: 'C##MY_GUITAR_SHOP',
|
||||
it('Oracle', () =>
|
||||
testDefinition('Oracle-connection', 'C##MY_GUITAR_SHOP', 'oracle@dbgate-plugin-oracle', {
|
||||
implicitTransactions: true,
|
||||
})
|
||||
);
|
||||
}));
|
||||
}
|
||||
if (localconfig.sqlite) {
|
||||
it(testName + ' Sqlite', () => testDefinition('Sqlite-connection', 'sqlite@dbgate-plugin-sqlite'));
|
||||
it('Sqlite', () => testDefinition('Sqlite-connection', null, 'sqlite@dbgate-plugin-sqlite'));
|
||||
}
|
||||
if (localconfig.mongo && !testProps.skipMongo) {
|
||||
it('MongoDB', () => testDefinition('Mongo-connection', 'my_guitar_shop', 'mongo@dbgate-plugin-mongo'));
|
||||
}
|
||||
}
|
||||
|
||||
describe('Mutli-sql tests', () => {
|
||||
multiTest('Transactions', (connectionName, engine, options = {}) => {
|
||||
describe('Transactions', () => {
|
||||
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
|
||||
const driver = requireEngineDriver(engine);
|
||||
const databaseName = options.databaseName ?? 'my_guitar_shop';
|
||||
const implicitTransactions = options.implicitTransactions ?? false;
|
||||
|
||||
cy.contains(connectionName).click();
|
||||
cy.contains(databaseName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.testid('TabsPanel_buttonNewQuery').click();
|
||||
cy.wait(1000);
|
||||
cy.get('body').type(
|
||||
@@ -99,3 +99,104 @@ describe('Mutli-sql tests', () => {
|
||||
cy.contains('Rows: 5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Backup table', () => {
|
||||
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.contains('customers').rightclick();
|
||||
cy.contains('Create table backup').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('_customers').click();
|
||||
cy.contains('Rows: 8').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Truncate table', () => {
|
||||
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.contains('order_items').rightclick();
|
||||
cy.contains('Truncate table').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('order_items').click();
|
||||
cy.contains('No rows loaded').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Drop table', () => {
|
||||
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.contains('order_items').rightclick();
|
||||
cy.contains('Drop table').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('order_items').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import CSV', () => {
|
||||
multiTest({}, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.testid('ConnectionList_container')
|
||||
.contains(databaseName ?? connectionName)
|
||||
.rightclick();
|
||||
cy.contains('Import').click();
|
||||
|
||||
cy.get('input[type=file]').selectFile('cypress/fixtures/customers-20.csv', { force: true });
|
||||
cy.contains('customers-20');
|
||||
cy.testid('ImportExportTab_preview_content').contains('50ddd99fAdF48B3').should('be.visible');
|
||||
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.contains('20 rows written').should('be.visible');
|
||||
|
||||
cy.testid('SqlObjectList_refreshButton').click();
|
||||
cy.testid('SqlObjectList_container').contains('customers-20').click();
|
||||
cy.contains('Rows: 20').should('be.visible');
|
||||
|
||||
// cy.get('table tbody tr')
|
||||
// .eq(1)
|
||||
// .within(() => {
|
||||
// cy.get('select').select('Append data');
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import CSV - source error', () => {
|
||||
multiTest({}, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.testid('ConnectionList_container')
|
||||
.contains(databaseName ?? connectionName)
|
||||
.rightclick();
|
||||
cy.contains('Import').click();
|
||||
|
||||
cy.get('input[type=file]').selectFile('cypress/fixtures/customers-20-err.csv', { force: true });
|
||||
cy.contains('customers-20-err');
|
||||
cy.testid('ImportExportTab_preview_content').contains('Invalid Closing Quote').should('be.visible');
|
||||
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err').click();
|
||||
|
||||
cy.testid('ErrorMessageModal_message').contains('Invalid Closing Quote').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Import CSV - target error', () => {
|
||||
multiTest({}, (connectionName, databaseName, engine, options = {}) => {
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.testid('ConnectionList_container')
|
||||
.contains(databaseName ?? connectionName)
|
||||
.rightclick();
|
||||
cy.contains('Import').click();
|
||||
|
||||
cy.get('input[type=file]').selectFile('cypress/fixtures/customers-20.csv', { force: true });
|
||||
cy.contains('customers-20');
|
||||
cy.testid('ImportExportConfigurator_targetName_customers-20').clear().type('system."]`');
|
||||
cy.testid('ImportExportTab_executeButton').click();
|
||||
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20').click();
|
||||
cy.testid('ErrorMessageModal_message').should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
21
e2e-tests/cypress/fixtures/customers-20-err.csv
Normal file
21
e2e-tests/cypress/fixtures/customers-20-err.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
Index,Customer Id,First Name,Last Name,Company,City,Country,Phone 1,Phone 2,Email,Subscription Date,Website
|
||||
1,50ddd99fAdF48B3,Jessica,Navarro,"Tran, Odom and Graham",Port Terranceview,Jersey,(883)287-7947,479-044-3187,wmcintyre@myers.net,2023-03-11,http://pennington.com/
|
||||
"2,BD1AB97979DDcDe,Preston,Andrews,"Townsend, Lawrence and Davenport",Harringtonmouth,Croatia,188-270-5676x63873,001-428-148-4270,bradley63@jacobs.net,2023-11-15,http://burke.com/
|
||||
3,0FDDD9aCa501acE,Melissa,Mckay,Rivas-Cooke,Oliviabury,Cook Islands,8257631531,001-650-689-5600x927,ugalloway@maxwell-mcclain.com,2025-01-25,https://patrick.com/
|
||||
4,A7dA284B7c4AfaD,Frank,Livingston,"Lambert, Garner and Mathews",Mallorytown,Faroe Islands,766.182.6201x689,+1-476-912-2027x4370,tracie00@webster.biz,2021-03-27,https://www.hart.com/
|
||||
5,38bb1624e4f8211,Ross,Mendoza,Cook and Sons,New Perry,Bolivia,001-440-468-2236,165.562.4328x9274,austincarmen@abbott.com,2024-12-12,http://harmon.com/
|
||||
6,55c9Fda2BABFE1a,Jane,Tate,Knapp PLC,West Kathyshire,Morocco,001-606-447-0584x07975,(003)872-1307x285,carlosgomez@mcpherson-ramos.biz,2021-09-13,http://www.yu.com/
|
||||
7,A15eccAc67cCc5b,Tanya,Chung,Frost Inc,South Julieburgh,United States of America,778-360-8301x268,+1-034-169-4237,onewton@crosby.com,2024-10-01,http://www.gill-velez.biz/
|
||||
8,aFdab04acACaf90,Jackson,Moyer,Bowen Ltd,Port Triciabury,Bahrain,001-187-882-2891x169,154.903.2525,gabriela16@mejia.org,2023-06-20,http://www.david.biz/
|
||||
9,A2EaeAd3c6529B9,Kelly,Douglas,Phillips PLC,Weissberg,Qatar,767.177.3199x5576,5156730935,wigginsmercedes@tanner.com,2021-06-30,https://gonzalez.net/
|
||||
10,188175A5641d080,Mariah,Riddle,"Mata, Fuller and Good",Choiton,Namibia,092-993-3559x342,190.937.3937,eileen32@greer.biz,2021-04-17,http://www.bowen.info/
|
||||
11,B2BA817C7bC09f0,Kristine,Pennington,"Koch, Diaz and Valdez",Port Rachael,Suriname,877.152.6488x921,790-804-9979x3290,tonya00@flynn.com,2024-03-28,https://www.rocha.net/
|
||||
12,fFAD70B5Febc43a,Robert,Adams,Bender-Wolfe,Billfort,Tanzania,219-696-1912x93626,518-969-4058,jeffersoncolton@moss-ashley.com,2022-07-31,https://www.alvarez.com/
|
||||
13,9a44524E1261Ed6,Franklin,Costa,Werner Group,North Bradley,Cook Islands,(895)448-4681x1999,648-345-3083x88242,marksmith@novak.com,2024-12-03,http://thornton.com/
|
||||
14,0C360EfE17D06cc,Dan,Ballard,Sampson-Huff,Michaelchester,Sao Tome and Principe,+1-803-469-5237x2580,(729)574-6101x0605,molly54@acevedo.info,2022-10-24,https://www.sloan-gonzales.biz/
|
||||
15,fb5cE8cb1eF2954,Clifford,Moyer,Fry-Whitehead,Lake Gary,Japan,(504)263-0450,247.514.1614x551,rhonda34@orr.com,2020-03-08,https://norton.com/
|
||||
16,3e53DcaD1cB0054,Tonya,Durham,"Lowe, West and Reynolds",South Sylviaton,Brunei Darussalam,(613)893-8183,(461)399-6120,kiara96@meadows.biz,2022-08-04,https://www.reeves.com/
|
||||
17,83fDDE5812EbEde,Dale,Bishop,Shaw-Ray,Lake Miranda,Congo,(082)202-9241,+1-413-610-1854x79378,bhoward@hodges.biz,2023-05-08,http://www.escobar.com/
|
||||
18,8dB7ACe2C5758cE,Priscilla,Mills,Pope PLC,North Cliffordshire,South Georgia and the South Sandwich Islands,+1-465-802-4573x30191,044-393-6049x598,nathaniel28@hess.com,2020-09-08,https://www.watts.org/
|
||||
19,c1092ebDaF2ceED,Alice,Bond,Chan-Liu,West Wesley,Lithuania,+1-333-285-8515,(715)759-3158x77103,jose92@peters.com,2024-05-30,http://www.marquez.com/
|
||||
20,4d0c95579D095aF,Cole,Compton,Calderon PLC,Robertaville,Saudi Arabia,029.862.3387x470,001-319-474-2394,sspence@dorsey.com,2020-08-02,http://hamilton.net/
|
||||
|
Can't render this file because it contains an unexpected character in line 3 and column 36.
|
21
e2e-tests/cypress/fixtures/customers-20.csv
Normal file
21
e2e-tests/cypress/fixtures/customers-20.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
Index,Customer Id,First Name,Last Name,Company,City,Country,Phone 1,Phone 2,Email,Subscription Date,Website
|
||||
1,50ddd99fAdF48B3,Jessica,Navarro,"Tran, Odom and Graham",Port Terranceview,Jersey,(883)287-7947,479-044-3187,wmcintyre@myers.net,2023-03-11,http://pennington.com/
|
||||
2,BD1AB97979DDcDe,Preston,Andrews,"Townsend, Lawrence and Davenport",Harringtonmouth,Croatia,188-270-5676x63873,001-428-148-4270,bradley63@jacobs.net,2023-11-15,http://burke.com/
|
||||
3,0FDDD9aCa501acE,Melissa,Mckay,Rivas-Cooke,Oliviabury,Cook Islands,8257631531,001-650-689-5600x927,ugalloway@maxwell-mcclain.com,2025-01-25,https://patrick.com/
|
||||
4,A7dA284B7c4AfaD,Frank,Livingston,"Lambert, Garner and Mathews",Mallorytown,Faroe Islands,766.182.6201x689,+1-476-912-2027x4370,tracie00@webster.biz,2021-03-27,https://www.hart.com/
|
||||
5,38bb1624e4f8211,Ross,Mendoza,Cook and Sons,New Perry,Bolivia,001-440-468-2236,165.562.4328x9274,austincarmen@abbott.com,2024-12-12,http://harmon.com/
|
||||
6,55c9Fda2BABFE1a,Jane,Tate,Knapp PLC,West Kathyshire,Morocco,001-606-447-0584x07975,(003)872-1307x285,carlosgomez@mcpherson-ramos.biz,2021-09-13,http://www.yu.com/
|
||||
7,A15eccAc67cCc5b,Tanya,Chung,Frost Inc,South Julieburgh,United States of America,778-360-8301x268,+1-034-169-4237,onewton@crosby.com,2024-10-01,http://www.gill-velez.biz/
|
||||
8,aFdab04acACaf90,Jackson,Moyer,Bowen Ltd,Port Triciabury,Bahrain,001-187-882-2891x169,154.903.2525,gabriela16@mejia.org,2023-06-20,http://www.david.biz/
|
||||
9,A2EaeAd3c6529B9,Kelly,Douglas,Phillips PLC,Weissberg,Qatar,767.177.3199x5576,5156730935,wigginsmercedes@tanner.com,2021-06-30,https://gonzalez.net/
|
||||
10,188175A5641d080,Mariah,Riddle,"Mata, Fuller and Good",Choiton,Namibia,092-993-3559x342,190.937.3937,eileen32@greer.biz,2021-04-17,http://www.bowen.info/
|
||||
11,B2BA817C7bC09f0,Kristine,Pennington,"Koch, Diaz and Valdez",Port Rachael,Suriname,877.152.6488x921,790-804-9979x3290,tonya00@flynn.com,2024-03-28,https://www.rocha.net/
|
||||
12,fFAD70B5Febc43a,Robert,Adams,Bender-Wolfe,Billfort,Tanzania,219-696-1912x93626,518-969-4058,jeffersoncolton@moss-ashley.com,2022-07-31,https://www.alvarez.com/
|
||||
13,9a44524E1261Ed6,Franklin,Costa,Werner Group,North Bradley,Cook Islands,(895)448-4681x1999,648-345-3083x88242,marksmith@novak.com,2024-12-03,http://thornton.com/
|
||||
14,0C360EfE17D06cc,Dan,Ballard,Sampson-Huff,Michaelchester,Sao Tome and Principe,+1-803-469-5237x2580,(729)574-6101x0605,molly54@acevedo.info,2022-10-24,https://www.sloan-gonzales.biz/
|
||||
15,fb5cE8cb1eF2954,Clifford,Moyer,Fry-Whitehead,Lake Gary,Japan,(504)263-0450,247.514.1614x551,rhonda34@orr.com,2020-03-08,https://norton.com/
|
||||
16,3e53DcaD1cB0054,Tonya,Durham,"Lowe, West and Reynolds",South Sylviaton,Brunei Darussalam,(613)893-8183,(461)399-6120,kiara96@meadows.biz,2022-08-04,https://www.reeves.com/
|
||||
17,83fDDE5812EbEde,Dale,Bishop,Shaw-Ray,Lake Miranda,Congo,(082)202-9241,+1-413-610-1854x79378,bhoward@hodges.biz,2023-05-08,http://www.escobar.com/
|
||||
18,8dB7ACe2C5758cE,Priscilla,Mills,Pope PLC,North Cliffordshire,South Georgia and the South Sandwich Islands,+1-465-802-4573x30191,044-393-6049x598,nathaniel28@hess.com,2020-09-08,https://www.watts.org/
|
||||
19,c1092ebDaF2ceED,Alice,Bond,Chan-Liu,West Wesley,Lithuania,+1-333-285-8515,(715)759-3158x77103,jose92@peters.com,2024-05-30,http://www.marquez.com/
|
||||
20,4d0c95579D095aF,Cole,Compton,Calderon PLC,Robertaville,Saudi Arabia,029.862.3387x470,001-319-474-2394,sspence@dorsey.com,2020-08-02,http://hamilton.net/
|
||||
|
@@ -22,7 +22,7 @@ services:
|
||||
restart: always
|
||||
ports:
|
||||
- 16005:3306
|
||||
- "16014:22"
|
||||
- "16015:22"
|
||||
|
||||
mysql-ssh-keyfile:
|
||||
build: containers/mysql-ssh-keyfile
|
||||
@@ -54,7 +54,7 @@ services:
|
||||
image: mcr.microsoft.com/mssql/server
|
||||
restart: always
|
||||
ports:
|
||||
- 16012:1433
|
||||
- 16014:1433
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- SA_PASSWORD=Pwd2020Db
|
||||
|
||||
13
e2e-tests/env/multi-sql/.env
vendored
13
e2e-tests/env/multi-sql/.env
vendored
@@ -1,4 +1,5 @@
|
||||
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite
|
||||
CONNECTIONS=mysql,postgres,mssql,oracle,sqlite,mongo
|
||||
LOG_CONNECTION_SENSITIVE_VALUES=true
|
||||
|
||||
LABEL_mysql=MySql-connection
|
||||
SERVER_mysql=localhost
|
||||
@@ -32,5 +33,13 @@ PORT_mssql=16014
|
||||
ENGINE_mssql=mssql@dbgate-plugin-mssql
|
||||
|
||||
LABEL_sqlite=Sqlite-connection
|
||||
FILE_sqlite=my_guitar_shop.db
|
||||
FILE_sqlite=%%E2E_TEST_DATA_DIRECTORY%%/my_guitar_shop.db
|
||||
ENGINE_sqlite=sqlite@dbgate-plugin-sqlite
|
||||
|
||||
LABEL_mongo=Mongo-connection
|
||||
SERVER_mongo=localhost
|
||||
USER_mongo=root
|
||||
PASSWORD_mongo=Pwd2020Db
|
||||
PORT_mongo=16010
|
||||
ENGINE_mongo=mongo@dbgate-plugin-mongo
|
||||
|
||||
|
||||
@@ -8,12 +8,13 @@ dbgateApi.registerPlugins(dbgatePluginMysql);
|
||||
const dbgatePluginPostgres = require('dbgate-plugin-postgres');
|
||||
dbgateApi.registerPlugins(dbgatePluginPostgres);
|
||||
|
||||
async function createDb(connection, dropDbSql, createDbSql, database = 'my_guitar_shop') {
|
||||
async function createDb(connection, dropDbSql, createDbSql, database = 'my_guitar_shop', { dropDatabaseName } = {}) {
|
||||
if (dropDbSql) {
|
||||
try {
|
||||
await dbgateApi.executeQuery({
|
||||
connection,
|
||||
sql: dropDbSql,
|
||||
database: dropDatabaseName,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to drop database', err);
|
||||
@@ -97,7 +98,10 @@ async function run() {
|
||||
if (localconfig.sqlite) {
|
||||
await createDb(
|
||||
{
|
||||
databaseFile: process.env.FILE_sqlite,
|
||||
databaseFile: process.env.FILE_sqlite.replace(
|
||||
'%%E2E_TEST_DATA_DIRECTORY%%',
|
||||
path.join(path.dirname(__dirname), 'tmpdata')
|
||||
),
|
||||
singleDatabase: true,
|
||||
engine: 'sqlite@dbgate-plugin-sqlite',
|
||||
},
|
||||
@@ -105,6 +109,22 @@ async function run() {
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
if (localconfig.mongo) {
|
||||
await createDb(
|
||||
{
|
||||
server: process.env.SERVER_mongo,
|
||||
user: process.env.USER_mongo,
|
||||
password: process.env.PASSWORD_mongo,
|
||||
port: process.env.PORT_mongo,
|
||||
engine: 'mongo@dbgate-plugin-mongo',
|
||||
},
|
||||
'db.dropDatabase()',
|
||||
null,
|
||||
'my_guitar_shop',
|
||||
{ dropDatabaseName: 'my_guitar_shop' }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
dbgateApi.runScript(run);
|
||||
|
||||
0
e2e-tests/tmpdata/.gitkeep
Normal file
0
e2e-tests/tmpdata/.gitkeep
Normal file
@@ -27,11 +27,11 @@ services:
|
||||
# - MYSQL_ROOT_PASSWORD=Pwd2020Db
|
||||
#
|
||||
|
||||
cassandradb:
|
||||
image: cassandra:5.0.2
|
||||
ports:
|
||||
- 15942:9042
|
||||
|
||||
# cassandradb:
|
||||
# image: cassandra:5.0.2
|
||||
# ports:
|
||||
# - 15942:9042
|
||||
#
|
||||
# clickhouse:
|
||||
# image: bitnami/clickhouse:24.8.4
|
||||
# restart: always
|
||||
@@ -55,28 +55,37 @@ services:
|
||||
# ports:
|
||||
# - 15003:26257
|
||||
# command: start-single-node --insecure
|
||||
# mongodb:
|
||||
# image: mongo:4.0.12
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - mongo-data:/data/db
|
||||
# - mongo-config:/data/configdb
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
# mongodb:
|
||||
# image: mongo:4.0.12
|
||||
# restart: always
|
||||
# volumes:
|
||||
# - mongo-data:/data/db
|
||||
# - mongo-config:/data/configdb
|
||||
# ports:
|
||||
# - 27017:27017
|
||||
|
||||
# cockroachdb-init:
|
||||
# image: cockroachdb/cockroach
|
||||
# # build: cockroach
|
||||
# # entrypoint: /cockroach/init.sh
|
||||
# entrypoint: ./cockroach sql --insecure --host="cockroachdb" --execute="CREATE DATABASE IF NOT EXISTS test;"
|
||||
# cockroachdb-init:
|
||||
# image: cockroachdb/cockroach
|
||||
# # build: cockroach
|
||||
# # entrypoint: /cockroach/init.sh
|
||||
# entrypoint: ./cockroach sql --insecure --host="cockroachdb" --execute="CREATE DATABASE IF NOT EXISTS test;"
|
||||
|
||||
# depends_on:
|
||||
# - cockroachdb
|
||||
# restart: on-failure
|
||||
# depends_on:
|
||||
# - cockroachdb
|
||||
# restart: on-failure
|
||||
|
||||
oracle:
|
||||
image: gvenzl/oracle-xe:21-slim
|
||||
environment:
|
||||
ORACLE_PASSWORD: Pwd2020Db
|
||||
# oracle:
|
||||
# image: gvenzl/oracle-xe:21-slim
|
||||
# environment:
|
||||
# ORACLE_PASSWORD: Pwd2020Db
|
||||
# ports:
|
||||
# - 15006:1521
|
||||
|
||||
libsql:
|
||||
image: ghcr.io/tursodatabase/libsql-server:latest
|
||||
platform: linux/amd64
|
||||
ports:
|
||||
- 15006:1521
|
||||
- '8080:8080'
|
||||
- '5002:5001'
|
||||
volumes:
|
||||
- ./data/libsql:/var/lib/sqld
|
||||
|
||||
@@ -506,6 +506,23 @@ const sqliteEngine = {
|
||||
],
|
||||
};
|
||||
|
||||
const libsqlFileEngine = {
|
||||
...sqliteEngine,
|
||||
label: 'LibSQL FILE',
|
||||
connection: {
|
||||
engine: 'libsql@dbgate-plugin-sqlite',
|
||||
},
|
||||
};
|
||||
|
||||
const libsqlWsEngine = {
|
||||
...sqliteEngine,
|
||||
label: 'LibSQL WS',
|
||||
connection: {
|
||||
engine: 'libsql@dbgate-plugin-sqlite',
|
||||
databaseUrl: 'ws://localhost:8080',
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {import('dbgate-types').TestEngineInfo} */
|
||||
const cockroachDbEngine = {
|
||||
label: 'CockroachDB',
|
||||
@@ -644,6 +661,8 @@ const enginesOnCi = [
|
||||
postgreSqlEngine,
|
||||
sqlServerEngine,
|
||||
sqliteEngine,
|
||||
libsqlFileEngine,
|
||||
libsqlWsEngine,
|
||||
// cockroachDbEngine,
|
||||
clickhouseEngine,
|
||||
oracleEngine,
|
||||
@@ -659,7 +678,9 @@ const enginesOnLocal = [
|
||||
// sqlServerEngine,
|
||||
// sqliteEngine,
|
||||
// cockroachDbEngine,
|
||||
clickhouseEngine,
|
||||
// clickhouseEngine,
|
||||
// libsqlFileEngine,
|
||||
libsqlWsEngine,
|
||||
// oracleEngine,
|
||||
];
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "6.2.1-premium-beta.9",
|
||||
"version": "6.2.2-packer-beta.4",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@@ -67,7 +67,11 @@
|
||||
"postinstall": "yarn resetPackagedPlugins && yarn build:lib && patch-package && yarn build:plugins:frontend",
|
||||
"dbgate-serve": "node packages/dbgate/bin/dbgate-serve.js",
|
||||
"workflows": "node common/processWorkflows.js",
|
||||
"cy:open": "cd e2e-tests && yarn cy:open"
|
||||
"cy:open": "cd e2e-tests && yarn cy:open",
|
||||
"translations:extract": "node common/translations-cli/index.js extract",
|
||||
"translations:add-missing": "node common/translations-cli/index.js add-missing",
|
||||
"translations:remove-unused": "node common/translations-cli/index.js remove-unused",
|
||||
"translations:check": "node common/translations-cli/index.js check"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
|
||||
@@ -62,7 +62,10 @@ function getPortalCollections() {
|
||||
port: process.env[`PORT_${id}`],
|
||||
databaseUrl: process.env[`URL_${id}`],
|
||||
useDatabaseUrl: !!process.env[`URL_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`],
|
||||
databaseFile: process.env[`FILE_${id}`]?.replace(
|
||||
'%%E2E_TEST_DATA_DIRECTORY%%',
|
||||
path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata')
|
||||
),
|
||||
socketPath: process.env[`SOCKET_PATH_${id}`],
|
||||
serviceName: process.env[`SERVICE_NAME_${id}`],
|
||||
authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
|
||||
@@ -77,6 +80,7 @@ function getPortalCollections() {
|
||||
allowedDatabasesRegex: process.env[`ALLOWED_DATABASES_REGEX_${id}`],
|
||||
parent: process.env[`PARENT_${id}`] || undefined,
|
||||
useSeparateSchemas: !!process.env[`USE_SEPARATE_SCHEMAS_${id}`],
|
||||
localDataCenter: process.env[`LOCAL_DATA_CENTER_${id}`],
|
||||
|
||||
// SSH tunnel
|
||||
useSshTunnel: process.env[`USE_SSH_${id}`],
|
||||
|
||||
@@ -233,6 +233,7 @@ module.exports = {
|
||||
dispatchDatabaseChangedEvent_meta: true,
|
||||
dispatchDatabaseChangedEvent({ event, conid, database }) {
|
||||
socket.emitChanged(event, { conid, database });
|
||||
return null;
|
||||
},
|
||||
|
||||
loadKeys_meta: true,
|
||||
|
||||
@@ -94,14 +94,26 @@ module.exports = {
|
||||
handle_ping() {},
|
||||
|
||||
handle_freeData(runid, { freeData }) {
|
||||
const [resolve, reject] = this.requests[runid];
|
||||
const { resolve } = this.requests[runid];
|
||||
resolve(freeData);
|
||||
delete this.requests[runid];
|
||||
},
|
||||
|
||||
handle_copyStreamError(runid, { copyStreamError }) {
|
||||
const { reject, exitOnStreamError } = this.requests[runid] || {};
|
||||
if (exitOnStreamError) {
|
||||
reject(copyStreamError);
|
||||
delete this.requests[runid];
|
||||
}
|
||||
},
|
||||
|
||||
handle_progress(runid, progressData) {
|
||||
socket.emit(`runner-progress-${runid}`, progressData);
|
||||
},
|
||||
|
||||
rejectRequest(runid, error) {
|
||||
if (this.requests[runid]) {
|
||||
const [resolve, reject] = this.requests[runid];
|
||||
const { reject } = this.requests[runid];
|
||||
reject(error);
|
||||
delete this.requests[runid];
|
||||
}
|
||||
@@ -113,6 +125,8 @@ module.exports = {
|
||||
fs.writeFileSync(`${scriptFile}`, scriptText);
|
||||
fs.mkdirSync(directory);
|
||||
const pluginNames = extractPlugins(scriptText);
|
||||
// console.log('********************** SCRIPT TEXT **********************');
|
||||
// console.log(scriptText);
|
||||
logger.info({ scriptFile }, 'Running script');
|
||||
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
|
||||
const subprocess = fork(
|
||||
@@ -150,11 +164,13 @@ module.exports = {
|
||||
byline(subprocess.stdout).on('data', pipeDispatcher('info'));
|
||||
byline(subprocess.stderr).on('data', pipeDispatcher('error'));
|
||||
subprocess.on('exit', code => {
|
||||
// console.log('... EXITED', code);
|
||||
this.rejectRequest(runid, { message: 'No data returned, maybe input data source is too big' });
|
||||
logger.info({ code, pid: subprocess.pid }, 'Exited process');
|
||||
socket.emit(`runner-done-${runid}`, code);
|
||||
});
|
||||
subprocess.on('error', error => {
|
||||
// console.log('... ERROR subprocess', error);
|
||||
this.rejectRequest(runid, { message: error && (error.message || error.toString()) });
|
||||
console.error('... ERROR subprocess', error);
|
||||
this.dispatchMessage({
|
||||
@@ -231,7 +247,7 @@ module.exports = {
|
||||
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const runid = crypto.randomUUID();
|
||||
this.requests[runid] = [resolve, reject];
|
||||
this.requests[runid] = { resolve, reject, exitOnStreamError: true };
|
||||
this.startCore(runid, loaderScriptTemplate(prefix, functionName, props, runid));
|
||||
});
|
||||
return promise;
|
||||
|
||||
@@ -1,6 +1,27 @@
|
||||
const EnsureStreamHeaderStream = require('../utility/EnsureStreamHeaderStream');
|
||||
const Stream = require('stream');
|
||||
const ColumnMapTransformStream = require('../utility/ColumnMapTransformStream');
|
||||
const streamPipeline = require('../utility/streamPipeline');
|
||||
const { getLogger, extractErrorLogData, RowProgressReporter, extractErrorMessage } = require('dbgate-tools');
|
||||
const logger = getLogger('copyStream');
|
||||
const stream = require('stream');
|
||||
|
||||
class ReportingTransform extends stream.Transform {
|
||||
constructor(reporter, options = {}) {
|
||||
super({ ...options, objectMode: true });
|
||||
this.reporter = reporter;
|
||||
}
|
||||
_transform(chunk, encoding, callback) {
|
||||
if (!chunk?.__isStreamHeader) {
|
||||
this.reporter.add(1);
|
||||
}
|
||||
this.push(chunk);
|
||||
callback();
|
||||
}
|
||||
_flush(callback) {
|
||||
this.reporter.finish();
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies reader to writer. Used for import, export tables and transfer data between tables
|
||||
@@ -9,10 +30,23 @@ const ColumnMapTransformStream = require('../utility/ColumnMapTransformStream');
|
||||
* @param {object} options - options
|
||||
* @returns {Promise}
|
||||
*/
|
||||
function copyStream(input, output, options) {
|
||||
const { columns } = options || {};
|
||||
async function copyStream(input, output, options) {
|
||||
const { columns, progressName } = options || {};
|
||||
|
||||
if (progressName) {
|
||||
process.send({
|
||||
msgtype: 'progress',
|
||||
progressName,
|
||||
status: 'running',
|
||||
});
|
||||
}
|
||||
|
||||
const transforms = [];
|
||||
|
||||
if (progressName) {
|
||||
const reporter = new RowProgressReporter(progressName, 'readRowCount');
|
||||
transforms.push(new ReportingTransform(reporter));
|
||||
}
|
||||
if (columns) {
|
||||
transforms.push(new ColumnMapTransformStream(columns));
|
||||
}
|
||||
@@ -20,36 +54,37 @@ function copyStream(input, output, options) {
|
||||
transforms.push(new EnsureStreamHeaderStream());
|
||||
}
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// Stream.pipeline(input, ...transforms, output, err => {
|
||||
// if (err) {
|
||||
// reject(err);
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
try {
|
||||
await streamPipeline(input, transforms, output);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const finisher = output['finisher'] || output;
|
||||
finisher.on('finish', resolve);
|
||||
finisher.on('error', reject);
|
||||
|
||||
let lastStream = input;
|
||||
for (const tran of transforms) {
|
||||
lastStream.pipe(tran);
|
||||
lastStream = tran;
|
||||
if (progressName) {
|
||||
process.send({
|
||||
msgtype: 'progress',
|
||||
progressName,
|
||||
status: 'done',
|
||||
});
|
||||
}
|
||||
lastStream.pipe(output);
|
||||
} catch (err) {
|
||||
process.send({
|
||||
msgtype: 'copyStreamError',
|
||||
copyStreamError: {
|
||||
message: extractErrorMessage(err),
|
||||
...err,
|
||||
},
|
||||
});
|
||||
|
||||
// if (output.requireFixedStructure) {
|
||||
// const ensureHeader = new EnsureStreamHeaderStream();
|
||||
// input.pipe(ensureHeader);
|
||||
// ensureHeader.pipe(output);
|
||||
// } else {
|
||||
// input.pipe(output);
|
||||
// }
|
||||
});
|
||||
if (progressName) {
|
||||
process.send({
|
||||
msgtype: 'progress',
|
||||
progressName,
|
||||
status: 'error',
|
||||
errorMessage: extractErrorMessage(err),
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(extractErrorLogData(err, { progressName }), 'Import/export job failed');
|
||||
// throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = copyStream;
|
||||
|
||||
@@ -24,8 +24,6 @@ async function dataDuplicator({
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'write'));
|
||||
|
||||
try {
|
||||
logger.info(`Connected.`);
|
||||
|
||||
if (!analysedStructure) {
|
||||
analysedStructure = await driver.analyseFull(dbhan);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ async function dropAllDbObjects({ connection, systemConnection, driver, analysed
|
||||
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'write'));
|
||||
|
||||
logger.info(`Connected.`);
|
||||
|
||||
if (!analysedStructure) {
|
||||
analysedStructure = await driver.analyseFull(dbhan);
|
||||
}
|
||||
|
||||
@@ -31,8 +31,6 @@ async function dumpDatabase({
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'read', { forceRowsAsObjects: true }));
|
||||
|
||||
try {
|
||||
logger.info(`Connected.`);
|
||||
|
||||
const dumper = await driver.createBackupDumper(dbhan, {
|
||||
outputFile,
|
||||
databaseName,
|
||||
|
||||
@@ -36,7 +36,7 @@ async function executeQuery({
|
||||
}
|
||||
|
||||
try {
|
||||
logger.info(`Connected.`);
|
||||
logger.debug(`Running SQL query, length: ${sql.length}`);
|
||||
|
||||
await driver.script(dbhan, sql, { logScriptItems });
|
||||
} finally {
|
||||
|
||||
@@ -5,6 +5,7 @@ const { splitQueryStream } = require('dbgate-query-splitter/lib/splitQueryStream
|
||||
const download = require('./download');
|
||||
const stream = require('stream');
|
||||
const { getLogger } = require('dbgate-tools');
|
||||
const streamPipeline = require('../utility/streamPipeline');
|
||||
|
||||
const logger = getLogger('importDb');
|
||||
|
||||
@@ -43,25 +44,12 @@ class ImportStream extends stream.Transform {
|
||||
}
|
||||
}
|
||||
|
||||
function awaitStreamEnd(stream) {
|
||||
return new Promise((resolve, reject) => {
|
||||
stream.once('end', () => {
|
||||
resolve(true);
|
||||
});
|
||||
stream.once('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function importDatabase({ connection = undefined, systemConnection = undefined, driver = undefined, inputFile }) {
|
||||
logger.info(`Importing database`);
|
||||
|
||||
if (!driver) driver = requireEngineDriver(connection);
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'write'));
|
||||
try {
|
||||
logger.info(`Connected.`);
|
||||
|
||||
logger.info(`Input file: ${inputFile}`);
|
||||
const downloadedFile = await download(inputFile);
|
||||
logger.info(`Downloaded file: ${downloadedFile}`);
|
||||
@@ -72,9 +60,8 @@ async function importDatabase({ connection = undefined, systemConnection = undef
|
||||
returnRichInfo: true,
|
||||
});
|
||||
const importStream = new ImportStream(dbhan, driver);
|
||||
// @ts-ignore
|
||||
splittedStream.pipe(importStream);
|
||||
await awaitStreamEnd(importStream);
|
||||
|
||||
await streamPipeline(splittedStream, importStream);
|
||||
} finally {
|
||||
if (!systemConnection) {
|
||||
await driver.close(dbhan);
|
||||
|
||||
@@ -23,82 +23,97 @@ async function importDbFromFolder({ connection, systemConnection, driver, folder
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'read'));
|
||||
|
||||
try {
|
||||
const model = await importDbModel(folder);
|
||||
if (driver?.databaseEngineTypes?.includes('sql')) {
|
||||
const model = await importDbModel(folder);
|
||||
|
||||
let modelAdapted = {
|
||||
...model,
|
||||
tables: model.tables.map(table => driver.adaptTableInfo(table)),
|
||||
};
|
||||
for (const transform of modelTransforms || []) {
|
||||
modelAdapted = transform(modelAdapted);
|
||||
}
|
||||
|
||||
const modelNoFk = {
|
||||
...modelAdapted,
|
||||
tables: modelAdapted.tables.map(table => ({
|
||||
...table,
|
||||
foreignKeys: [],
|
||||
})),
|
||||
};
|
||||
|
||||
// const plan = createAlterDatabasePlan(
|
||||
// DatabaseAnalyser.createEmptyStructure(),
|
||||
// driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
// {},
|
||||
// DatabaseAnalyser.createEmptyStructure(),
|
||||
// driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
// driver
|
||||
// );
|
||||
// const dmp1 = driver.createDumper({ useHardSeparator: true });
|
||||
// if (driver.dialect.enableAllForeignKeys) {
|
||||
// dmp1.enableAllForeignKeys(false);
|
||||
// }
|
||||
// plan.run(dmp1);
|
||||
// if (driver.dialect.enableAllForeignKeys) {
|
||||
// dmp1.enableAllForeignKeys(true);
|
||||
// }
|
||||
|
||||
const { sql } = getAlterDatabaseScript(
|
||||
DatabaseAnalyser.createEmptyStructure(),
|
||||
driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
{},
|
||||
DatabaseAnalyser.createEmptyStructure(),
|
||||
driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
driver
|
||||
);
|
||||
// console.log('CREATING STRUCTURE:', sql);
|
||||
await executeQuery({ connection, systemConnection: dbhan, driver, sql, logScriptItems: true });
|
||||
|
||||
if (driver.dialect.enableAllForeignKeys) {
|
||||
await runCommandOnDriver(dbhan, driver, dmp => dmp.enableAllForeignKeys(false));
|
||||
}
|
||||
|
||||
for (const table of modelAdapted.tables) {
|
||||
const fileName = path.join(folder, `${table.pureName}.jsonl`);
|
||||
if (await fs.exists(fileName)) {
|
||||
const src = await jsonLinesReader({ fileName });
|
||||
const dst = await tableWriter({
|
||||
systemConnection: dbhan,
|
||||
pureName: table.pureName,
|
||||
driver,
|
||||
targetTableStructure: table,
|
||||
});
|
||||
await copyStream(src, dst);
|
||||
let modelAdapted = {
|
||||
...model,
|
||||
tables: model.tables.map(table => driver.adaptTableInfo(table)),
|
||||
};
|
||||
for (const transform of modelTransforms || []) {
|
||||
modelAdapted = transform(modelAdapted);
|
||||
}
|
||||
|
||||
const modelNoFk = {
|
||||
...modelAdapted,
|
||||
tables: modelAdapted.tables.map(table => ({
|
||||
...table,
|
||||
foreignKeys: [],
|
||||
})),
|
||||
};
|
||||
|
||||
// const plan = createAlterDatabasePlan(
|
||||
// DatabaseAnalyser.createEmptyStructure(),
|
||||
// driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
// {},
|
||||
// DatabaseAnalyser.createEmptyStructure(),
|
||||
// driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
// driver
|
||||
// );
|
||||
// const dmp1 = driver.createDumper({ useHardSeparator: true });
|
||||
// if (driver.dialect.enableAllForeignKeys) {
|
||||
// dmp1.enableAllForeignKeys(false);
|
||||
// }
|
||||
// plan.run(dmp1);
|
||||
// if (driver.dialect.enableAllForeignKeys) {
|
||||
// dmp1.enableAllForeignKeys(true);
|
||||
// }
|
||||
|
||||
const { sql } = getAlterDatabaseScript(
|
||||
DatabaseAnalyser.createEmptyStructure(),
|
||||
driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
{},
|
||||
DatabaseAnalyser.createEmptyStructure(),
|
||||
driver.dialect.enableAllForeignKeys ? modelAdapted : modelNoFk,
|
||||
driver
|
||||
);
|
||||
// console.log('CREATING STRUCTURE:', sql);
|
||||
await executeQuery({ connection, systemConnection: dbhan, driver, sql, logScriptItems: true });
|
||||
|
||||
if (driver.dialect.enableAllForeignKeys) {
|
||||
await runCommandOnDriver(dbhan, driver, dmp => dmp.enableAllForeignKeys(false));
|
||||
}
|
||||
}
|
||||
|
||||
if (driver.dialect.enableAllForeignKeys) {
|
||||
await runCommandOnDriver(dbhan, driver, dmp => dmp.enableAllForeignKeys(true));
|
||||
} else if (driver.dialect.createForeignKey) {
|
||||
const dmp = driver.createDumper();
|
||||
for (const table of modelAdapted.tables) {
|
||||
for (const fk of table.foreignKeys) {
|
||||
dmp.createForeignKey(fk);
|
||||
const fileName = path.join(folder, `${table.pureName}.jsonl`);
|
||||
if (await fs.exists(fileName)) {
|
||||
const src = await jsonLinesReader({ fileName });
|
||||
const dst = await tableWriter({
|
||||
systemConnection: dbhan,
|
||||
pureName: table.pureName,
|
||||
driver,
|
||||
targetTableStructure: table,
|
||||
});
|
||||
await copyStream(src, dst);
|
||||
}
|
||||
}
|
||||
|
||||
// create foreign keys
|
||||
await executeQuery({ connection, systemConnection: dbhan, driver, sql: dmp.s, logScriptItems: true });
|
||||
if (driver.dialect.enableAllForeignKeys) {
|
||||
await runCommandOnDriver(dbhan, driver, dmp => dmp.enableAllForeignKeys(true));
|
||||
} else if (driver.dialect.createForeignKey) {
|
||||
const dmp = driver.createDumper();
|
||||
for (const table of modelAdapted.tables) {
|
||||
for (const fk of table.foreignKeys) {
|
||||
dmp.createForeignKey(fk);
|
||||
}
|
||||
}
|
||||
|
||||
// create foreign keys
|
||||
await executeQuery({ connection, systemConnection: dbhan, driver, sql: dmp.s, logScriptItems: true });
|
||||
}
|
||||
} else if (driver?.databaseEngineTypes?.includes('document')) {
|
||||
for (const file of fs.readdirSync(folder)) {
|
||||
if (!file.endsWith('.jsonl')) continue;
|
||||
const pureName = path.parse(file).name;
|
||||
const src = await jsonLinesReader({ fileName: path.join(folder, file) });
|
||||
const dst = await tableWriter({
|
||||
systemConnection: dbhan,
|
||||
pureName,
|
||||
driver,
|
||||
createIfNotExists: true,
|
||||
});
|
||||
await copyStream(src, dst);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!systemConnection) {
|
||||
|
||||
@@ -53,8 +53,7 @@ async function jsonLinesReader({ fileName, encoding = 'utf-8', limitRows = undef
|
||||
);
|
||||
const liner = byline(fileStream);
|
||||
const parser = new ParseStream({ limitRows });
|
||||
liner.pipe(parser);
|
||||
return parser;
|
||||
return [liner, parser];
|
||||
}
|
||||
|
||||
module.exports = jsonLinesReader;
|
||||
|
||||
@@ -10,7 +10,6 @@ const download = require('./download');
|
||||
|
||||
const logger = getLogger('jsonReader');
|
||||
|
||||
|
||||
class ParseStream extends stream.Transform {
|
||||
constructor({ limitRows, jsonStyle, keyField }) {
|
||||
super({ objectMode: true });
|
||||
@@ -72,8 +71,12 @@ async function jsonReader({
|
||||
// @ts-ignore
|
||||
encoding
|
||||
);
|
||||
|
||||
const parseJsonStream = parser();
|
||||
fileStream.pipe(parseJsonStream);
|
||||
|
||||
const resultPipe = [fileStream, parseJsonStream];
|
||||
|
||||
// fileStream.pipe(parseJsonStream);
|
||||
|
||||
const parseStream = new ParseStream({ limitRows, jsonStyle, keyField });
|
||||
|
||||
@@ -81,15 +84,20 @@ async function jsonReader({
|
||||
|
||||
if (rootField) {
|
||||
const filterStream = pick({ filter: rootField });
|
||||
parseJsonStream.pipe(filterStream);
|
||||
filterStream.pipe(tramsformer);
|
||||
} else {
|
||||
parseJsonStream.pipe(tramsformer);
|
||||
resultPipe.push(filterStream);
|
||||
// parseJsonStream.pipe(filterStream);
|
||||
// filterStream.pipe(tramsformer);
|
||||
}
|
||||
// else {
|
||||
// parseJsonStream.pipe(tramsformer);
|
||||
// }
|
||||
|
||||
tramsformer.pipe(parseStream);
|
||||
resultPipe.push(tramsformer);
|
||||
resultPipe.push(parseStream);
|
||||
|
||||
return parseStream;
|
||||
// tramsformer.pipe(parseStream);
|
||||
|
||||
return resultPipe;
|
||||
}
|
||||
|
||||
module.exports = jsonReader;
|
||||
|
||||
@@ -99,9 +99,10 @@ async function jsonWriter({ fileName, jsonStyle, keyField = '_key', rootField, e
|
||||
logger.info(`Writing file ${fileName}`);
|
||||
const stringify = new StringifyStream({ jsonStyle, keyField, rootField });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
return [stringify, fileStream];
|
||||
// stringify.pipe(fileStream);
|
||||
// stringify['finisher'] = fileStream;
|
||||
// return stringify;
|
||||
}
|
||||
|
||||
module.exports = jsonWriter;
|
||||
|
||||
@@ -6,15 +6,13 @@ const exportDbModel = require('../utility/exportDbModel');
|
||||
const logger = getLogger('analyseDb');
|
||||
|
||||
async function loadDatabase({ connection = undefined, systemConnection = undefined, driver = undefined, outputDir }) {
|
||||
logger.info(`Analysing database`);
|
||||
logger.debug(`Analysing database`);
|
||||
|
||||
if (!driver) driver = requireEngineDriver(connection);
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'read', { forceRowsAsObjects: true }));
|
||||
try {
|
||||
logger.info(`Connected.`);
|
||||
|
||||
const dbInfo = await driver.analyseFull(dbhan);
|
||||
logger.info(`Analyse finished`);
|
||||
logger.debug(`Analyse finished`);
|
||||
|
||||
await exportDbModel(dbInfo, outputDir);
|
||||
} finally {
|
||||
|
||||
@@ -141,8 +141,9 @@ async function modifyJsonLinesReader({
|
||||
);
|
||||
const liner = byline(fileStream);
|
||||
const parser = new ParseStream({ limitRows, changeSet, mergedRows, mergeKey, mergeMode });
|
||||
liner.pipe(parser);
|
||||
return parser;
|
||||
return [liner, parser];
|
||||
// liner.pipe(parser);
|
||||
// return parser;
|
||||
}
|
||||
|
||||
module.exports = modifyJsonLinesReader;
|
||||
|
||||
@@ -30,7 +30,6 @@ async function queryReader({
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await connectUtility(driver, connection, queryType == 'json' ? 'read' : 'script');
|
||||
logger.info(`Connected.`);
|
||||
const reader =
|
||||
queryType == 'json' ? await driver.readJsonQuery(pool, query) : await driver.readQuery(pool, query || sql);
|
||||
return reader;
|
||||
|
||||
@@ -44,9 +44,10 @@ async function sqlDataWriter({ fileName, dataName, driver, encoding = 'utf-8' })
|
||||
logger.info(`Writing file ${fileName}`);
|
||||
const stringify = new SqlizeStream({ fileName, dataName });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
stringify.pipe(fileStream);
|
||||
stringify['finisher'] = fileStream;
|
||||
return stringify;
|
||||
return [stringify, fileStream];
|
||||
// stringify.pipe(fileStream);
|
||||
// stringify['finisher'] = fileStream;
|
||||
// return stringify;
|
||||
}
|
||||
|
||||
module.exports = sqlDataWriter;
|
||||
|
||||
@@ -18,7 +18,6 @@ async function tableReader({ connection, systemConnection, pureName, schemaName,
|
||||
driver = requireEngineDriver(connection);
|
||||
}
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'read'));
|
||||
logger.info(`Connected.`);
|
||||
|
||||
const fullName = { pureName, schemaName };
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ const logger = getLogger('tableWriter');
|
||||
* @param {boolean} options.truncate - truncate table before insert
|
||||
* @param {boolean} options.createIfNotExists - create table if not exists
|
||||
* @param {boolean} options.commitAfterInsert - commit transaction after insert
|
||||
* @param {string} options.progressName - name for reporting progress
|
||||
* @param {any} options.targetTableStructure - target table structure (don't analyse if given)
|
||||
* @returns {Promise<writerType>} - writer object
|
||||
*/
|
||||
@@ -26,8 +27,20 @@ async function tableWriter({ connection, schemaName, pureName, driver, systemCon
|
||||
}
|
||||
const dbhan = systemConnection || (await connectUtility(driver, connection, 'write'));
|
||||
|
||||
logger.info(`Connected.`);
|
||||
return await driver.writeTable(dbhan, { schemaName, pureName }, options);
|
||||
try {
|
||||
return await driver.writeTable(dbhan, { schemaName, pureName }, options);
|
||||
} catch (err) {
|
||||
if (options.progressName) {
|
||||
process.send({
|
||||
msgtype: 'progress',
|
||||
progressName: options.progressName,
|
||||
status: 'error',
|
||||
errorMessage: err.message,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = tableWriter;
|
||||
|
||||
@@ -88,6 +88,9 @@ function decryptConnection(connection) {
|
||||
}
|
||||
|
||||
function pickSafeConnectionInfo(connection) {
|
||||
if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
|
||||
return connection;
|
||||
}
|
||||
return _.mapValues(connection, (v, k) => {
|
||||
if (k == 'engine' || k == 'port' || k == 'authType' || k == 'sshMode' || k == 'passwordMode') return v;
|
||||
if (v === null || v === true || v === false) return v;
|
||||
|
||||
18
packages/api/src/utility/streamPipeline.js
Normal file
18
packages/api/src/utility/streamPipeline.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const stream = require('stream');
|
||||
const _ = require('lodash');
|
||||
|
||||
function streamPipeline(...processedStreams) {
|
||||
const streams = _.flattenDeep(processedStreams);
|
||||
return new Promise((resolve, reject) => {
|
||||
// @ts-ignore
|
||||
stream.pipeline(...streams, err => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = streamPipeline;
|
||||
@@ -128,7 +128,7 @@ export class ScriptDrivedDeployer {
|
||||
|
||||
logger.debug(`Running ${category} script ${file.name}`);
|
||||
try {
|
||||
await this.driver.script(this.dbhan, file.text);
|
||||
await this.driver.script(this.dbhan, file.text, { useTransaction: false });
|
||||
await this.saveToJournal(file, category, hash);
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), `Error running ${category} script ${file.name}`);
|
||||
|
||||
@@ -41,12 +41,13 @@ export class ScriptWriter {
|
||||
this.packageNames.push(packageName);
|
||||
}
|
||||
|
||||
copyStream(sourceVar, targetVar, colmapVar = null) {
|
||||
if (colmapVar) {
|
||||
this._put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar}, {columns: ${colmapVar}});`);
|
||||
} else {
|
||||
this._put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar});`);
|
||||
}
|
||||
copyStream(sourceVar, targetVar, colmapVar = null, progressName?: string) {
|
||||
let opts = '{';
|
||||
if (colmapVar) opts += `columns: ${colmapVar}, `;
|
||||
if (progressName) opts += `progressName: "${progressName}", `;
|
||||
opts += '}';
|
||||
|
||||
this._put(`await dbgateApi.copyStream(${sourceVar}, ${targetVar}, ${opts});`);
|
||||
}
|
||||
|
||||
dumpDatabase(options) {
|
||||
@@ -117,12 +118,13 @@ export class ScriptWriterJson {
|
||||
});
|
||||
}
|
||||
|
||||
copyStream(sourceVar, targetVar, colmapVar = null) {
|
||||
copyStream(sourceVar, targetVar, colmapVar = null, progressName?: string) {
|
||||
this.commands.push({
|
||||
type: 'copyStream',
|
||||
sourceVar,
|
||||
targetVar,
|
||||
colmapVar,
|
||||
progressName,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -183,7 +185,7 @@ export function jsonScriptToJavascript(json) {
|
||||
script.assignValue(cmd.variableName, cmd.jsonValue);
|
||||
break;
|
||||
case 'copyStream':
|
||||
script.copyStream(cmd.sourceVar, cmd.targetVar, cmd.colmapVar);
|
||||
script.copyStream(cmd.sourceVar, cmd.targetVar, cmd.colmapVar, cmd.progressName);
|
||||
break;
|
||||
case 'endLine':
|
||||
script.endLine();
|
||||
|
||||
@@ -3,6 +3,8 @@ import _intersection from 'lodash/intersection';
|
||||
import _fromPairs from 'lodash/fromPairs';
|
||||
import { getLogger } from './getLogger';
|
||||
import { prepareTableForImport } from './tableTransforms';
|
||||
import { RowProgressReporter } from './rowProgressReporter';
|
||||
import { extractErrorLogData } from './stringTools';
|
||||
|
||||
const logger = getLogger('bulkStreamBase');
|
||||
|
||||
@@ -21,6 +23,7 @@ export function createBulkInsertStreamBase(driver: EngineDriver, stream, dbhan,
|
||||
writable.columnNames = null;
|
||||
writable.columnDataTypes = null;
|
||||
writable.requireFixedStructure = driver.databaseEngineTypes.includes('sql');
|
||||
writable.rowsReporter = new RowProgressReporter(options.progressName);
|
||||
|
||||
writable.addRow = async row => {
|
||||
if (writable.structure) {
|
||||
@@ -32,86 +35,100 @@ export function createBulkInsertStreamBase(driver: EngineDriver, stream, dbhan,
|
||||
};
|
||||
|
||||
writable.checkStructure = async () => {
|
||||
let structure = options.targetTableStructure ?? (await driver.analyseSingleTable(dbhan, name));
|
||||
if (structure) {
|
||||
writable.structure = structure;
|
||||
}
|
||||
if (structure && options.dropIfExists) {
|
||||
logger.info(`Dropping table ${fullNameQuoted}`);
|
||||
await driver.script(dbhan, `DROP TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
if (options.createIfNotExists && (!structure || options.dropIfExists)) {
|
||||
const dmp = driver.createDumper();
|
||||
const createdTableInfo = driver.adaptTableInfo(prepareTableForImport({ ...writable.structure, ...name }));
|
||||
dmp.createTable(createdTableInfo);
|
||||
logger.info({ sql: dmp.s }, `Creating table ${fullNameQuoted}`);
|
||||
await driver.script(dbhan, dmp.s);
|
||||
structure = await driver.analyseSingleTable(dbhan, name);
|
||||
writable.structure = structure;
|
||||
}
|
||||
if (!writable.structure) {
|
||||
throw new Error(`Error importing table - ${fullNameQuoted} not found`);
|
||||
}
|
||||
if (options.truncate) {
|
||||
await driver.script(dbhan, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
try {
|
||||
let structure = options.targetTableStructure ?? (await driver.analyseSingleTable(dbhan, name));
|
||||
if (structure) {
|
||||
writable.structure = structure;
|
||||
}
|
||||
if (structure && options.dropIfExists) {
|
||||
logger.info(`Dropping table ${fullNameQuoted}`);
|
||||
await driver.script(dbhan, `DROP TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
if (options.createIfNotExists && (!structure || options.dropIfExists)) {
|
||||
const dmp = driver.createDumper();
|
||||
const createdTableInfo = driver.adaptTableInfo(prepareTableForImport({ ...writable.structure, ...name }));
|
||||
dmp.createTable(createdTableInfo);
|
||||
logger.info({ sql: dmp.s }, `Creating table ${fullNameQuoted}`);
|
||||
await driver.script(dbhan, dmp.s);
|
||||
structure = await driver.analyseSingleTable(dbhan, name);
|
||||
writable.structure = structure;
|
||||
}
|
||||
if (!writable.structure) {
|
||||
throw new Error(`Error importing table - ${fullNameQuoted} not found`);
|
||||
}
|
||||
if (options.truncate) {
|
||||
await driver.script(dbhan, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
|
||||
writable.columnNames = _intersection(
|
||||
structure.columns.map(x => x.columnName),
|
||||
writable.structure.columns.map(x => x.columnName)
|
||||
);
|
||||
writable.columnDataTypes = _fromPairs(
|
||||
writable.columnNames.map(colName => [
|
||||
colName,
|
||||
writable.structure.columns.find(x => x.columnName == colName)?.dataType,
|
||||
])
|
||||
);
|
||||
writable.columnNames = _intersection(
|
||||
structure.columns.map(x => x.columnName),
|
||||
writable.structure.columns.map(x => x.columnName)
|
||||
);
|
||||
writable.columnDataTypes = _fromPairs(
|
||||
writable.columnNames.map(colName => [
|
||||
colName,
|
||||
writable.structure.columns.find(x => x.columnName == colName)?.dataType,
|
||||
])
|
||||
);
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error during preparing bulk insert table, stopped');
|
||||
writable.destroy(err);
|
||||
}
|
||||
};
|
||||
|
||||
writable.send = async () => {
|
||||
const rows = writable.buffer;
|
||||
writable.buffer = [];
|
||||
|
||||
if (driver.dialect.allowMultipleValuesInsert) {
|
||||
const dmp = driver.createDumper();
|
||||
dmp.putRaw(`INSERT INTO ${fullNameQuoted} (`);
|
||||
dmp.putCollection(',', writable.columnNames, col => dmp.putRaw(driver.dialect.quoteIdentifier(col as string)));
|
||||
dmp.putRaw(')\n VALUES\n');
|
||||
|
||||
let wasRow = false;
|
||||
for (const row of rows) {
|
||||
if (wasRow) dmp.putRaw(',\n');
|
||||
dmp.putRaw('(');
|
||||
dmp.putCollection(',', writable.columnNames, col =>
|
||||
dmp.putValue(row[col as string], writable.columnDataTypes?.[col as string])
|
||||
);
|
||||
dmp.putRaw(')');
|
||||
wasRow = true;
|
||||
}
|
||||
dmp.putRaw(';');
|
||||
// require('fs').writeFileSync('/home/jena/test.sql', dmp.s);
|
||||
// console.log(dmp.s);
|
||||
await driver.query(dbhan, dmp.s, { discardResult: true });
|
||||
} else {
|
||||
for (const row of rows) {
|
||||
try {
|
||||
if (driver.dialect.allowMultipleValuesInsert) {
|
||||
const dmp = driver.createDumper();
|
||||
dmp.putRaw(`INSERT INTO ${fullNameQuoted} (`);
|
||||
dmp.putCollection(',', writable.columnNames, col => dmp.putRaw(driver.dialect.quoteIdentifier(col as string)));
|
||||
dmp.putRaw(')\n VALUES\n');
|
||||
|
||||
dmp.putRaw('(');
|
||||
dmp.putCollection(',', writable.columnNames, col =>
|
||||
dmp.putValue(row[col as string], writable.columnDataTypes?.[col as string])
|
||||
);
|
||||
dmp.putRaw(')');
|
||||
let wasRow = false;
|
||||
for (const row of rows) {
|
||||
if (wasRow) dmp.putRaw(',\n');
|
||||
dmp.putRaw('(');
|
||||
dmp.putCollection(',', writable.columnNames, col =>
|
||||
dmp.putValue(row[col as string], writable.columnDataTypes?.[col as string])
|
||||
);
|
||||
dmp.putRaw(')');
|
||||
wasRow = true;
|
||||
}
|
||||
dmp.putRaw(';');
|
||||
// require('fs').writeFileSync('/home/jena/test.sql', dmp.s);
|
||||
// console.log(dmp.s);
|
||||
await driver.query(dbhan, dmp.s, { discardResult: true });
|
||||
writable.rowsReporter.add(rows.length);
|
||||
} else {
|
||||
for (const row of rows) {
|
||||
const dmp = driver.createDumper();
|
||||
dmp.putRaw(`INSERT INTO ${fullNameQuoted} (`);
|
||||
dmp.putCollection(',', writable.columnNames, col =>
|
||||
dmp.putRaw(driver.dialect.quoteIdentifier(col as string))
|
||||
);
|
||||
dmp.putRaw(')\n VALUES\n');
|
||||
|
||||
dmp.putRaw('(');
|
||||
dmp.putCollection(',', writable.columnNames, col =>
|
||||
dmp.putValue(row[col as string], writable.columnDataTypes?.[col as string])
|
||||
);
|
||||
dmp.putRaw(')');
|
||||
// console.log(dmp.s);
|
||||
await driver.query(dbhan, dmp.s, { discardResult: true });
|
||||
writable.rowsReporter.add(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (options.commitAfterInsert) {
|
||||
const dmp = driver.createDumper();
|
||||
dmp.commitTransaction();
|
||||
await driver.query(dbhan, dmp.s, { discardResult: true });
|
||||
if (options.commitAfterInsert) {
|
||||
const dmp = driver.createDumper();
|
||||
dmp.commitTransaction();
|
||||
await driver.query(dbhan, dmp.s, { discardResult: true });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error during base bulk insert, insert stopped');
|
||||
writable.destroy(err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,6 +146,7 @@ export function createBulkInsertStreamBase(driver: EngineDriver, stream, dbhan,
|
||||
|
||||
writable._final = async callback => {
|
||||
await writable.send();
|
||||
writable.rowsReporter.finish();
|
||||
callback();
|
||||
};
|
||||
|
||||
|
||||
@@ -25,3 +25,4 @@ export * from './detectSqlFilterBehaviour';
|
||||
export * from './filterBehaviours';
|
||||
export * from './schemaInfoTools';
|
||||
export * from './dbKeysLoader';
|
||||
export * from './rowProgressReporter';
|
||||
45
packages/tools/src/rowProgressReporter.ts
Normal file
45
packages/tools/src/rowProgressReporter.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export class RowProgressReporter {
|
||||
counter = 0;
|
||||
timeoutHandle = null;
|
||||
|
||||
constructor(public progressName, public field = 'writtenRowCount') {}
|
||||
|
||||
add(count: number) {
|
||||
this.counter += count;
|
||||
if (!this.progressName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timeoutHandle) {
|
||||
return;
|
||||
}
|
||||
this.timeoutHandle = setTimeout(() => {
|
||||
this.timeoutHandle = null;
|
||||
this.send();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
finish() {
|
||||
if (!this.progressName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.timeoutHandle) {
|
||||
clearTimeout(this.timeoutHandle);
|
||||
this.timeoutHandle = null;
|
||||
}
|
||||
this.send();
|
||||
}
|
||||
|
||||
send() {
|
||||
if (!this.progressName) {
|
||||
return;
|
||||
}
|
||||
|
||||
process.send({
|
||||
msgtype: 'progress',
|
||||
progressName: this.progressName,
|
||||
[this.field]: this.counter,
|
||||
});
|
||||
}
|
||||
}
|
||||
1
packages/types/dialect.d.ts
vendored
1
packages/types/dialect.d.ts
vendored
@@ -17,6 +17,7 @@ export interface SqlDialect {
|
||||
defaultSchemaName?: string;
|
||||
enableConstraintsPerTable?: boolean;
|
||||
enableAllForeignKeys?: boolean;
|
||||
enableForeignKeyChecks?: boolean;
|
||||
requireStandaloneSelectForScopeIdentity?: boolean;
|
||||
allowMultipleValuesInsert?: boolean;
|
||||
|
||||
|
||||
16
packages/types/engines.d.ts
vendored
16
packages/types/engines.d.ts
vendored
@@ -41,6 +41,7 @@ export interface WriteTableOptions {
|
||||
createIfNotExists?: boolean;
|
||||
commitAfterInsert?: boolean;
|
||||
targetTableStructure?: TableInfo;
|
||||
progressName?: string;
|
||||
}
|
||||
|
||||
export interface EngineAuthType {
|
||||
@@ -144,6 +145,8 @@ export interface DatabaseHandle<TClient = any> {
|
||||
treeKeySeparator?: string;
|
||||
}
|
||||
|
||||
export type StreamResult = stream.Readable | (stream.Readable | stream.Writable)[];
|
||||
|
||||
export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
engine: string;
|
||||
title: string;
|
||||
@@ -183,6 +186,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
beforeConnectionSave?: (values: any) => any;
|
||||
databaseUrlPlaceholder?: string;
|
||||
defaultAuthTypeName?: string;
|
||||
authTypeFirst?: boolean;
|
||||
defaultLocalDataCenter?: string;
|
||||
defaultSocketPath?: string;
|
||||
authTypeLabel?: string;
|
||||
@@ -191,15 +195,11 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
close(dbhan: DatabaseHandle<TClient>): Promise<any>;
|
||||
query(dbhan: DatabaseHandle<TClient>, sql: string, options?: QueryOptions): Promise<QueryResult>;
|
||||
stream(dbhan: DatabaseHandle<TClient>, sql: string, options: StreamOptions);
|
||||
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<stream.Readable>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<stream.Readable>;
|
||||
readQuery(dbhan: DatabaseHandle<TClient>, sql: string, structure?: TableInfo): Promise<StreamResult>;
|
||||
readJsonQuery(dbhan: DatabaseHandle<TClient>, query: any, structure?: TableInfo): Promise<StreamResult>;
|
||||
// eg. PostgreSQL COPY FROM stdin
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<stream.Writable>;
|
||||
writeTable(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
name: NamedObjectInfo,
|
||||
options: WriteTableOptions
|
||||
): Promise<stream.Writable>;
|
||||
writeQueryFromStream(dbhan: DatabaseHandle<TClient>, sql: string): Promise<StreamResult>;
|
||||
writeTable(dbhan: DatabaseHandle<TClient>, name: NamedObjectInfo, options: WriteTableOptions): Promise<StreamResult>;
|
||||
analyseSingleObject(
|
||||
dbhan: DatabaseHandle<TClient>,
|
||||
name: NamedObjectInfo,
|
||||
|
||||
@@ -115,7 +115,6 @@
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
<div>Loading DbGate App</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@ant-design/colors": "^5.0.0",
|
||||
"@mdi/font": "^7.1.96",
|
||||
"@rollup/plugin-commonjs": "^20.0.0",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^13.0.5",
|
||||
"@rollup/plugin-replace": "^3.0.0",
|
||||
"@rollup/plugin-typescript": "^8.2.5",
|
||||
@@ -57,6 +58,7 @@
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@messageformat/core": "^3.4.0",
|
||||
"chartjs-plugin-zoom": "^1.2.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"debug": "^4.3.4",
|
||||
|
||||
@@ -8,6 +8,7 @@ import sveltePreprocess from 'svelte-preprocess';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import replace from '@rollup/plugin-replace';
|
||||
import css from 'rollup-plugin-css-only';
|
||||
import json from '@rollup/plugin-json';
|
||||
|
||||
const production = !process.env.ROLLUP_WATCH;
|
||||
|
||||
@@ -121,6 +122,7 @@ export default [
|
||||
sourceMap: !production,
|
||||
inlineSources: !production,
|
||||
}),
|
||||
json(),
|
||||
|
||||
// In dev mode, call `npm run start` once
|
||||
// the bundle has been generated
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import SettingsListener from './utility/SettingsListener.svelte';
|
||||
import { handleAuthOnStartup } from './clientAuth';
|
||||
import { initializeAppUpdates } from './utility/appUpdate';
|
||||
import { _t } from './translations';
|
||||
|
||||
export let isAdminPage = false;
|
||||
|
||||
@@ -95,10 +96,13 @@
|
||||
{:else}
|
||||
<AppStartInfo
|
||||
message={$loadingPluginStore.loadingPackageName
|
||||
? `Loading plugin ${$loadingPluginStore.loadingPackageName} ...`
|
||||
: 'Preparing plugins ...'}
|
||||
? _t('app.loading_plugin', {
|
||||
defaultMessage: `Loading plugin {plugin} ...`,
|
||||
values: { plugin: $loadingPluginStore.loadingPackageName },
|
||||
})
|
||||
: _t('app.preparingPlugins', { defaultMessage: 'Preparing plugins ...' })}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<AppStartInfo message="Starting DbGate" />
|
||||
<AppStartInfo message={_t('app.starting', { defaultMessage: 'Starting DbGate' })} />
|
||||
{/if}
|
||||
|
||||
@@ -67,7 +67,10 @@
|
||||
const count = getOpenedTabs().filter(closeCondition).length;
|
||||
if (count > 0) {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Closing connection will close ${count} opened tabs, continue?`,
|
||||
message: _t('connection.closeConfirm', {
|
||||
defaultMessage: 'Closing connection will close {count} opened tabs, continue?',
|
||||
values: { count },
|
||||
}),
|
||||
onConfirm: () => disconnectServerConnection(conid, false),
|
||||
});
|
||||
return;
|
||||
@@ -137,6 +140,7 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { switchCurrentDatabase } from '../utility/common';
|
||||
import { getConnectionClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -244,7 +248,10 @@
|
||||
};
|
||||
const handleDelete = () => {
|
||||
showModal(ConfirmModal, {
|
||||
message: `Really delete connection ${getConnectionLabel(data)}?`,
|
||||
message: _t('connection.deleteConfirm', {
|
||||
defaultMessage: 'Really delete connection {name}?',
|
||||
values: { name: getConnectionLabel(data) },
|
||||
}),
|
||||
onConfirm: () => apiCall('connections/delete', data),
|
||||
});
|
||||
};
|
||||
@@ -257,9 +264,9 @@
|
||||
};
|
||||
const handleCreateDatabase = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Create database',
|
||||
header: _t('connection.createDatabase', { defaultMessage: 'Create database' }),
|
||||
value: 'newdb',
|
||||
label: 'Database name',
|
||||
label: _t('connection.databaseName', { defaultMessage: 'Database name' }),
|
||||
onConfirm: name =>
|
||||
apiCall('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
@@ -294,12 +301,12 @@
|
||||
return [
|
||||
!data.singleDatabase && [
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Connect',
|
||||
text: _t('connection.connect', { defaultMessage: 'Connect' }),
|
||||
onClick: handleConnect,
|
||||
isBold: true,
|
||||
},
|
||||
$openedConnections.includes(data._id) && {
|
||||
text: 'Disconnect',
|
||||
text: _t('connection.disconnect', { defaultMessage: 'Disconnect' }),
|
||||
onClick: handleDisconnect,
|
||||
},
|
||||
],
|
||||
@@ -307,35 +314,41 @@
|
||||
config.runAsPortal == false &&
|
||||
!config.storageDatabase && [
|
||||
{
|
||||
text: $openedConnections.includes(data._id) ? 'View details' : 'Edit',
|
||||
text: $openedConnections.includes(data._id)
|
||||
? _t('connection.viewDetails', { defaultMessage: 'View details' })
|
||||
: _t('connection.edit', { defaultMessage: 'Edit' }),
|
||||
onClick: handleOpenConnectionTab,
|
||||
},
|
||||
!$openedConnections.includes(data._id) && {
|
||||
text: 'Delete',
|
||||
text: _t('connection.delete', { defaultMessage: 'Delete' }),
|
||||
onClick: handleDelete,
|
||||
},
|
||||
{
|
||||
text: 'Duplicate',
|
||||
text: _t('connection.duplicate', { defaultMessage: 'Duplicate' }),
|
||||
onClick: handleDuplicate,
|
||||
},
|
||||
],
|
||||
{ divider: true },
|
||||
!data.singleDatabase && [
|
||||
hasPermission(`dbops/query`) && { onClick: handleNewQuery, text: 'New Query (server)', isNewQuery: true },
|
||||
hasPermission(`dbops/query`) && {
|
||||
onClick: handleNewQuery,
|
||||
text: _t('connection.newQuery', { defaultMessage: 'New Query (server)' }),
|
||||
isNewQuery: true,
|
||||
},
|
||||
$openedConnections.includes(data._id) &&
|
||||
data.status && {
|
||||
text: 'Refresh',
|
||||
text: _t('connection.refresh', { defaultMessage: 'Refresh' }),
|
||||
onClick: handleRefresh,
|
||||
},
|
||||
hasPermission(`dbops/createdb`) &&
|
||||
$openedConnections.includes(data._id) &&
|
||||
driver?.supportedCreateDatabase &&
|
||||
!data.isReadOnly && {
|
||||
text: 'Create database',
|
||||
text: _t('connection.createDatabase', { defaultMessage: 'Create database' }),
|
||||
onClick: handleCreateDatabase,
|
||||
},
|
||||
driver?.supportsServerSummary && {
|
||||
text: 'Server summary',
|
||||
text: _t('connection.serverSummary', { defaultMessage: 'Server summary' }),
|
||||
onClick: handleServerSummary,
|
||||
},
|
||||
],
|
||||
@@ -352,7 +365,10 @@
|
||||
],
|
||||
|
||||
driver?.databaseEngineTypes?.includes('sql') &&
|
||||
!data.isReadOnly && { onClick: handleSqlRestore, text: 'Restore/import SQL dump' },
|
||||
!data.isReadOnly && {
|
||||
onClick: handleSqlRestore,
|
||||
text: _t('connection.sqlRestore', { defaultMessage: 'Restore/import SQL dump' }),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -365,7 +381,11 @@
|
||||
} else {
|
||||
extInfo = data.engine;
|
||||
engineStatusIcon = 'img warn';
|
||||
engineStatusTitle = `Engine driver ${data.engine} not found, review installed plugins and change engine in edit connection dialog`;
|
||||
engineStatusTitle = _t('connection.engineDriverNotFound', {
|
||||
defaultMessage:
|
||||
'Engine driver {engine} not found, review installed plugins and change engine in edit connection dialog',
|
||||
values: { engine: data.engine },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.refresh',
|
||||
category: 'Data grid',
|
||||
name: 'Refresh',
|
||||
name: _t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -28,7 +28,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.revertRowChanges',
|
||||
category: 'Data grid',
|
||||
name: 'Revert row changes',
|
||||
name: _t('command.datagrid.revertRowChanges', { defaultMessage: 'Revert row changes' }),
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertRowChanges(),
|
||||
@@ -37,8 +37,8 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.revertAllChanges',
|
||||
category: 'Data grid',
|
||||
name: 'Revert all changes',
|
||||
toolbarName: 'Revert all',
|
||||
name: _t('command.datagrid.revertAllChanges', { defaultMessage: 'Revert all changes' }),
|
||||
toolbarName: _t('command.datagrid.revertAllChanges.toolbar', { defaultMessage: 'Revert all' }),
|
||||
icon: 'icon undo',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertAllChanges(),
|
||||
@@ -47,8 +47,8 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.deleteSelectedRows',
|
||||
category: 'Data grid',
|
||||
name: 'Delete selected rows',
|
||||
toolbarName: 'Delete row(s)',
|
||||
name: _t('command.datagrid.deleteSelectedRows', { defaultMessage: 'Delete selected rows' }),
|
||||
toolbarName: _t('command.datagrid.deleteSelectedRows.toolbar', { defaultMessage: 'Delete row(s)' }),
|
||||
keyText: isMac() ? 'Command+Backspace' : 'CtrlOrCommand+Delete',
|
||||
icon: 'icon minus',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
@@ -58,8 +58,8 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.insertNewRow',
|
||||
category: 'Data grid',
|
||||
name: 'Insert new row',
|
||||
toolbarName: 'New row',
|
||||
name: _t('command.datagrid.insertNewRow', { defaultMessage: 'Insert new row' }),
|
||||
toolbarName: _t('command.datagrid.insertNewRow.toolbar', { defaultMessage: 'New row' }),
|
||||
icon: 'icon add',
|
||||
keyText: isMac() ? 'Command+I' : 'Insert',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
@@ -69,8 +69,8 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.addNewColumn',
|
||||
category: 'Data grid',
|
||||
name: 'Add new column',
|
||||
toolbarName: 'New column',
|
||||
name: _t('command.datagrid.addNewColumn', { defaultMessage: 'Add new column' }),
|
||||
toolbarName: _t('command.datagrid.addNewColumn.toolbar', { defaultMessage: 'New column' }),
|
||||
icon: 'icon add-column',
|
||||
testEnabled: () => getCurrentDataGrid()?.addNewColumnEnabled(),
|
||||
onClick: () => getCurrentDataGrid().addNewColumn(),
|
||||
@@ -79,8 +79,8 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.cloneRows',
|
||||
category: 'Data grid',
|
||||
name: 'Clone rows',
|
||||
toolbarName: 'Clone row(s)',
|
||||
name: _t('command.datagrid.cloneRows', { defaultMessage: 'Clone rows' }),
|
||||
toolbarName: _t('command.datagrid.cloneRows.toolbar', { defaultMessage: 'Clone row(s)' }),
|
||||
keyText: 'CtrlOrCommand+Shift+C',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
onClick: () => getCurrentDataGrid().cloneRows(),
|
||||
@@ -89,7 +89,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.setNull',
|
||||
category: 'Data grid',
|
||||
name: 'Set NULL',
|
||||
name: _t('command.datagrid.setNull', { defaultMessage: 'Set NULL' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () =>
|
||||
getCurrentDataGrid()?.getGrider()?.editable && !getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval,
|
||||
@@ -99,7 +99,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.removeField',
|
||||
category: 'Data grid',
|
||||
name: 'Remove field',
|
||||
name: _t('command.datagrid.removeField', { defaultMessage: 'Remove field' }),
|
||||
keyText: 'CtrlOrCommand+0',
|
||||
testEnabled: () =>
|
||||
getCurrentDataGrid()?.getGrider()?.editable && getCurrentDataGrid()?.getEditorTypes()?.supportFieldRemoval,
|
||||
@@ -109,7 +109,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.undo',
|
||||
category: 'Data grid',
|
||||
name: 'Undo',
|
||||
name: _t('command.datagrid.undo', { defaultMessage: 'Undo' }),
|
||||
group: 'undo',
|
||||
icon: 'icon undo',
|
||||
toolbar: true,
|
||||
@@ -121,7 +121,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.redo',
|
||||
category: 'Data grid',
|
||||
name: 'Redo',
|
||||
name: _t('command.datagrid.redo', { defaultMessage: 'Redo' }),
|
||||
group: 'redo',
|
||||
icon: 'icon redo',
|
||||
toolbar: true,
|
||||
@@ -133,7 +133,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.reconnect',
|
||||
category: 'Data grid',
|
||||
name: 'Reconnect',
|
||||
name: _t('command.datagrid.reconnect', { defaultMessage: 'Reconnect' }),
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().reconnect(),
|
||||
});
|
||||
@@ -141,7 +141,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.copyToClipboard',
|
||||
category: 'Data grid',
|
||||
name: 'Copy to clipboard',
|
||||
name: _t('command.datagrid.copyToClipboard', { defaultMessage: 'Copy to clipboard' }),
|
||||
keyText: 'CtrlOrCommand+C',
|
||||
disableHandleKeyText: 'CtrlOrCommand+C',
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
@@ -152,7 +152,7 @@
|
||||
id: 'dataGrid.editJsonDocument',
|
||||
category: 'Data grid',
|
||||
keyText: 'CtrlOrCommand+J',
|
||||
name: 'Edit row as JSON document',
|
||||
name: _t('command.datagrid.editJsonDocument', { defaultMessage: 'Edit row as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.editJsonEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editJsonDocument(),
|
||||
});
|
||||
@@ -160,15 +160,15 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.openSelectionInMap',
|
||||
category: 'Data grid',
|
||||
name: 'Open selection in map',
|
||||
testEnabled: () => getCurrentDataGrid() != null, // ?.openSelectionInMapEnabled(),
|
||||
name: _t('command.datagrid.openSelectionInMap', { defaultMessage: 'Open selection in map' }),
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().openSelectionInMap(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonDocument',
|
||||
category: 'Data grid',
|
||||
name: 'View row as JSON document',
|
||||
name: _t('command.datagrid.viewJsonDocument', { defaultMessage: 'View row as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.viewJsonDocumentEnabled(),
|
||||
onClick: () => getCurrentDataGrid().viewJsonDocument(),
|
||||
});
|
||||
@@ -176,7 +176,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.viewJsonValue',
|
||||
category: 'Data grid',
|
||||
name: 'View cell as JSON document',
|
||||
name: _t('command.datagrid.viewJsonValue', { defaultMessage: 'View cell as JSON document' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.viewJsonValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().viewJsonValue(),
|
||||
});
|
||||
@@ -184,7 +184,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.openJsonArrayInSheet',
|
||||
category: 'Data grid',
|
||||
name: 'Open array as table',
|
||||
name: _t('command.datagrid.openJsonArrayInSheet', { defaultMessage: 'Open array as table' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.openJsonArrayInSheetEnabled(),
|
||||
onClick: () => getCurrentDataGrid().openJsonArrayInSheet(),
|
||||
});
|
||||
@@ -192,7 +192,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.saveCellToFile',
|
||||
category: 'Data grid',
|
||||
name: 'Save cell to file',
|
||||
name: _t('command.datagrid.saveCellToFile', { defaultMessage: 'Save cell to file' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.saveCellToFileEnabled(),
|
||||
onClick: () => getCurrentDataGrid().saveCellToFile(),
|
||||
});
|
||||
@@ -200,7 +200,7 @@
|
||||
registerCommand({
|
||||
id: 'dataGrid.loadCellFromFile',
|
||||
category: 'Data grid',
|
||||
name: 'Load cell from file',
|
||||
name: _t('command.datagrid.loadCellFromFile', { defaultMessage: 'Load cell from file' }),
|
||||
testEnabled: () => getCurrentDataGrid()?.loadCellFromFileEnabled(),
|
||||
onClick: () => getCurrentDataGrid().loadCellFromFile(),
|
||||
});
|
||||
@@ -212,7 +212,8 @@
|
||||
// testEnabled: () => getCurrentDataGrid()?.copyJsonEnabled(),
|
||||
// onClick: () => getCurrentDataGrid().copyJsonDocument(),
|
||||
// });
|
||||
|
||||
//
|
||||
//
|
||||
registerCommand({
|
||||
id: 'dataGrid.filterSelected',
|
||||
category: 'Data grid',
|
||||
@@ -221,7 +222,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.getDisplay().filterable,
|
||||
onClick: () => getCurrentDataGrid().filterSelectedValue(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.findColumn',
|
||||
category: 'Data grid',
|
||||
@@ -230,7 +230,6 @@
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
getSubCommands: () => getCurrentDataGrid().buildFindMenu(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.hideColumn',
|
||||
category: 'Data grid',
|
||||
@@ -239,7 +238,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.canShowLeftPanel(),
|
||||
onClick: () => getCurrentDataGrid().hideColumn(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.clearFilter',
|
||||
category: 'Data grid',
|
||||
@@ -248,7 +246,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.clearFilterEnabled(),
|
||||
onClick: () => getCurrentDataGrid().clearFilter(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.generateSqlFromData',
|
||||
category: 'Data grid',
|
||||
@@ -257,7 +254,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.generateSqlFromDataEnabled(),
|
||||
onClick: () => getCurrentDataGrid().generateSqlFromData(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openFreeTable',
|
||||
category: 'Data grid',
|
||||
@@ -265,7 +261,6 @@
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().openFreeTable(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.openChartFromSelection',
|
||||
category: 'Data grid',
|
||||
@@ -273,7 +268,6 @@
|
||||
testEnabled: () => getCurrentDataGrid() != null,
|
||||
onClick: () => getCurrentDataGrid().openChartFromSelection(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.newJson',
|
||||
category: 'Data grid',
|
||||
@@ -281,7 +275,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.addJsonDocumentEnabled(),
|
||||
onClick: () => getCurrentDataGrid().addJsonDocument(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.editCellValue',
|
||||
category: 'Data grid',
|
||||
@@ -289,7 +282,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.editCellValueEnabled(),
|
||||
onClick: () => getCurrentDataGrid().editCellValue(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.mergeSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -297,7 +289,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: false }),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.mergeSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -305,7 +296,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'merge', fullRows: true }),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.appendSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -313,7 +303,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: false }),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.appendSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -321,7 +310,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'append', fullRows: true }),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.replaceSelectedCellsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -329,7 +317,6 @@
|
||||
testEnabled: () => getCurrentDataGrid()?.mirrorWriteEnabled(true),
|
||||
onClick: () => getCurrentDataGrid().mergeSelectionIntoMirror({ mergeMode: 'replace', fullRows: false }),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.replaceSelectedRowsIntoMirror',
|
||||
category: 'Data grid',
|
||||
@@ -430,6 +417,7 @@
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
import contextMenuActivator from '../utility/contextMenuActivator';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let onLoadNextData = undefined;
|
||||
export let grider = undefined;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let title;
|
||||
export let fieldDefinitions;
|
||||
@@ -42,7 +43,7 @@
|
||||
<FormSelectField
|
||||
isNative
|
||||
name="schemaName"
|
||||
label="Schema"
|
||||
label={_t('common.schema', { defaultMessage: 'Schema' })}
|
||||
options={schemaList.map(x => ({ label: x.schemaName, value: x.schemaName }))}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
export let columns: TableControlColumn[];
|
||||
export let columns: (TableControlColumn | false)[];
|
||||
export let rows;
|
||||
export let focusOnCreate = false;
|
||||
export let selectable = false;
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
registerCommand({
|
||||
id: 'dataForm.refresh',
|
||||
category: 'Data form',
|
||||
name: 'Refresh',
|
||||
name: _t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -197,6 +197,7 @@
|
||||
import resizeObserver from '../utility/resizeObserver';
|
||||
import openReferenceForm from './openReferenceForm';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
|
||||
@@ -149,6 +149,7 @@
|
||||
'icon download': 'mdi mdi-download',
|
||||
'icon text': 'mdi mdi-text',
|
||||
'icon ai': 'mdi mdi-head-lightbulb',
|
||||
'icon wait': 'mdi mdi-timer-sand',
|
||||
|
||||
'icon run': 'mdi mdi-play',
|
||||
'icon chevron-down': 'mdi mdi-chevron-down',
|
||||
|
||||
@@ -76,11 +76,13 @@
|
||||
import { compositeDbNameIfNeeded } from 'dbgate-tools';
|
||||
import createRef from '../utility/createRef';
|
||||
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
|
||||
// export let uploadedFile = undefined;
|
||||
// export let openedFile = undefined;
|
||||
export let previewReaderStore;
|
||||
export let isTabActive;
|
||||
export let isRunning = false;
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
|
||||
@@ -104,6 +106,7 @@
|
||||
$: sourceList = $values.sourceList;
|
||||
|
||||
let targetEditKey = 0;
|
||||
export let progressHolder = null;
|
||||
|
||||
const previewSource = writable(null);
|
||||
|
||||
@@ -211,92 +214,141 @@
|
||||
<div class="title"><FontIcon icon="icon tables" /> Map source tables/files</div>
|
||||
|
||||
{#key targetEditKey}
|
||||
<TableControl
|
||||
rows={$values.sourceList || []}
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'source',
|
||||
header: 'Source',
|
||||
component: SourceName,
|
||||
getProps: row => ({ name: row }),
|
||||
},
|
||||
{
|
||||
fieldName: 'action',
|
||||
header: 'Action',
|
||||
component: SourceAction,
|
||||
getProps: row => ({ name: row, targetDbinfo }),
|
||||
},
|
||||
{
|
||||
fieldName: 'target',
|
||||
header: 'Target',
|
||||
slot: 1,
|
||||
},
|
||||
{
|
||||
fieldName: 'preview',
|
||||
header: 'Preview',
|
||||
slot: 0,
|
||||
},
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: 'Columns',
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
{#if supportsPreview}
|
||||
<CheckboxField
|
||||
checked={$previewSource == row}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
if (e.target.checked) $previewSource = row;
|
||||
else $previewSource = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<div class="flex">
|
||||
<TextField
|
||||
value={getTargetName($extensions, row, $values)}
|
||||
on:input={e =>
|
||||
setFieldValue(
|
||||
`targetName_${row}`,
|
||||
{#key progressHolder}
|
||||
<TableControl
|
||||
rows={$values.sourceList || []}
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'source',
|
||||
header: 'Source',
|
||||
component: SourceName,
|
||||
getProps: row => ({ name: row }),
|
||||
},
|
||||
{
|
||||
fieldName: 'action',
|
||||
header: 'Action',
|
||||
component: SourceAction,
|
||||
getProps: row => ({ name: row, targetDbinfo }),
|
||||
},
|
||||
{
|
||||
fieldName: 'target',
|
||||
header: 'Target',
|
||||
slot: 1,
|
||||
},
|
||||
supportsPreview && {
|
||||
fieldName: 'preview',
|
||||
header: 'Preview',
|
||||
slot: 0,
|
||||
},
|
||||
!!progressHolder && {
|
||||
fieldName: 'status',
|
||||
header: 'Status',
|
||||
slot: 3,
|
||||
},
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: 'Columns',
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="0" let:row>
|
||||
{#if supportsPreview}
|
||||
<CheckboxField
|
||||
checked={$previewSource == row}
|
||||
on:change={e => {
|
||||
// @ts-ignore
|
||||
e.target.value
|
||||
)}
|
||||
/>
|
||||
{#if $targetDbinfo}
|
||||
<DropDownButton
|
||||
menu={() => {
|
||||
return $targetDbinfo.tables.map(opt => ({
|
||||
text: opt.pureName,
|
||||
onClick: () => {
|
||||
setFieldValue(`targetName_${row}`, opt.pureName);
|
||||
targetEditKey += 1;
|
||||
},
|
||||
}));
|
||||
if (e.target.checked) $previewSource = row;
|
||||
else $previewSource = null;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
{@const columnCount = ($values[`columns_${row}`] || []).filter(x => !x.skip).length}
|
||||
<Link
|
||||
onClick={() => {
|
||||
const targetNameLower = ($values[`targetName_${row}`] || row)?.toLowerCase();
|
||||
showModal(ColumnMapModal, {
|
||||
initialValue: $values[`columns_${row}`],
|
||||
sourceTableInfo: $sourceDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == row?.toLowerCase()),
|
||||
targetTableInfo: $targetDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == targetNameLower),
|
||||
onConfirm: value => setFieldValue(`columns_${row}`, value),
|
||||
});
|
||||
}}
|
||||
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="1" let:row>
|
||||
<div class="flex">
|
||||
<TextField
|
||||
value={getTargetName($extensions, row, $values)}
|
||||
on:input={e =>
|
||||
setFieldValue(
|
||||
`targetName_${row}`,
|
||||
// @ts-ignore
|
||||
e.target.value
|
||||
)}
|
||||
data-testid={`ImportExportConfigurator_targetName_${row}`}
|
||||
/>
|
||||
{#if $targetDbinfo}
|
||||
<DropDownButton
|
||||
menu={() => {
|
||||
return $targetDbinfo.tables.map(opt => ({
|
||||
text: opt.pureName,
|
||||
onClick: () => {
|
||||
setFieldValue(`targetName_${row}`, opt.pureName);
|
||||
targetEditKey += 1;
|
||||
},
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2" let:row>
|
||||
{@const columnCount = ($values[`columns_${row}`] || []).filter(x => !x.skip).length}
|
||||
<Link
|
||||
onClick={() => {
|
||||
const targetNameLower = ($values[`targetName_${row}`] || row)?.toLowerCase();
|
||||
showModal(ColumnMapModal, {
|
||||
initialValue: $values[`columns_${row}`],
|
||||
sourceTableInfo: $sourceDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == row?.toLowerCase()),
|
||||
targetTableInfo: $targetDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == targetNameLower),
|
||||
onConfirm: value => setFieldValue(`columns_${row}`, value),
|
||||
});
|
||||
}}
|
||||
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}
|
||||
</Link>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="3" let:row>
|
||||
{#if progressHolder[row]?.status == 'running' && isRunning}
|
||||
<FontIcon icon="icon loading" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows writtem
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows read
|
||||
{:else}
|
||||
Running
|
||||
{/if}
|
||||
{:else if progressHolder[row]?.status == 'error'}
|
||||
<FontIcon icon="img error" /> Error
|
||||
{#if progressHolder[row]?.errorMessage}
|
||||
<FontIcon
|
||||
icon="img info"
|
||||
title={progressHolder[row]?.errorMessage}
|
||||
on:click={() => showModal(ErrorMessageModal, { message: progressHolder[row]?.errorMessage })}
|
||||
style="cursor: pointer"
|
||||
data-testid={`ImportExportConfigurator_errorInfoIcon_${row}`}
|
||||
/>
|
||||
{/if}
|
||||
{:else if progressHolder[row]?.status == 'done'}
|
||||
<FontIcon icon="img ok" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows written
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows written
|
||||
{:else}
|
||||
Done
|
||||
{/if}
|
||||
{:else}
|
||||
<FontIcon icon="icon wait" />
|
||||
{#if progressHolder[row]?.writtenRowCount}
|
||||
{progressHolder[row]?.writtenRowCount} rows writtem
|
||||
{:else if progressHolder[row]?.readRowCount}
|
||||
{progressHolder[row]?.readRowCount} rows read
|
||||
{:else}
|
||||
Queued
|
||||
{/if}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
{/key}
|
||||
{/key}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import FormTablesSelect from './FormTablesSelect.svelte';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import AceEditor from '../query/AceEditor.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let direction;
|
||||
export let storageTypeField;
|
||||
@@ -40,14 +41,22 @@
|
||||
$values[storageTypeField] == 'jsldata'
|
||||
? [{ value: 'jsldata', label: 'Query result data', directions: ['source'] }]
|
||||
: [
|
||||
{ value: 'database', label: 'Database', directions: ['source', 'target'] },
|
||||
{
|
||||
value: 'database',
|
||||
label: _t('common.database', { defaultMessage: 'Database' }),
|
||||
directions: ['source', 'target'],
|
||||
},
|
||||
...$extensions.fileFormats.map(format => ({
|
||||
value: format.storageType,
|
||||
label: `${format.name} files(s)`,
|
||||
directions: getFileFormatDirections(format),
|
||||
})),
|
||||
{ value: 'query', label: 'Query', directions: ['source'] },
|
||||
{ value: 'archive', label: 'Archive', directions: ['source', 'target'] },
|
||||
{ value: 'query', label: _t('common.query', { defaultMessage: 'Query' }), directions: ['source'] },
|
||||
{
|
||||
value: 'archive',
|
||||
label: _t('common.archive', { defaultMessage: 'Archive' }),
|
||||
directions: ['source', 'target'],
|
||||
},
|
||||
];
|
||||
|
||||
$: storageType = $values[storageTypeField];
|
||||
@@ -124,7 +133,7 @@
|
||||
conidName={connectionIdField}
|
||||
databaseName={databaseNameField}
|
||||
name={schemaNameField}
|
||||
label="Schema"
|
||||
label={_t('common.schema', { defaultMessage: 'Schema' })}
|
||||
/>
|
||||
{#if tablesField}
|
||||
<FormTablesSelect
|
||||
@@ -132,12 +141,12 @@
|
||||
schemaName={schemaNameField}
|
||||
databaseName={databaseNameField}
|
||||
name={tablesField}
|
||||
label="Tables / views / collections"
|
||||
label={_t('importExport.tablesViewsCollections', { defaultMessage: 'Tables / views / collections' })}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if storageType == 'query'}
|
||||
<div class="label">Query</div>
|
||||
<div class="label">{_t('common.query', { defaultMessage: 'Query' })}</div>
|
||||
<div class="sqlwrap">
|
||||
{#if $values.sourceQueryType == 'json'}
|
||||
<AceEditor value={$values.sourceQuery} on:input={e => setFieldValue('sourceQuery', e.detail)} mode="json" />
|
||||
@@ -156,7 +165,11 @@
|
||||
{/if}
|
||||
|
||||
{#if storageType == 'archive' && direction == 'source'}
|
||||
<FormArchiveFilesSelect label="Source files" folderName={$values[archiveFolderField]} name={tablesField} />
|
||||
<FormArchiveFilesSelect
|
||||
label={_t('importExport.sourceFiles', { defaultMessage: 'Source files' })}
|
||||
folderName={$values[archiveFolderField]}
|
||||
name={tablesField}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if format && direction == 'source'}
|
||||
|
||||
@@ -164,6 +164,7 @@ function getTargetExpr(extensions, sourceName, values, targetConnection, targetD
|
||||
pureName: getTargetName(extensions, sourceName, values),
|
||||
...extractDriverApiParameters(values, 'target', targetDriver),
|
||||
...getFlagsFroAction(values[`actionType_${sourceName}`]),
|
||||
progressName: sourceName,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -233,7 +234,7 @@ export default async function createImpExpScript(extensions, values, forceScript
|
||||
script.assignValue(colmapVar, colmap);
|
||||
}
|
||||
|
||||
script.copyStream(sourceVar, targetVar, colmapVar);
|
||||
script.copyStream(sourceVar, targetVar, colmapVar, sourceName);
|
||||
script.endLine();
|
||||
}
|
||||
return script.getScript(values.schedule);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal, showModal } from './modalTools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let onSave;
|
||||
export let json;
|
||||
@@ -43,7 +44,7 @@
|
||||
|
||||
<div slot="footer">
|
||||
<FormStyledButton
|
||||
value="Save"
|
||||
value={_t('common.save', { defaultMessage: 'Save' })}
|
||||
data-testid="EditJsonModal_saveButton"
|
||||
on:click={() => {
|
||||
try {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<div class="icon">
|
||||
<FontIcon icon="img error" />
|
||||
</div>
|
||||
<div>
|
||||
<div data-testid="ErrorMessageModal_message">
|
||||
{message}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import { currentArchive } from '../stores';
|
||||
import { _t } from '../translations';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
|
||||
@@ -28,7 +29,7 @@
|
||||
<FormTextField label="File name" name="file" />
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit value="Save" on:click={handleSubmit} />
|
||||
<FormSubmit value={_t('common.save', { defaultMessage: 'Save' })} on:click={handleSubmit} />
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import { _t } from '../translations';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
import getElectron from '../utility/getElectron';
|
||||
@@ -56,7 +57,7 @@
|
||||
<svelte:fragment slot="header">Save file</svelte:fragment>
|
||||
<FormTextField label="File name" name="name" focused />
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit value="Save" on:click={handleSubmit} />
|
||||
<FormSubmit value={_t('common.save', { defaultMessage: 'Save' })} on:click={handleSubmit} />
|
||||
{#if electron}
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
// $: console.log('MESSAGE ROWS', items);
|
||||
const values = writable({
|
||||
hideDebug: false,
|
||||
hideDebug: true,
|
||||
hideInfo: false,
|
||||
hideError: false,
|
||||
});
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import FormDropDownTextField from '../forms/FormDropDownTextField.svelte';
|
||||
import { getConnectionLabel } from 'dbgate-tools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let getDatabaseList;
|
||||
export let currentConnection;
|
||||
@@ -153,6 +154,15 @@
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if driver?.showConnectionField('authToken', $values, showConnectionFieldArgs)}
|
||||
<FormTextField
|
||||
label={_t('authToken', { defaultMessage: 'Auth token' })}
|
||||
name="authToken"
|
||||
data-testid="ConnectionDriverFields_authToken"
|
||||
disabled={isConnected || disabledFields.includes('authToken')}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if $authTypes && driver?.showConnectionField('authType', $values, showConnectionFieldArgs)}
|
||||
{#key $authTypes}
|
||||
<FormSelectField
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import ModalBase from '../modals/ModalBase.svelte';
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import { closeCurrentModal, showModal } from '../modals/modalTools';
|
||||
import { EDITOR_KEYBINDINGS_MODES, EDITOR_THEMES, FONT_SIZES } from '../query/AceEditor.svelte';
|
||||
import SqlEditor from '../query/SqlEditor.svelte';
|
||||
import {
|
||||
@@ -39,6 +39,9 @@
|
||||
import { derived } from 'svelte/store';
|
||||
import { safeFormatDate } from 'dbgate-tools';
|
||||
import FormDefaultActionField from './FormDefaultActionField.svelte';
|
||||
import { _t, getSelectedLanguage } from '../translations';
|
||||
import { internalRedirectTo } from '../clientAuth';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
|
||||
const electron = getElectron();
|
||||
let restartWarning = false;
|
||||
@@ -121,7 +124,32 @@ ORDER BY
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FormCheckboxField name="tabGroup.showServerName" label="Show server name alongside database name in title of the tab group" defaultValue={false} />
|
||||
<FormCheckboxField
|
||||
name="tabGroup.showServerName"
|
||||
label="Show server name alongside database name in title of the tab group"
|
||||
defaultValue={false}
|
||||
/>
|
||||
<div class="heading">{_t('settings.localization', { defaultMessage: 'Localization' })}</div>
|
||||
<FormSelectField
|
||||
label="Language"
|
||||
name="localization.language"
|
||||
defaultValue={getSelectedLanguage()}
|
||||
isNative
|
||||
options={[
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'cs', label: 'Czech' },
|
||||
]}
|
||||
on:change={() => {
|
||||
showModal(ConfirmModal, {
|
||||
message: 'Application will be reloaded to apply new language settings',
|
||||
onConfirm: () => {
|
||||
setTimeout(() => {
|
||||
internalRedirectTo('/');
|
||||
}, 100);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="heading">Data grid</div>
|
||||
<FormTextField
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import DataTypeEditor from './DataTypeEditor.svelte';
|
||||
import { editorAddColumn, editorDeleteColumn, editorModifyColumn, fillEditorColumnInfo } from 'dbgate-tools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let columnInfo;
|
||||
export let setTableInfo = null;
|
||||
@@ -75,7 +76,7 @@
|
||||
{#if !columnInfo}
|
||||
<FormButton
|
||||
type="button"
|
||||
value="Save"
|
||||
value={_t('common.save', { defaultMessage: 'Save' })}
|
||||
disabled={isReadOnly}
|
||||
on:click={e => {
|
||||
closeCurrentModal();
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import _ from 'lodash';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let constraintInfo;
|
||||
export let setTableInfo;
|
||||
@@ -204,7 +205,7 @@
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value="Save"
|
||||
value={_t('common.save', { defaultMessage: 'Save' })}
|
||||
disabled={isReadOnly}
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
import { _t } from '../translations';
|
||||
|
||||
const getCurrentValueMarker: any = {};
|
||||
|
||||
export function shouldShowTab(tab, lockedDbModeArg = getCurrentValueMarker, currentDbArg = getCurrentValueMarker) {
|
||||
@@ -184,8 +186,8 @@
|
||||
};
|
||||
|
||||
function getTabDbName(tab, connectionList) {
|
||||
if (tab.tabComponent == 'ConnectionTab') return 'Connections';
|
||||
if (tab.tabComponent?.startsWith('Admin')) return 'Administration';
|
||||
if (tab.tabComponent == 'ConnectionTab') return _t('common.connections', { defaultMessage: 'Connections' });
|
||||
if (tab.tabComponent?.startsWith('Admin')) return _t('tab.administration', { defaultMessage: 'Administration' });
|
||||
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
|
||||
if (tab.props && tab.props.conid) {
|
||||
const connection = connectionList?.find(x => x._id == tab.props.conid);
|
||||
@@ -232,7 +234,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.nextTab',
|
||||
category: 'Tabs',
|
||||
name: 'Next tab',
|
||||
name: _t('command.tabs.nextTab', { defaultMessage: 'Next tab' }),
|
||||
keyText: 'Ctrl+Tab',
|
||||
testEnabled: () => getOpenedTabs().filter(x => !x.closedTime).length >= 2,
|
||||
onClick: () => switchTabByOrder(false),
|
||||
@@ -241,7 +243,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.previousTab',
|
||||
category: 'Tabs',
|
||||
name: 'Previous tab',
|
||||
name: _t('command.tabs.previousTab', { defaultMessage: 'Previous tab' }),
|
||||
keyText: 'Ctrl+Shift+Tab',
|
||||
testEnabled: () => getOpenedTabs().filter(x => !x.closedTime).length >= 2,
|
||||
onClick: () => switchTabByOrder(true),
|
||||
@@ -250,7 +252,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.closeAll',
|
||||
category: 'Tabs',
|
||||
name: 'Close all tabs',
|
||||
name: _t('command.tabs.closeAll', { defaultMessage: 'Close all tabs' }),
|
||||
testEnabled: () => getOpenedTabs().filter(x => !x.closedTime).length >= 1,
|
||||
onClick: closeAll,
|
||||
});
|
||||
@@ -258,7 +260,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.closeTab',
|
||||
category: 'Tabs',
|
||||
name: 'Close tab',
|
||||
name: _t('command.tabs.closeTab', { defaultMessage: 'Close tab' }),
|
||||
keyText: isElectronAvailable() ? 'CtrlOrCommand+W' : 'CtrlOrCommand+Shift+W',
|
||||
testEnabled: () => {
|
||||
const hasAnyOtherTab = getOpenedTabs().filter(x => !x.closedTime).length >= 1;
|
||||
@@ -272,7 +274,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.closeTabsWithCurrentDb',
|
||||
category: 'Tabs',
|
||||
name: 'Close tabs with current DB',
|
||||
name: _t('command.tabs.closeTabsWithCurrentDb', { defaultMessage: 'Close tabs with current DB' }),
|
||||
testEnabled: () => getOpenedTabs().filter(x => !x.closedTime).length >= 1 && !!getCurrentDatabase(),
|
||||
onClick: closeTabsWithCurrentDb,
|
||||
});
|
||||
@@ -280,7 +282,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.closeTabsButCurrentDb',
|
||||
category: 'Tabs',
|
||||
name: 'Close tabs but current DB',
|
||||
name: _t('command.tabs.closeTabsButCurrentDb', { defaultMessage: 'Close tabs but current DB' }),
|
||||
testEnabled: () => getOpenedTabs().filter(x => !x.closedTime).length >= 1 && !!getCurrentDatabase(),
|
||||
onClick: closeTabsButCurrentDb,
|
||||
});
|
||||
@@ -288,7 +290,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.reopenClosedTab',
|
||||
category: 'Tabs',
|
||||
name: 'Reopen closed tab',
|
||||
name: _t('command.tabs.reopenClosedTab', { defaultMessage: 'Reopen closed tab' }),
|
||||
keyText: 'CtrlOrCommand+Shift+T',
|
||||
testEnabled: () => getOpenedTabs().filter(x => x.closedTime).length >= 1,
|
||||
onClick: reopenClosedTab,
|
||||
@@ -297,7 +299,7 @@
|
||||
registerCommand({
|
||||
id: 'tabs.addToFavorites',
|
||||
category: 'Tabs',
|
||||
name: 'Add current tab to favorites',
|
||||
name: _t('command.tabs.addToFavorites', { defaultMessage: 'Add current tab to favorites' }),
|
||||
// icon: 'icon favorite',
|
||||
// toolbar: true,
|
||||
testEnabled: () =>
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { useConfig } from '../utility/metadataLoaders';
|
||||
import ConnectionAdvancedDriverFields from '../settings/ConnectionAdvancedDriverFields.svelte';
|
||||
import DatabaseLoginModal from '../modals/DatabaseLoginModal.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let connection;
|
||||
export let tabid;
|
||||
@@ -289,7 +290,11 @@
|
||||
{:else}
|
||||
<FormButton value="Test" on:click={() => handleTest(false)} data-testid="ConnectionTab_buttonTest" />
|
||||
{/if}
|
||||
<FormButton value="Save" on:click={handleSave} data-testid="ConnectionTab_buttonSave" />
|
||||
<FormButton
|
||||
value={_t('common.save', { defaultMessage: 'Save' })}
|
||||
on:click={handleSave}
|
||||
data-testid="ConnectionTab_buttonSave"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="test-result">
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
import { changeTab } from '../utility/common';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import DbKeyValueDetail from '../dbkeyvalue/DbKeyValueDetail.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@@ -134,12 +135,16 @@
|
||||
</div>
|
||||
<FormStyledButton value={`TTL:${keyInfo.ttl}`} on:click={() => handleChangeTtl(keyInfo)} />
|
||||
{#if keyInfo.type == 'string'}
|
||||
<FormStyledButton value="Save" on:click={saveString} disabled={!editedValue} />
|
||||
<FormStyledButton
|
||||
value={_t('common.save', { defaultMessage: 'Save' })}
|
||||
on:click={saveString}
|
||||
disabled={!editedValue}
|
||||
/>
|
||||
{/if}
|
||||
{#if keyInfo.keyType?.addMethod && keyInfo.keyType?.showItemList}
|
||||
<FormStyledButton value="Add item" on:click={() => addItem(keyInfo)} />
|
||||
{/if}
|
||||
<FormStyledButton value="Refresh" on:click={refresh} />
|
||||
<FormStyledButton value={_t('common.refresh', { defaultMessage: 'Refresh' })} on:click={refresh} />
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
export let savedFile;
|
||||
export let savedFilePath;
|
||||
|
||||
let progressHolder = null;
|
||||
const refreshArchiveFolderRef = createRef(null);
|
||||
|
||||
const formValues = writable({});
|
||||
@@ -179,6 +180,7 @@
|
||||
|
||||
const handleExecute = async e => {
|
||||
if (busy) return;
|
||||
progressHolder = {};
|
||||
const values = $formValues as any;
|
||||
busy = true;
|
||||
const script = await createImpExpScript($extensions, values);
|
||||
@@ -228,6 +230,29 @@
|
||||
title: `${getSourceTargetTitle('source', values)}->${getSourceTargetTitle('target', values)}(${values.sourceList?.length || 0})`,
|
||||
}));
|
||||
}
|
||||
|
||||
const handleProgress = progress => {
|
||||
progressHolder = {
|
||||
...progressHolder,
|
||||
[progress.progressName]: {
|
||||
...progressHolder[progress.progressName],
|
||||
...progress,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
$: progressEffect = useEffect(() => {
|
||||
if (runnerId) {
|
||||
const eventName = `runner-progress-${runnerId}`;
|
||||
apiOn(eventName, handleProgress);
|
||||
return () => {
|
||||
apiOff(eventName, handleProgress);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
});
|
||||
|
||||
$progressEffect;
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
@@ -237,7 +262,9 @@
|
||||
<ImportExportConfigurator
|
||||
bind:this={domConfigurator}
|
||||
{previewReaderStore}
|
||||
{progressHolder}
|
||||
isTabActive={tabid == $activeTabId}
|
||||
isRunning={busy}
|
||||
/>
|
||||
|
||||
{#if busy}
|
||||
@@ -247,7 +274,12 @@
|
||||
|
||||
<svelte:fragment slot="2">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Output files" name="output" height="20%">
|
||||
<WidgetColumnBarItem
|
||||
title="Output files"
|
||||
name="output"
|
||||
height="20%"
|
||||
data-testid="ImportExportTab_outputFiles"
|
||||
>
|
||||
<RunnerOutputFiles {runnerId} {executeNumber} />
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Messages" name="messages">
|
||||
@@ -258,7 +290,12 @@
|
||||
showCaller
|
||||
/>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Preview" name="preview" skip={!$previewReaderStore}>
|
||||
<WidgetColumnBarItem
|
||||
title="Preview"
|
||||
name="preview"
|
||||
skip={!$previewReaderStore}
|
||||
data-testid="ImportExportTab_preview"
|
||||
>
|
||||
<PreviewDataGrid reader={$previewReaderStore} />
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Advanced configuration" name="config" collapsed>
|
||||
@@ -271,11 +308,17 @@
|
||||
</FormProviderCore>
|
||||
<svelte:fragment slot="toolstrip">
|
||||
{#if busy}
|
||||
<ToolStripButton icon="icon stop" on:click={handleCancel}>Stop</ToolStripButton>
|
||||
<ToolStripButton icon="icon stop" on:click={handleCancel} data-testid="ImportExportTab_stopButton"
|
||||
>Stop</ToolStripButton
|
||||
>
|
||||
{:else}
|
||||
<ToolStripButton on:click={handleExecute} icon="icon run">Run</ToolStripButton>
|
||||
<ToolStripButton on:click={handleExecute} icon="icon run" data-testid="ImportExportTab_executeButton"
|
||||
>Run</ToolStripButton
|
||||
>
|
||||
{/if}
|
||||
<ToolStripButton icon="img shell" on:click={handleGenerateScript}>Generate script</ToolStripButton>
|
||||
<ToolStripButton icon="img shell" on:click={handleGenerateScript} data-testid="ImportExportTab_generateScriptButton"
|
||||
>Generate script</ToolStripButton
|
||||
>
|
||||
<ToolStripSaveButton idPrefix="job" />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
@@ -522,19 +522,22 @@
|
||||
|
||||
async function handleKeyDown(event) {
|
||||
if (isProApp()) {
|
||||
if (event.code == 'Space' && event.shiftKey && !isAiAssistantVisible) {
|
||||
if (event.code == 'Space' && event.shiftKey && event.ctrlKey && !isAiAssistantVisible) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleAiAssistant();
|
||||
await sleep(100);
|
||||
if (domAiAssistant) {
|
||||
domAiAssistant.handleCompleteOnCursor();
|
||||
domEditor?.getEditor()?.focus();
|
||||
}
|
||||
} else if (event.code == 'Space' && event.shiftKey && isAiAssistantVisible && domAiAssistant) {
|
||||
} else if (event.code == 'Space' && event.shiftKey && event.ctrlKey && isAiAssistantVisible && domAiAssistant) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
domAiAssistant.handleCompleteOnCursor();
|
||||
} else if (event.code?.startsWith('Digit') && event.altKey && isAiAssistantVisible && domAiAssistant) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
domAiAssistant.insertCompletion(parseInt(event.code.substring(5)) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
registerCommand({
|
||||
id: 'serverSummary.refresh',
|
||||
category: 'Server sumnmary',
|
||||
name: 'Refresh',
|
||||
name: _t('common.refresh', { defaultMessage: 'Refresh' }),
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
@@ -22,6 +22,7 @@
|
||||
import LoadingInfo from '../elements/LoadingInfo.svelte';
|
||||
|
||||
import ObjectListControl from '../elements/ObjectListControl.svelte';
|
||||
import { _t } from '../translations';
|
||||
import { apiCall } from '../utility/api';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import formatFileSize from '../utility/formatFileSize';
|
||||
|
||||
69
packages/web/src/translations.ts
Normal file
69
packages/web/src/translations.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import cs from '../../../translations/cs.json';
|
||||
|
||||
import MessageFormat, { MessageFunction } from '@messageformat/core';
|
||||
import { getStringSettingsValue } from './settings/settingsTools';
|
||||
|
||||
const translations = {
|
||||
en: {},
|
||||
cs,
|
||||
};
|
||||
const supportedLanguages = Object.keys(translations);
|
||||
|
||||
const compiledMessages: Partial<Record<string, Record<string, MessageFunction<'string'>>>> = {};
|
||||
|
||||
const defaultLanguage = 'en';
|
||||
|
||||
export function getSelectedLanguage(): string {
|
||||
const borwserLanguage = getBrowserLanguage();
|
||||
const selectedLanguage = getStringSettingsValue('localization.language', borwserLanguage);
|
||||
|
||||
if (!supportedLanguages.includes(selectedLanguage)) return defaultLanguage;
|
||||
|
||||
return selectedLanguage;
|
||||
}
|
||||
|
||||
export function getBrowserLanguage(): string {
|
||||
if (typeof window !== 'undefined') {
|
||||
return (
|
||||
(navigator.languages && navigator.languages[0]).slice(0, 2) || navigator.language.slice(0, 2) || defaultLanguage
|
||||
);
|
||||
}
|
||||
return defaultLanguage;
|
||||
}
|
||||
|
||||
type TranslateOptions = {
|
||||
defaultMessage: string;
|
||||
values?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
function getTranslation(key: string, defaultMessage: string, language: string) {
|
||||
const selectedTranslations = translations[language] ?? {};
|
||||
const translation = selectedTranslations[key];
|
||||
|
||||
if (!translation) {
|
||||
console.warn(`Translation not found for key: ${key}. For language: ${language}`);
|
||||
return defaultMessage;
|
||||
}
|
||||
|
||||
return translation;
|
||||
}
|
||||
|
||||
export function _t(key: string, options: TranslateOptions): string {
|
||||
const { defaultMessage, values } = options;
|
||||
|
||||
const selectedLanguage = getSelectedLanguage();
|
||||
|
||||
if (!compiledMessages[selectedLanguage]) {
|
||||
compiledMessages[selectedLanguage] = {};
|
||||
}
|
||||
|
||||
if (!compiledMessages[selectedLanguage][key]) {
|
||||
const translation = getTranslation(key, defaultMessage, selectedLanguage);
|
||||
const complied = new MessageFormat(selectedLanguage).compile(translation);
|
||||
compiledMessages[selectedLanguage][key] = complied;
|
||||
}
|
||||
|
||||
const compliledTranslation = compiledMessages[selectedLanguage][key];
|
||||
|
||||
return compliledTranslation(values ?? {});
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
import { ScriptWriter, ScriptWriterJson } from 'dbgate-tools';
|
||||
import getElectron from './getElectron';
|
||||
import { showSnackbar, showSnackbarInfo, showSnackbarError, closeSnackbar } from '../utility/snackbar';
|
||||
import {
|
||||
showSnackbar,
|
||||
showSnackbarInfo,
|
||||
showSnackbarError,
|
||||
closeSnackbar,
|
||||
updateSnackbarProgressMessage,
|
||||
} from '../utility/snackbar';
|
||||
import resolveApi, { resolveApiHeaders } from './resolveApi';
|
||||
import { apiCall, apiOff, apiOn } from './api';
|
||||
import { normalizeExportColumnMap } from '../impexp/createImpExpScript';
|
||||
@@ -70,9 +76,17 @@ async function runImportExportScript({ script, runningMessage, canceledMessage,
|
||||
],
|
||||
});
|
||||
|
||||
function handleRunnerProgress(data) {
|
||||
const rows = data.writtenRowsCount || data.readRowCount;
|
||||
if (rows) {
|
||||
updateSnackbarProgressMessage(snackId, `${rows} rows processed`);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRunnerDone() {
|
||||
closeSnackbar(snackId);
|
||||
apiOff(`runner-done-${runid}`, handleRunnerDone);
|
||||
apiOff(`runner-progress-${runid}`, handleRunnerProgress);
|
||||
if (isCanceled) {
|
||||
showSnackbarError(canceledMessage);
|
||||
} else {
|
||||
@@ -82,6 +96,7 @@ async function runImportExportScript({ script, runningMessage, canceledMessage,
|
||||
}
|
||||
|
||||
apiOn(`runner-done-${runid}`, handleRunnerDone);
|
||||
apiOn(`runner-progress-${runid}`, handleRunnerProgress);
|
||||
}
|
||||
|
||||
export async function saveExportedFile(filters, defaultPath, extension, dataName, getScript: (filaPath: string) => {}) {
|
||||
@@ -141,7 +156,7 @@ function generateQuickExportScript(
|
||||
script.assignValue(colmapVar, colmap);
|
||||
}
|
||||
|
||||
script.copyStream(sourceVar, targetVar, colmapVar);
|
||||
script.copyStream(sourceVar, targetVar, colmapVar, 'data');
|
||||
script.endLine();
|
||||
|
||||
return script.getScript();
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface SnackbarButton {
|
||||
|
||||
export interface SnackbarInfo {
|
||||
message: string;
|
||||
progressMessage?: string;
|
||||
icon?: string;
|
||||
autoClose?: boolean;
|
||||
allowClose?: boolean;
|
||||
@@ -59,6 +60,11 @@ export function showSnackbarError(message: string) {
|
||||
export function closeSnackbar(snackId: string) {
|
||||
openedSnackbars.update(x => x.filter(x => x.id != snackId));
|
||||
}
|
||||
|
||||
export function updateSnackbarProgressMessage(snackId: string, progressMessage: string) {
|
||||
openedSnackbars.update(x => x.map(x => (x.id === snackId ? { ...x, progressMessage } : x)));
|
||||
}
|
||||
|
||||
// showSnackbar({
|
||||
// icon: 'img ok',
|
||||
// message: 'Test snackbar',
|
||||
|
||||
@@ -248,6 +248,7 @@
|
||||
handleDropOnGroup(data, '');
|
||||
}
|
||||
}}
|
||||
data-testid="ConnectionList_container"
|
||||
>
|
||||
<AppObjectListHandler
|
||||
bind:this={domListHandler}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import SingleConnectionDatabaseList from './SingleConnectionDatabaseList.svelte';
|
||||
import _ from 'lodash';
|
||||
import FocusedConnectionInfoWidget from './FocusedConnectionInfoWidget.svelte';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let hidden = false;
|
||||
let domSqlObjectList = null;
|
||||
@@ -29,16 +30,26 @@
|
||||
|
||||
<WidgetColumnBar {hidden}>
|
||||
{#if $config?.singleConnection}
|
||||
<WidgetColumnBarItem title="Databases" name="databases" height="35%" storageName="databasesWidget">
|
||||
<WidgetColumnBarItem
|
||||
title={_t('widget.databases', { defaultMessage: 'Databases' })}
|
||||
name="databases"
|
||||
height="35%"
|
||||
storageName="databasesWidget"
|
||||
>
|
||||
<SingleConnectionDatabaseList connection={$config?.singleConnection} />
|
||||
</WidgetColumnBarItem>
|
||||
{:else if !$config?.singleDbConnection}
|
||||
<WidgetColumnBarItem title="Connections" name="connections" height="35%" storageName="connectionsWidget">
|
||||
<WidgetColumnBarItem
|
||||
title={_t('common.connections', { defaultMessage: 'Connections' })}
|
||||
name="connections"
|
||||
height="35%"
|
||||
storageName="connectionsWidget"
|
||||
>
|
||||
<ConnectionList passProps={{ onFocusSqlObjectList: () => domSqlObjectList.focus() }} />
|
||||
</WidgetColumnBarItem>
|
||||
{/if}
|
||||
<WidgetColumnBarItem
|
||||
title="Pinned"
|
||||
title={_t('widget.pinned', { defaultMessage: 'Pinned' })}
|
||||
name="pinned"
|
||||
height="15%"
|
||||
storageName="pinnedItemsWidget"
|
||||
@@ -51,7 +62,7 @@
|
||||
<WidgetColumnBarItem
|
||||
title={driver?.databaseEngineTypes?.includes('document')
|
||||
? (driver?.collectionPluralLabel ?? 'Collections/containers')
|
||||
: 'Tables, views, functions'}
|
||||
: _t('widget.tablesViewsFunctions', { defaultMessage: 'Tables, views, functions' })}
|
||||
name="dbObjects"
|
||||
storageName="dbObjectsWidget"
|
||||
skip={!(
|
||||
@@ -64,7 +75,7 @@
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title={'Keys'}
|
||||
title={_t('widget.keys', { defaultMessage: 'Keys' })}
|
||||
name="dbObjects"
|
||||
storageName="dbObjectsWidget"
|
||||
skip={!(conid && (database || singleDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'))}
|
||||
@@ -73,7 +84,7 @@
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="Database content"
|
||||
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
||||
name="dbObjects"
|
||||
storageName="dbObjectsWidget"
|
||||
skip={conid && (database || singleDatabase)}
|
||||
@@ -86,7 +97,7 @@
|
||||
</WidgetColumnBarItem>
|
||||
|
||||
<WidgetColumnBarItem
|
||||
title="Database content"
|
||||
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
|
||||
name="dbObjects"
|
||||
storageName="dbObjectsWidget"
|
||||
skip={!(conid && (database || singleDatabase) && !driver)}
|
||||
@@ -94,7 +105,9 @@
|
||||
<WidgetsInnerContainer>
|
||||
<FocusedConnectionInfoWidget {conid} {database} connection={$connection} />
|
||||
|
||||
<ErrorInfo message="Invalid database connection, driver not found" />
|
||||
<ErrorInfo
|
||||
message={_t('error.driverNotFound', { defaultMessage: 'Invalid database connection, driver not found' })}
|
||||
/>
|
||||
</WidgetsInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { useFavorites } from '../utility/metadataLoaders';
|
||||
import { _t } from '../translations';
|
||||
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
import { appliedCurrentSchema, currentDatabase } from '../stores';
|
||||
import { switchCurrentDatabase } from '../utility/common';
|
||||
import { extractDbNameFromComposite, extractSchemaNameFromComposite, findDefaultSchema } from 'dbgate-tools';
|
||||
import { _t } from '../translations';
|
||||
|
||||
export let schemaList;
|
||||
export let objectList;
|
||||
@@ -58,9 +59,9 @@
|
||||
|
||||
function handleCreateSchema() {
|
||||
showModal(InputTextModal, {
|
||||
header: 'Create schema',
|
||||
header: _t('schema.createSchema', { defaultMessage: 'Create schema' }),
|
||||
value: 'newschema',
|
||||
label: 'Schema name',
|
||||
label: _t('schema.schemaName', { defaultMessage: 'Schema name' }),
|
||||
onConfirm: async name => {
|
||||
const dbid = { conid, database };
|
||||
await runOperationOnDatabase(
|
||||
@@ -105,13 +106,19 @@
|
||||
|
||||
{#if realSchemaList.length > 0}
|
||||
<div class="wrapper" class:negativeMarginTop>
|
||||
<div class="mr-1">Schema:</div>
|
||||
<div class="mr-1">{_t('common.schema', { defaultMessage: 'Schema' })}:</div>
|
||||
<SelectField
|
||||
isNative
|
||||
options={connection?.useSeparateSchemas
|
||||
? (schemaList?.map(x => ({ label: x.schemaName, value: x.schemaName })) ?? [])
|
||||
: [
|
||||
{ label: `All schemas (${objectList?.length ?? 0})`, value: '' },
|
||||
{
|
||||
label: _t('schema.all_schemas', {
|
||||
defaultMessage: 'All schemas ({count})',
|
||||
values: { count: objectList?.length ?? 0 },
|
||||
}),
|
||||
value: '',
|
||||
},
|
||||
...realSchemaList.map(x => ({ label: `${x} (${countBySchema[x] ?? 0})`, value: x })),
|
||||
]}
|
||||
value={selectedSchema ?? $appliedCurrentSchema ?? ''}
|
||||
@@ -135,15 +142,20 @@
|
||||
selectedSchema = null;
|
||||
localStorage.removeItem(valueStorageKey);
|
||||
}}
|
||||
title="Reset to default"
|
||||
title={_t('schema.resetToDefault', { defaultMessage: 'Reset to default' })}
|
||||
>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
{/if}
|
||||
<InlineButton on:click={handleCreateSchema} title="Add new schema" square>
|
||||
<InlineButton on:click={handleCreateSchema} title={_t('schema.add', { defaultMessage: 'Add new schema' })} square>
|
||||
<FontIcon icon="icon plus-thick" />
|
||||
</InlineButton>
|
||||
<InlineButton on:click={handleDropSchema} title="Delete schema" square disabled={!$appliedCurrentSchema}>
|
||||
<InlineButton
|
||||
on:click={handleDropSchema}
|
||||
title={_t('schema.delete', { defaultMessage: 'Delete schema' })}
|
||||
square
|
||||
disabled={!$appliedCurrentSchema}
|
||||
>
|
||||
<FontIcon icon="icon minus-thick" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
export let autoClose = false;
|
||||
export let allowClose = false;
|
||||
export let buttons = [];
|
||||
export let progressMessage = null;
|
||||
|
||||
function handleClose() {
|
||||
openedSnackbars.update(x => x.filter(x => x.id != id));
|
||||
@@ -25,6 +26,11 @@
|
||||
<FontIcon {icon} />
|
||||
{message}
|
||||
</div>
|
||||
{#if progressMessage}
|
||||
<div class="progress-message">
|
||||
{progressMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if allowClose}
|
||||
<div class="close" on:click={handleClose}>
|
||||
@@ -83,4 +89,10 @@
|
||||
.button {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.progress-message {
|
||||
color: var(--theme-font-3);
|
||||
margin: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -76,7 +76,11 @@
|
||||
>
|
||||
|
||||
{#if visible}
|
||||
<div class="wrapper" style={$dynamicProps.splitterVisible ? `height:${size}px` : 'flex: 1 1 0'}>
|
||||
<div
|
||||
class="wrapper"
|
||||
style={$dynamicProps.splitterVisible ? `height:${size}px` : 'flex: 1 1 0'}
|
||||
data-testid={$$props['data-testid'] ? `${$$props['data-testid']}_content` : undefined}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"exclude": ["node_modules/*", "public/*"],
|
||||
|
||||
"compilerOptions": {
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
@@ -12,7 +13,7 @@
|
||||
"noImplicitAny": false,
|
||||
"strictNullChecks": false,
|
||||
"strict": false,
|
||||
"target": "es6",
|
||||
"target": "es6"
|
||||
// "allowJs": true,
|
||||
// "checkJs": true,
|
||||
}
|
||||
|
||||
@@ -44,9 +44,17 @@ const driver = {
|
||||
analyserClass: Analyser,
|
||||
// creating connection
|
||||
async connect({ server, user, password, database, localDataCenter, useDatabaseUrl, databaseUrl }) {
|
||||
let credentials;
|
||||
|
||||
if (user && password) {
|
||||
credentials = {
|
||||
username: user,
|
||||
password,
|
||||
}
|
||||
}
|
||||
|
||||
const client = new cassandra.Client({
|
||||
// user,
|
||||
// password,
|
||||
credentials,
|
||||
contactPoints: server.split(','),
|
||||
localDataCenter: localDataCenter ?? this.defaultLocalDataCenter,
|
||||
keyspace: database,
|
||||
|
||||
@@ -95,9 +95,10 @@ async function reader({ fileName, encoding = 'utf-8', header = true, delimiter,
|
||||
});
|
||||
const fileStream = fs.createReadStream(downloadedFile, encoding);
|
||||
const csvPrepare = new CsvPrepareStream({ header });
|
||||
fileStream.pipe(csvStream);
|
||||
csvStream.pipe(csvPrepare);
|
||||
return csvPrepare;
|
||||
return [fileStream, csvStream, csvPrepare];
|
||||
// fileStream.pipe(csvStream);
|
||||
// csvStream.pipe(csvPrepare);
|
||||
// return csvPrepare;
|
||||
}
|
||||
|
||||
reader.initialize = (dbgateEnv) => {
|
||||
|
||||
@@ -31,11 +31,13 @@ async function writer({ fileName, encoding = 'utf-8', header = true, delimiter,
|
||||
const csvPrepare = new CsvPrepareStream({ header });
|
||||
const csvStream = csv.stringify({ delimiter, quoted });
|
||||
const fileStream = fs.createWriteStream(fileName, encoding);
|
||||
csvPrepare.pipe(csvStream);
|
||||
csvStream.pipe(fileStream);
|
||||
csvPrepare['finisher'] = fileStream;
|
||||
// csvPrepare.pipe(csvStream);
|
||||
// csvStream.pipe(fileStream);
|
||||
// csvPrepare['finisher'] = fileStream;
|
||||
csvPrepare.requireFixedStructure = true;
|
||||
return csvPrepare;
|
||||
|
||||
return [csvPrepare, csvStream, fileStream];
|
||||
// return csvPrepare;
|
||||
}
|
||||
|
||||
module.exports = writer;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
const { getLogger } = global.DBGATE_PACKAGES['dbgate-tools'];
|
||||
const { getLogger, extractErrorLogData } = global.DBGATE_PACKAGES['dbgate-tools'];
|
||||
const { EJSON } = require('bson');
|
||||
|
||||
const logger = getLogger('mongoBulkInsert');
|
||||
|
||||
|
||||
function createBulkInsertStream(driver, stream, dbhan, name, options) {
|
||||
const collectionName = name.pureName;
|
||||
const db = dbhan.getDatabase();
|
||||
@@ -31,21 +30,31 @@ function createBulkInsertStream(driver, stream, dbhan, name, options) {
|
||||
};
|
||||
|
||||
writable.checkStructure = async () => {
|
||||
if (options.dropIfExists) {
|
||||
logger.info(`Dropping collection ${collectionName}`);
|
||||
await db.collection(collectionName).drop();
|
||||
}
|
||||
if (options.truncate) {
|
||||
logger.info(`Truncating collection ${collectionName}`);
|
||||
await db.collection(collectionName).deleteMany({});
|
||||
try {
|
||||
if (options.dropIfExists) {
|
||||
logger.info(`Dropping collection ${collectionName}`);
|
||||
await db.collection(collectionName).drop();
|
||||
}
|
||||
if (options.truncate) {
|
||||
logger.info(`Truncating collection ${collectionName}`);
|
||||
await db.collection(collectionName).deleteMany({});
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error during preparing mongo bulk insert collection, stopped');
|
||||
writable.destroy(err);
|
||||
}
|
||||
};
|
||||
|
||||
writable.send = async () => {
|
||||
const rows = writable.buffer;
|
||||
writable.buffer = [];
|
||||
try {
|
||||
const rows = writable.buffer;
|
||||
writable.buffer = [];
|
||||
|
||||
await db.collection(collectionName).insertMany(rows);
|
||||
await db.collection(collectionName).insertMany(rows);
|
||||
} catch (err) {
|
||||
logger.error(extractErrorLogData(err), 'Error bulk insert collection, stopped');
|
||||
writable.destroy(err);
|
||||
}
|
||||
};
|
||||
|
||||
writable.sendIfFull = async () => {
|
||||
|
||||
@@ -266,6 +266,11 @@ const driver = {
|
||||
pass.write(transformMongoData(row));
|
||||
});
|
||||
|
||||
// propagate error
|
||||
cursorStream.on('error', (err) => {
|
||||
pass.emit('error', err);
|
||||
});
|
||||
|
||||
// Called once the cursor is fully read
|
||||
cursorStream.on('end', () => {
|
||||
pass.emit('end');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user