mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-18 00:56:02 +00:00
Merge branch 'master' into feature/translation4
This commit is contained in:
2
.github/workflows/build-app-pro-beta.yaml
vendored
2
.github/workflows/build-app-pro-beta.yaml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- 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
@@ -43,7 +43,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- 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: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- 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: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-npm-pro.yaml
vendored
2
.github/workflows/build-npm-pro.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- 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: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -113,6 +113,18 @@ describe('Add connection', () => {
|
||||
cy.contains('performance_schema');
|
||||
});
|
||||
|
||||
it('Plugin tab', () => {
|
||||
cy.testid('WidgetIconPanel_menu').click();
|
||||
cy.contains('Tools').click();
|
||||
cy.contains('Manage plugins').click();
|
||||
cy.contains('dbgate-plugin-theme-total-white').click();
|
||||
// text from plugin markdown
|
||||
cy.contains('Total white theme');
|
||||
// wait for load logos
|
||||
cy.wait(2000);
|
||||
cy.themeshot('view-plugin-tab');
|
||||
});
|
||||
|
||||
it('export connections', () => {
|
||||
cy.testid('WidgetIconPanel_menu').click();
|
||||
cy.contains('Tools').click();
|
||||
|
||||
@@ -310,17 +310,6 @@ describe('Data browser data', () => {
|
||||
cy.themeshot('search-in-connections');
|
||||
});
|
||||
|
||||
it('Plugin tab', () => {
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Manage plugins').click();
|
||||
cy.contains('dbgate-plugin-theme-total-white').click();
|
||||
// text from plugin markdown
|
||||
cy.contains('Total white theme');
|
||||
// wait for load logos
|
||||
cy.wait(2000);
|
||||
cy.themeshot('view-plugin-tab');
|
||||
});
|
||||
|
||||
it('Edit mongo data JSON', () => {
|
||||
// TODO FIX: Missing button+ctx menu Revert all changes, missing button+ctx menu add document
|
||||
// TODO: Dark theme - not visible changed and deleted document
|
||||
|
||||
@@ -163,41 +163,35 @@ describe('Charts', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Settings').click();
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('Deutsch');
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Einstellungen').click();
|
||||
cy.contains('Lokalisierung');
|
||||
cy.contains('Sprache');
|
||||
cy.themeshot('switch-language-de');
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('Français');
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Paramètres').click();
|
||||
cy.contains('Localisation');
|
||||
cy.contains('Langue');
|
||||
cy.themeshot('switch-language-fr');
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('Español');
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Configuración').click();
|
||||
cy.contains('Localización');
|
||||
cy.contains('Idioma');
|
||||
cy.themeshot('switch-language-es');
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('Čeština');
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('Nastavení').click();
|
||||
cy.contains('Lokalizace');
|
||||
cy.contains('Jazyk');
|
||||
cy.themeshot('switch-language-cs');
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('中文');
|
||||
cy.testid('ConfirmModal_okButton').click();
|
||||
cy.testid('WidgetIconPanel_settings').click();
|
||||
cy.contains('设置').click();
|
||||
cy.contains('本地化');
|
||||
cy.contains('语言');
|
||||
cy.themeshot('switch-language-zh');
|
||||
|
||||
cy.testid('SettingsModal_languageSelect').select('English');
|
||||
|
||||
@@ -103,13 +103,70 @@ describe('Transactions', () => {
|
||||
|
||||
describe('Backup table', () => {
|
||||
multiTest({ skipMongo: true }, (connectionName, databaseName, engine, options = {}) => {
|
||||
const implicitTransactions = options.implicitTransactions ?? false;
|
||||
|
||||
cy.contains(connectionName).click();
|
||||
if (databaseName) cy.contains(databaseName).click();
|
||||
cy.contains('customers').rightclick();
|
||||
cy.contains('addresses').rightclick();
|
||||
cy.contains('Create table backup').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('_customers').click();
|
||||
cy.contains('Rows: 8').should('be.visible');
|
||||
cy.testid('app-object-group-items-table-backups').contains('addresses').click();
|
||||
cy.contains('Rows: 12').should('be.visible');
|
||||
cy.testid('app-object-group-items-tables').contains('addresses').click();
|
||||
|
||||
cy.contains('Ridgewood').click();
|
||||
cy.testid('TableDataTab_deleteSelectedRows').click();
|
||||
cy.contains('Rosewood').click();
|
||||
cy.testid('TableDataTab_deleteSelectedRows').click();
|
||||
|
||||
cy.contains('Vermont').click();
|
||||
cy.get('body').realType('Wermont{enter}');
|
||||
|
||||
cy.testid('TableDataTab_insertNewRow').click();
|
||||
cy.get('body').realType('Modranska{enter}');
|
||||
cy.realPress(['ArrowLeft']);
|
||||
cy.realPress(['ArrowLeft']);
|
||||
cy.get('body').realType('13{enter}');
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.get('body').realType('1{enter}');
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.get('body').realType('Prague{enter}');
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.get('body').realType('CZ{enter}');
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.get('body').realType('10000{enter}');
|
||||
cy.realPress(['ArrowRight']);
|
||||
cy.get('body').realType('111222333{enter}');
|
||||
|
||||
cy.testid('TableDataTab_save').click();
|
||||
cy.testid('ConfirmSqlModal_okButton').click();
|
||||
cy.contains('Rows: 11').should('be.visible'); // wait for save
|
||||
|
||||
cy.testid('app-object-group-items-table-backups').contains('addresses').rightclick();
|
||||
cy.contains('restore script').click();
|
||||
cy.contains('UPDATE'); // wait for query
|
||||
cy.testid('QueryTab_executeButton').click();
|
||||
cy.contains('Query execution finished');
|
||||
|
||||
if (implicitTransactions) {
|
||||
cy.testid('QueryTab_commitTransactionButton').click();
|
||||
cy.contains('Commit Transaction finished');
|
||||
}
|
||||
|
||||
cy.realPress('F1');
|
||||
cy.realType('Close all');
|
||||
cy.realPress('Enter');
|
||||
// cy.testid('CloseTabModal_buttonConfirm').click();
|
||||
cy.wait(1000);
|
||||
|
||||
cy.testid('app-object-group-items-tables').contains('addresses').click();
|
||||
|
||||
// check whether data was successfully restored
|
||||
cy.contains('Rows: 12').should('be.visible');
|
||||
cy.contains('Ridgewood');
|
||||
cy.contains('Vermont');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -146,13 +203,14 @@ describe('Import CSV', () => {
|
||||
cy.contains('Import').click();
|
||||
|
||||
cy.get('input[type=file]').selectFile('cypress/fixtures/customers-20.csv', { force: true });
|
||||
cy.contains('customers-20');
|
||||
cy.testid('ImportExportConfigurator_tableMappingSection').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('ImportExportConfigurator_tableMappingSection').contains('20 rows written').should('be.visible');
|
||||
|
||||
cy.testid('SqlObjectList_refreshButton').click();
|
||||
cy.contains('Refresh DB structure (incremental)').click();
|
||||
cy.testid('SqlObjectList_container').contains('customers-20').click();
|
||||
cy.contains('Rows: 20').should('be.visible');
|
||||
|
||||
|
||||
@@ -28,12 +28,14 @@ describe('Schema tests', () => {
|
||||
const count = schemas1.length;
|
||||
expect(structure1.tables.length).toEqual(2);
|
||||
await runCommandOnDriver(conn, driver, dmp => dmp.createSchema('myschema'));
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
const schemas2 = await driver.listSchemas(conn);
|
||||
expect(schemas2.find(x => x.schemaName == 'myschema')).toBeTruthy();
|
||||
expect(schemas2.length).toEqual(count + 1);
|
||||
expect(schemas2.find(x => x.isDefault).schemaName).toEqual(engine.defaultSchemaName);
|
||||
expect(structure2).toBeNull();
|
||||
if (!engine.skipIncrementalAnalysis) {
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
const schemas2 = await driver.listSchemas(conn);
|
||||
expect(schemas2.find(x => x.schemaName == 'myschema')).toBeTruthy();
|
||||
expect(schemas2.length).toEqual(count + 1);
|
||||
expect(schemas2.find(x => x.isDefault).schemaName).toEqual(engine.defaultSchemaName);
|
||||
expect(structure2).toBeNull();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -48,10 +50,12 @@ describe('Schema tests', () => {
|
||||
expect(schemas1.find(x => x.schemaName == 'myschema')).toBeTruthy();
|
||||
expect(structure1.tables.length).toEqual(2);
|
||||
await runCommandOnDriver(conn, driver, dmp => dmp.dropSchema('myschema'));
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
const schemas2 = await driver.listSchemas(conn);
|
||||
expect(schemas2.find(x => x.schemaName == 'myschema')).toBeFalsy();
|
||||
expect(structure2).toBeNull();
|
||||
if (!engine.skipIncrementalAnalysis) {
|
||||
const structure2 = await driver.analyseIncremental(conn, structure1);
|
||||
const schemas2 = await driver.listSchemas(conn);
|
||||
expect(schemas2.find(x => x.schemaName == 'myschema')).toBeFalsy();
|
||||
expect(structure2).toBeNull();
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "6.7.2-alpha.1",
|
||||
"version": "6.7.2-premium-beta.4",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Command, Select, Update, Delete, Insert } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { dumpSqlFromDefinition, dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlFromDefinition, dumpSqlSourceDef, dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlCondition } from './dumpSqlCondition';
|
||||
|
||||
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||
@@ -115,7 +115,10 @@ export function dumpSqlInsert(dmp: SqlDumper, cmd: Insert) {
|
||||
cmd.fields.map(x => x.targetColumn)
|
||||
);
|
||||
dmp.putCollection(',', cmd.fields, x => dumpSqlExpression(dmp, x));
|
||||
if (dmp.dialect.requireFromDual) {
|
||||
if (cmd.whereNotExistsSource) {
|
||||
dmp.put(' ^from ');
|
||||
dumpSqlSourceDef(dmp, cmd.whereNotExistsSource);
|
||||
} else if (dmp.dialect.requireFromDual) {
|
||||
dmp.put(' ^from ^dual ');
|
||||
}
|
||||
dmp.put(' ^where ^not ^exists (^select * ^from %f ^where ', cmd.targetTable);
|
||||
|
||||
@@ -2,6 +2,7 @@ import _ from 'lodash';
|
||||
import type { SqlDumper } from 'dbgate-types';
|
||||
import { Expression, ColumnRefExpression } from './types';
|
||||
import { dumpSqlSourceRef } from './dumpSqlSource';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
|
||||
export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
switch (expr.exprType) {
|
||||
@@ -67,5 +68,11 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
});
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
case 'select':
|
||||
dmp.put('(');
|
||||
dumpSqlSelect(dmp, expr.select);
|
||||
dmp.put(')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface Insert {
|
||||
fields: UpdateField[];
|
||||
targetTable: NamedObjectInfo;
|
||||
insertWhereNotExistsCondition?: Condition;
|
||||
whereNotExistsSource?: Source;
|
||||
}
|
||||
|
||||
export interface AllowIdentityInsert {
|
||||
@@ -226,6 +227,11 @@ export interface RowNumberExpression {
|
||||
orderBy: OrderByExpression[];
|
||||
}
|
||||
|
||||
export interface SelectExpression {
|
||||
exprType: 'select';
|
||||
select: Select;
|
||||
}
|
||||
|
||||
export type Expression =
|
||||
| ColumnRefExpression
|
||||
| ValueExpression
|
||||
@@ -235,7 +241,8 @@ export type Expression =
|
||||
| CallExpression
|
||||
| MethodCallExpression
|
||||
| TranformExpression
|
||||
| RowNumberExpression;
|
||||
| RowNumberExpression
|
||||
| SelectExpression;
|
||||
export type OrderByExpression = Expression & { direction: 'ASC' | 'DESC' };
|
||||
|
||||
export type ResultField = Expression & { alias?: string };
|
||||
|
||||
1
packages/types/dumper.d.ts
vendored
1
packages/types/dumper.d.ts
vendored
@@ -16,6 +16,7 @@ export interface SqlDumper extends AlterProcessor {
|
||||
transform(type: TransformType, dumpExpr: () => void);
|
||||
createDatabase(name: string);
|
||||
dropDatabase(name: string);
|
||||
comment(value: string);
|
||||
|
||||
callableTemplate(func: CallableObjectInfo);
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div on:drop={handleDrop}>
|
||||
<div on:drop={handleDrop} data-testid={`app-object-group-items-${_.kebabCase(group)}`}>
|
||||
{#each items as item}
|
||||
<AppObjectListItem
|
||||
isHidden={!item.isMatched}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts" context="module">
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { _t, _tval, DefferedTranslationResult } from '../translations';
|
||||
import sqlFormatter from 'sql-formatter';
|
||||
|
||||
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
|
||||
export const createMatcher =
|
||||
@@ -88,7 +89,8 @@
|
||||
isRename?: boolean;
|
||||
isTruncate?: boolean;
|
||||
isCopyTableName?: boolean;
|
||||
isDuplicateTable?: boolean;
|
||||
isTableBackup?: boolean;
|
||||
isTableRestore?: boolean;
|
||||
isDiagram?: boolean;
|
||||
functionName?: string;
|
||||
isExport?: boolean;
|
||||
@@ -106,6 +108,8 @@
|
||||
}
|
||||
|
||||
function createMenusCore(objectTypeField, driver, data): DbObjMenuItem[] {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
switch (objectTypeField) {
|
||||
case 'tables':
|
||||
return [
|
||||
@@ -175,11 +179,18 @@
|
||||
isCopyTableName: true,
|
||||
requiresWriteAccess: false,
|
||||
},
|
||||
hasPermission('dbops/table/backup') && {
|
||||
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
||||
isDuplicateTable: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/backup') &&
|
||||
!backupMatch && {
|
||||
label: _t('dbObject.createTableBackup', { defaultMessage: 'Create table backup' }),
|
||||
isTableBackup: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/table/restore') &&
|
||||
backupMatch && {
|
||||
label: _t('dbObject.createRestoreScript', { defaultMessage: 'Create restore script' }),
|
||||
isTableRestore: true,
|
||||
requiresWriteAccess: true,
|
||||
},
|
||||
hasPermission('dbops/model/view') && {
|
||||
label: _t('dbObject.showDiagram', { defaultMessage: 'Show diagram' }),
|
||||
isDiagram: true,
|
||||
@@ -637,7 +648,7 @@
|
||||
});
|
||||
},
|
||||
});
|
||||
} else if (menu.isDuplicateTable) {
|
||||
} else if (menu.isTableBackup) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const newTable = _.cloneDeep(data);
|
||||
@@ -671,6 +682,25 @@
|
||||
},
|
||||
engine: driver.engine,
|
||||
});
|
||||
} else if (menu.isTableRestore) {
|
||||
const backupMatch = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
const db = await getDatabaseInfo(data);
|
||||
if (db) {
|
||||
const originalTable = db?.tables?.find(x => x.pureName == backupMatch[1] && x.schemaName == data.schemaName);
|
||||
if (originalTable) {
|
||||
createTableRestoreScript(data, originalTable, dmp);
|
||||
newQuery({
|
||||
title: _t('dbObject.restoreScript', {
|
||||
defaultMessage: 'Restore {name} #',
|
||||
values: { name: backupMatch[1] },
|
||||
}),
|
||||
initialData: sqlFormatter.format(dmp.s),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (menu.isImport) {
|
||||
const { conid, database } = data;
|
||||
openImportExportTab({
|
||||
@@ -724,9 +754,8 @@
|
||||
const coreMenus = createMenusCore(objectTypeField, driver, data);
|
||||
|
||||
const filteredSumenus = coreMenus.map(item => {
|
||||
if (!item) return item;
|
||||
if (!item.submenu) {
|
||||
if (!item) return item;
|
||||
|
||||
return { ...item, label: _tval(item.label) };
|
||||
}
|
||||
return {
|
||||
@@ -743,7 +772,7 @@
|
||||
};
|
||||
});
|
||||
|
||||
const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
|
||||
const filteredNoEmptySubmenus = _.compact(filteredSumenus).filter(x => !x.submenu || x.submenu.length > 0);
|
||||
|
||||
return filteredNoEmptySubmenus;
|
||||
}
|
||||
@@ -1008,6 +1037,8 @@
|
||||
|
||||
return handleDatabaseObjectClick(data, { forceNewTab, tabPreviewMode, focusTab });
|
||||
}
|
||||
|
||||
export const TABLE_BACKUP_REGEX = /^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/;
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@@ -1025,7 +1056,7 @@
|
||||
} from '../stores';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { extractDbNameFromComposite, filterNameCompoud, getConnectionLabel } from 'dbgate-tools';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import { getConnectionInfo, getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
@@ -1047,6 +1078,8 @@
|
||||
import { getBoolSettingsValue, getOpenDetailOnArrowsSettings } from '../settings/settingsTools';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import formatFileSize from '../utility/formatFileSize';
|
||||
import { createTableRestoreScript } from '../utility/tableRestoreScript';
|
||||
import newQuery from '../query/newQuery';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -1087,10 +1120,7 @@
|
||||
|
||||
$: isPinned = !!$pinnedTables.find(x => testEqual(data, x));
|
||||
|
||||
$: backupParsed =
|
||||
data.objectTypeField === 'tables'
|
||||
? data.pureName.match(/^_(.*)_(\d\d\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)-(\d\d)$/)
|
||||
: null;
|
||||
$: backupParsed = data.objectTypeField === 'tables' ? data.pureName.match(TABLE_BACKUP_REGEX) : null;
|
||||
$: backupTitle =
|
||||
backupParsed != null
|
||||
? `${backupParsed[1]} (${backupParsed[2]}-${backupParsed[3]}-${backupParsed[4]} ${backupParsed[5]}:${backupParsed[6]}:${backupParsed[7]})`
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
import registerCommand from './registerCommand';
|
||||
import { get } from 'svelte/store';
|
||||
import AboutModal from '../modals/AboutModal.svelte';
|
||||
import SettingsModal from '../settings/SettingsModal.svelte';
|
||||
import SqlGeneratorModal from '../modals/SqlGeneratorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import newQuery, { newDiagram, newPerspective, newQueryDesign } from '../query/newQuery';
|
||||
@@ -74,7 +73,14 @@ registerCommand({
|
||||
category: __t('command.theme', { defaultMessage: 'Theme' }),
|
||||
name: __t('command.theme.change', { defaultMessage: 'Change' }),
|
||||
toolbarName: __t('command.theme.changeToolbar', { defaultMessage: 'Change theme' }),
|
||||
onClick: () => showModal(SettingsModal, { selectedTab: 'theme' }),
|
||||
onClick: () => openNewTab({
|
||||
title: 'Settings',
|
||||
icon: 'icon settings',
|
||||
tabComponent: 'SettingsTab',
|
||||
props: {
|
||||
selectedItem: 'theme',
|
||||
},
|
||||
}),
|
||||
// getSubCommands: () => get(extensions).themes.map(themeCommand),
|
||||
});
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</script>
|
||||
|
||||
<div class="main" class:maxHeight100 class:flex1>
|
||||
<HorizontalSplitter initialValue="20%">
|
||||
<HorizontalSplitter initialValue="170px">
|
||||
<svelte:fragment slot="1">
|
||||
<div class="menu">
|
||||
{#each _.compact(items) as item, index}
|
||||
@@ -118,6 +118,10 @@
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.menu-item:first-child {
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
@@ -211,8 +211,11 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="m-2">
|
||||
<div class="title"><FontIcon icon="icon tables" /> {_t('importExport.mapSourceTablesFiles', { defaultMessage: "Map source tables/files" })}</div>
|
||||
<div class="m-2" data-testid="ImportExportConfigurator_tableMappingSection">
|
||||
<div class="title">
|
||||
<FontIcon icon="icon tables" />
|
||||
{_t('importExport.mapSourceTablesFiles', { defaultMessage: 'Map source tables/files' })}
|
||||
</div>
|
||||
|
||||
{#key targetEditKey}
|
||||
{#key progressHolder}
|
||||
@@ -221,34 +224,34 @@
|
||||
columns={[
|
||||
{
|
||||
fieldName: 'source',
|
||||
header: _t('importExport.source', { defaultMessage: "Source" }),
|
||||
header: _t('importExport.source', { defaultMessage: 'Source' }),
|
||||
component: SourceName,
|
||||
getProps: row => ({ name: row }),
|
||||
},
|
||||
{
|
||||
fieldName: 'action',
|
||||
header: _t('importExport.action', { defaultMessage: "Action" }),
|
||||
header: _t('importExport.action', { defaultMessage: 'Action' }),
|
||||
component: SourceAction,
|
||||
getProps: row => ({ name: row, targetDbinfo }),
|
||||
},
|
||||
{
|
||||
fieldName: 'target',
|
||||
header: _t('importExport.target', { defaultMessage: "Target" }),
|
||||
header: _t('importExport.target', { defaultMessage: 'Target' }),
|
||||
slot: 1,
|
||||
},
|
||||
supportsPreview && {
|
||||
fieldName: 'preview',
|
||||
header: _t('importExport.preview', { defaultMessage: "Preview" }),
|
||||
header: _t('importExport.preview', { defaultMessage: 'Preview' }),
|
||||
slot: 0,
|
||||
},
|
||||
!!progressHolder && {
|
||||
fieldName: 'status',
|
||||
header: _t('importExport.status', { defaultMessage: "Status" }),
|
||||
header: _t('importExport.status', { defaultMessage: 'Status' }),
|
||||
slot: 3,
|
||||
},
|
||||
{
|
||||
fieldName: 'columns',
|
||||
header: _t('importExport.columns', { defaultMessage: "Columns" }),
|
||||
header: _t('importExport.columns', { defaultMessage: 'Columns' }),
|
||||
slot: 2,
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value={_t('datagrid.closeTabs.close', { defaultMessage: 'Close tabs' })}
|
||||
data-testid="CloseTabModal_buttonConfirm"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
onConfirm();
|
||||
@@ -52,6 +53,7 @@
|
||||
<FormStyledButton
|
||||
type="button"
|
||||
value={_t('common.cancel', { defaultMessage: 'Cancel' })}
|
||||
data-testid="CloseTabModal_buttonCancel"
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
onCancel();
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
</script>
|
||||
|
||||
<ModalBase {...$$restProps} simple>
|
||||
<div class="mb-2">_{_t('commandModal.showKeyCombination', { defaultMessage: 'Show desired key combination and press ENTER' })}</div>
|
||||
<div class="mb-2">{_t('commandModal.showKeyCombination', { defaultMessage: 'Show desired key combination and press ENTER' })}</div>
|
||||
<div class="largeFormMarker">
|
||||
<TextField on:keydown={handleKeyDown} bind:value focused />
|
||||
</div>
|
||||
|
||||
@@ -1,61 +1,27 @@
|
||||
<script lang="ts">
|
||||
import { internalRedirectTo } from '../clientAuth';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { EDITOR_KEYBINDINGS_MODES } from '../query/AceEditor.svelte';
|
||||
import { currentEditorKeybindigMode, currentEditorWrapEnabled } from '../stores';
|
||||
import { _t, getSelectedLanguage, setSelectedLanguage } from '../translations';
|
||||
import { isMac } from '../utility/common';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { internalRedirectTo } from '../clientAuth';
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { _t, getSelectedLanguage, setSelectedLanguage } from '../translations';
|
||||
import { isMac } from '../utility/common';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
|
||||
const electron = getElectron();
|
||||
let restartWarning = false;
|
||||
</script>
|
||||
<div class="wrapper">
|
||||
<div class="heading">{_t('settings.general', { defaultMessage: 'General' })}</div>
|
||||
{#if electron}
|
||||
<div class="heading">{_t('settings.appearance', { defaultMessage: 'Appearance' })}</div>
|
||||
<FormCheckboxField
|
||||
name="app.useNativeMenu"
|
||||
label={isMac()
|
||||
? _t('settings.useNativeWindowTitle', { defaultMessage: 'Use native window title' })
|
||||
: _t('settings.useSystemNativeMenu', { defaultMessage: 'Use system native menu' })}
|
||||
on:change={() => {
|
||||
restartWarning = true;
|
||||
}}
|
||||
/>
|
||||
{#if restartWarning}
|
||||
<div class="ml-5 mb-3">
|
||||
<FontIcon icon="img warn" />
|
||||
{_t('settings.nativeMenuRestartWarning', {
|
||||
defaultMessage: 'Native menu settings will be applied after app restart',
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FormCheckboxField
|
||||
name="tabGroup.showServerName"
|
||||
label={_t('settings.tabGroup.showServerName', {
|
||||
defaultMessage: 'Show server name alongside database name in title of the tab group',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<div class="heading">{_t('settings.localization', { defaultMessage: 'Localization' })}</div>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.localization.language', { defaultMessage: 'Language' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
<div class="heading">{_t('settings.application', { defaultMessage: 'Application' })}</div>
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.localization.language', { defaultMessage: 'Language' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
isNative
|
||||
data-testid="SettingsModal_languageSelect"
|
||||
options={[
|
||||
@@ -86,7 +52,64 @@ type="combo"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.other.autoUpdateApplication', { defaultMessage: 'Auto update application' })}
|
||||
name="app.autoUpdateMode"
|
||||
isNative
|
||||
defaultValue=""
|
||||
options={[
|
||||
{
|
||||
value: 'skip',
|
||||
label: _t('settings.other.autoUpdateApplication.skip', {
|
||||
defaultMessage: 'Do not check for new versions',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: _t('settings.other.autoUpdateApplication.check', { defaultMessage: 'Check for new versions' }),
|
||||
},
|
||||
{
|
||||
value: 'download',
|
||||
label: _t('settings.other.autoUpdateApplication.download', {
|
||||
defaultMessage: 'Check and download new versions',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="heading">{_t('settings.appearance', { defaultMessage: 'Appearance' })}</div>
|
||||
|
||||
{#if electron}
|
||||
|
||||
<FormCheckboxField
|
||||
name="app.useNativeMenu"
|
||||
label={isMac()
|
||||
? _t('settings.useNativeWindowTitle', { defaultMessage: 'Use native window title' })
|
||||
: _t('settings.useSystemNativeMenu', { defaultMessage: 'Use system native menu' })}
|
||||
on:change={() => {
|
||||
restartWarning = true;
|
||||
}}
|
||||
/>
|
||||
{#if restartWarning}
|
||||
<div class="ml-5 mb-3">
|
||||
<FontIcon icon="img warn" />
|
||||
{_t('settings.nativeMenuRestartWarning', {
|
||||
defaultMessage: 'Native menu settings will be applied after app restart',
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FormCheckboxField
|
||||
name="tabGroup.showServerName"
|
||||
label={_t('settings.tabGroup.showServerName', {
|
||||
defaultMessage: 'Show server name alongside database name in title of the tab group',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import FormCheckboxField from "../forms/FormCheckboxField.svelte";
|
||||
import FormSelectField from "../forms/FormSelectField.svelte";
|
||||
import FormTextField from "../forms/FormTextField.svelte";
|
||||
import { _t } from "../translations";
|
||||
import { isProApp } from "../utility/proTools";
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="heading">{_t('settings.other', { defaultMessage: 'Other' })}</div>
|
||||
|
||||
<FormTextField
|
||||
name="other.gistCreateToken"
|
||||
label={_t('settings.other.gistCreateToken', { defaultMessage: 'API token for creating error gists' })}
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.other.autoUpdateApplication', { defaultMessage: 'Auto update application' })}
|
||||
name="app.autoUpdateMode"
|
||||
isNative
|
||||
defaultValue=""
|
||||
options={[
|
||||
{
|
||||
value: 'skip',
|
||||
label: _t('settings.other.autoUpdateApplication.skip', {
|
||||
defaultMessage: 'Do not check for new versions',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: _t('settings.other.autoUpdateApplication.check', { defaultMessage: 'Check for new versions' }),
|
||||
},
|
||||
{
|
||||
value: 'download',
|
||||
label: _t('settings.other.autoUpdateApplication.download', {
|
||||
defaultMessage: 'Check and download new versions',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if isProApp()}
|
||||
<FormCheckboxField
|
||||
name="ai.allowSendModels"
|
||||
label={_t('settings.other.ai.allowSendModels', {
|
||||
defaultMessage: 'Allow to send DB models and query snippets to AI service',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
font-size: 20px;
|
||||
margin: 5px;
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
margin-top: var(--dim-large-form-margin);
|
||||
}
|
||||
</style>
|
||||
@@ -1,850 +0,0 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import Link from '../elements/Link.svelte';
|
||||
import TabControl from '../elements/TabControl.svelte';
|
||||
import CheckboxField from '../forms/CheckboxField.svelte';
|
||||
|
||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||
import FormFieldTemplateLarge from '../forms/FormFieldTemplateLarge.svelte';
|
||||
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import FormValues from '../forms/FormValues.svelte';
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import SettingsFormProvider from '../forms/SettingsFormProvider.svelte';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
|
||||
import ModalBase from '../modals/ModalBase.svelte';
|
||||
import { closeCurrentModal } from '../modals/modalTools';
|
||||
import { EDITOR_KEYBINDINGS_MODES, EDITOR_THEMES, FONT_SIZES } from '../query/AceEditor.svelte';
|
||||
import SqlEditor from '../query/SqlEditor.svelte';
|
||||
import {
|
||||
currentEditorFontSize,
|
||||
currentEditorWrapEnabled,
|
||||
currentEditorTheme,
|
||||
currentEditorKeybindigMode,
|
||||
extensions,
|
||||
selectedWidget,
|
||||
lockedDatabaseMode,
|
||||
visibleWidgetSideBar,
|
||||
currentTheme,
|
||||
getSystemTheme,
|
||||
} from '../stores';
|
||||
import { isMac } from '../utility/common';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import ThemeSkeleton from './ThemeSkeleton.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import FormTextAreaField from '../forms/FormTextAreaField.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
import { derived } from 'svelte/store';
|
||||
import { safeFormatDate } from 'dbgate-tools';
|
||||
import FormDefaultActionField from './FormDefaultActionField.svelte';
|
||||
import AiSettingsTab from './AiSettingsTab.svelte';
|
||||
import { _t, setSelectedLanguage } from '../translations';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { internalRedirectTo } from '../clientAuth';
|
||||
import { getSelectedLanguage } from '../translations';
|
||||
|
||||
const electron = getElectron();
|
||||
let restartWarning = false;
|
||||
let licenseKeyCheckResult = null;
|
||||
|
||||
export let selectedTab = 'general';
|
||||
|
||||
const sqlPreview = `-- example query
|
||||
SELECT
|
||||
MAX(Album.AlbumId) AS max_album,
|
||||
MAX(Album.Title) AS max_title,
|
||||
Artist.ArtistId,
|
||||
'album' AS test_string,
|
||||
123 AS test_number
|
||||
FROM
|
||||
Album
|
||||
INNER JOIN Artist ON Album.ArtistId = Artist.ArtistId
|
||||
GROUP BY
|
||||
Artist.ArtistId
|
||||
ORDER BY
|
||||
Artist.Name ASC
|
||||
`;
|
||||
|
||||
function openThemePlugins() {
|
||||
closeCurrentModal();
|
||||
$selectedWidget = 'plugins';
|
||||
$visibleWidgetSideBar = true;
|
||||
}
|
||||
|
||||
const settings = useSettings();
|
||||
const settingsValues = derived(settings, $settings => {
|
||||
if (!$settings) {
|
||||
return {};
|
||||
}
|
||||
return $settings;
|
||||
});
|
||||
|
||||
$: licenseKey = $settingsValues['other.licenseKey'];
|
||||
let checkedLicenseKey = false;
|
||||
$: if (licenseKey && !checkedLicenseKey) {
|
||||
checkedLicenseKey = true;
|
||||
apiCall('config/check-license', { licenseKey }).then(result => {
|
||||
licenseKeyCheckResult = result;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<SettingsFormProvider>
|
||||
<ModalBase {...$$restProps} noPadding fixedHeight>
|
||||
<div slot="header">{_t('settings.title', { defaultMessage: 'Settings' })}</div>
|
||||
|
||||
<FormValues let:values>
|
||||
<TabControl
|
||||
bind:value={selectedTab}
|
||||
isInline
|
||||
inlineTabs
|
||||
scrollableContentContainer
|
||||
containerMaxWidth="100%"
|
||||
containerMaxHeight="calc(100% - 34px)"
|
||||
maxHeight100
|
||||
flex1
|
||||
tabs={[
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'general',
|
||||
label: _t('settings.general', { defaultMessage: 'General' }),
|
||||
slot: 1,
|
||||
},
|
||||
isProApp() &&
|
||||
electron && {
|
||||
identifier: 'license',
|
||||
label: _t('settings.license', { defaultMessage: 'License' }),
|
||||
slot: 7,
|
||||
},
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'connection',
|
||||
label: _t('settings.connection', { defaultMessage: 'Connection' }),
|
||||
slot: 2,
|
||||
},
|
||||
{ identifier: 'theme', label: _t('settings.theme', { defaultMessage: 'Themes' }), slot: 3 },
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'default-actions',
|
||||
label: _t('settings.defaultActions', { defaultMessage: 'Default Actions' }),
|
||||
slot: 4,
|
||||
},
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'behaviour',
|
||||
label: _t('settings.behaviour', { defaultMessage: 'Behaviour' }),
|
||||
slot: 5,
|
||||
},
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'external-tools',
|
||||
label: _t('settings.externalTools', { defaultMessage: 'External Tools' }),
|
||||
slot: 8,
|
||||
},
|
||||
hasPermission('settings/change') && {
|
||||
identifier: 'other',
|
||||
label: _t('settings.other', { defaultMessage: 'Other' }),
|
||||
slot: 6,
|
||||
},
|
||||
isProApp() && hasPermission('settings/change') && { identifier: 'ai', label: 'AI', slot: 9 },
|
||||
]}
|
||||
>
|
||||
<svelte:fragment slot="1">
|
||||
{#if electron}
|
||||
<div class="heading">{_t('settings.appearance', { defaultMessage: 'Appearance' })}</div>
|
||||
<FormCheckboxField
|
||||
name="app.useNativeMenu"
|
||||
label={isMac()
|
||||
? _t('settings.useNativeWindowTitle', { defaultMessage: 'Use native window title' })
|
||||
: _t('settings.useSystemNativeMenu', { defaultMessage: 'Use system native menu' })}
|
||||
on:change={() => {
|
||||
restartWarning = true;
|
||||
}}
|
||||
/>
|
||||
{#if restartWarning}
|
||||
<div class="ml-5 mb-3">
|
||||
<FontIcon icon="img warn" />
|
||||
{_t('settings.nativeMenuRestartWarning', {
|
||||
defaultMessage: 'Native menu settings will be applied after app restart',
|
||||
})}
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<FormCheckboxField
|
||||
name="tabGroup.showServerName"
|
||||
label={_t('settings.tabGroup.showServerName', {
|
||||
defaultMessage: 'Show server name alongside database name in title of the tab group',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<div class="heading">{_t('settings.localization', { defaultMessage: 'Localization' })}</div>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.localization.language', { defaultMessage: 'Language' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
isNative
|
||||
data-testid="SettingsModal_languageSelect"
|
||||
options={[
|
||||
{ value: 'cs', label: 'Čeština' },
|
||||
{ value: 'de', label: 'Deutsch' },
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'es', label: 'Español' },
|
||||
{ value: 'fr', label: 'Français' },
|
||||
{ value: 'it', label: 'Italiano' },
|
||||
{ value: 'pt', label: 'Português (Brasil)' },
|
||||
{ value: 'sk', label: 'Slovenčina' },
|
||||
{ value: 'ja', label: '日本語' },
|
||||
{ value: 'zh', label: '中文' },
|
||||
]}
|
||||
defaultValue={getSelectedLanguage()}
|
||||
value={getSelectedLanguage()}
|
||||
on:change={e => {
|
||||
setSelectedLanguage(e.detail);
|
||||
showModal(ConfirmModal, {
|
||||
message: _t('settings.localization.reloadWarning', {
|
||||
defaultMessage: 'Application will be reloaded to apply new language settings',
|
||||
}),
|
||||
onConfirm: () => {
|
||||
setTimeout(() => {
|
||||
internalRedirectTo(electron ? '/index.html' : '/');
|
||||
}, 100);
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<div class="heading">{_t('settings.dataGrid.title', { defaultMessage: 'Data grid' })}</div>
|
||||
<FormTextField
|
||||
name="dataGrid.pageSize"
|
||||
label={_t('settings.dataGrid.pageSize', {
|
||||
defaultMessage: 'Page size (number of rows for incremental loading, must be between 5 and 50000)',
|
||||
})}
|
||||
defaultValue="100"
|
||||
/>
|
||||
{#if isProApp()}
|
||||
<FormCheckboxField
|
||||
name="dataGrid.showHintColumns"
|
||||
label={_t('settings.dataGrid.showHintColumns', { defaultMessage: 'Show foreign key hints' })}
|
||||
defaultValue={true}
|
||||
/>
|
||||
{/if}
|
||||
<!-- <FormCheckboxField name="dataGrid.showHintColumns" label="Show foreign key hints" defaultValue={true} /> -->
|
||||
|
||||
<FormCheckboxField
|
||||
name="dataGrid.thousandsSeparator"
|
||||
label={_t('settings.dataGrid.thousandsSeparator', {
|
||||
defaultMessage: 'Use thousands separator for numbers',
|
||||
})}
|
||||
/>
|
||||
|
||||
<FormTextField
|
||||
name="dataGrid.defaultAutoRefreshInterval"
|
||||
label={_t('settings.dataGrid.defaultAutoRefreshInterval', {
|
||||
defaultMessage: 'Default grid auto refresh interval in seconds',
|
||||
})}
|
||||
defaultValue="10"
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="dataGrid.alignNumbersRight"
|
||||
label={_t('settings.dataGrid.alignNumbersRight', { defaultMessage: 'Align numbers to right' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormTextField
|
||||
name="dataGrid.collectionPageSize"
|
||||
label={_t('settings.dataGrid.collectionPageSize', {
|
||||
defaultMessage: 'Collection page size (for MongoDB JSON view, must be between 5 and 1000)',
|
||||
})}
|
||||
defaultValue="50"
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.dataGrid.coloringMode', { defaultMessage: 'Row coloring mode' })}
|
||||
name="dataGrid.coloringMode"
|
||||
isNative
|
||||
defaultValue="36"
|
||||
options={[
|
||||
{
|
||||
value: '36',
|
||||
label: _t('settings.dataGrid.coloringMode.36', { defaultMessage: 'Every 3rd and 6th row' }),
|
||||
},
|
||||
{
|
||||
value: '2-primary',
|
||||
label: _t('settings.dataGrid.coloringMode.2-primary', {
|
||||
defaultMessage: 'Every 2-nd row, primary color',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '2-secondary',
|
||||
label: _t('settings.dataGrid.coloringMode.2-secondary', {
|
||||
defaultMessage: 'Every 2-nd row, secondary color',
|
||||
}),
|
||||
},
|
||||
{ value: 'none', label: _t('settings.dataGrid.coloringMode.none', { defaultMessage: 'None' }) },
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="dataGrid.showAllColumnsWhenSearch"
|
||||
label={_t('settings.dataGrid.showAllColumnsWhenSearch', {
|
||||
defaultMessage: 'Show all columns when searching',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<div class="heading">{_t('settings.sqlEditor', { defaultMessage: 'SQL editor' })}</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="col-3">
|
||||
<FormSelectField
|
||||
label={_t('settings.sqlEditor.sqlCommandsCase', { defaultMessage: 'SQL commands case' })}
|
||||
name="sqlEditor.sqlCommandsCase"
|
||||
isNative
|
||||
defaultValue="upperCase"
|
||||
options={[
|
||||
{ value: 'upperCase', label: 'UPPER CASE' },
|
||||
{ value: 'lowerCase', label: 'lower case' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.editor.keybinds', { defaultMessage: 'Editor keybinds' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
isNative
|
||||
defaultValue="default"
|
||||
options={EDITOR_KEYBINDINGS_MODES.map(mode => ({ label: mode.label, value: mode.value }))}
|
||||
value={$currentEditorKeybindigMode}
|
||||
on:change={e => ($currentEditorKeybindigMode = e.detail)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.editor.wordWrap', { defaultMessage: 'Enable word wrap' })}
|
||||
type="combo"
|
||||
>
|
||||
<CheckboxField
|
||||
checked={$currentEditorWrapEnabled}
|
||||
on:change={e => ($currentEditorWrapEnabled = e.target.checked)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormTextField
|
||||
name="sqlEditor.limitRows"
|
||||
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
|
||||
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.showTableAliasesInCodeCompletion"
|
||||
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
|
||||
defaultMessage: 'Show table aliases in code completion',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableSplitByEmptyLine"
|
||||
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.disableExecuteCurrentLine"
|
||||
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
|
||||
defaultMessage: 'Disable current line execution (Execute current)',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="sqlEditor.hideColumnsPanel"
|
||||
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="2">
|
||||
<div class="heading">{_t('settings.connection', { defaultMessage: 'Connection' })}</div>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.connection.showOnlyTabsFromSelectedDatabase', {
|
||||
defaultMessage: 'Show only tabs from selected database',
|
||||
})}
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
$lockedDatabaseMode = !$lockedDatabaseMode;
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField checked={$lockedDatabaseMode} on:change={e => ($lockedDatabaseMode = e.target.checked)} />
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<FormCheckboxField
|
||||
name="connection.autoRefresh"
|
||||
label={_t('settings.connection.autoRefresh', {
|
||||
defaultMessage: 'Automatic refresh of database model on background',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
<FormTextField
|
||||
name="connection.autoRefreshInterval"
|
||||
label={_t('settings.connection.autoRefreshInterval', {
|
||||
defaultMessage: 'Interval between automatic DB structure reloads in seconds',
|
||||
})}
|
||||
defaultValue="30"
|
||||
disabled={values['connection.autoRefresh'] === false}
|
||||
/>
|
||||
<FormSelectField
|
||||
label={_t('settings.connection.sshBindHost', { defaultMessage: 'Local host address for SSH connections' })}
|
||||
name="connection.sshBindHost"
|
||||
isNative
|
||||
defaultValue="127.0.0.1"
|
||||
options={[
|
||||
{ value: '127.0.0.1', label: '127.0.0.1 (IPv4)' },
|
||||
{ value: '::1', label: '::1 (IPv6)' },
|
||||
{ value: 'localhost', label: 'localhost (domain name)' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div class="heading">{_t('settings.session', { defaultMessage: 'Query sessions' })}</div>
|
||||
<FormCheckboxField
|
||||
name="session.autoClose"
|
||||
label={_t('settings.session.autoClose', {
|
||||
defaultMessage: 'Automatic close query sessions after period without any activity',
|
||||
})}
|
||||
defaultValue={true}
|
||||
/>
|
||||
<FormTextField
|
||||
name="session.autoCloseTimeout"
|
||||
label={_t('settings.session.autoCloseTimeout', {
|
||||
defaultMessage: 'Interval, after which query session without activity is closed (in minutes)',
|
||||
})}
|
||||
defaultValue="15"
|
||||
disabled={values['session.autoClose'] === false}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="3">
|
||||
<div class="heading">{_t('settings.applicationTheme', { defaultMessage: 'Application theme' })}</div>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.applicationTheme.useSystemTheme', { defaultMessage: 'Use system theme' })}
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
if ($currentTheme) {
|
||||
$currentTheme = null;
|
||||
} else {
|
||||
$currentTheme = getSystemTheme();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={!$currentTheme}
|
||||
on:change={e => {
|
||||
if (e.target['checked']) {
|
||||
$currentTheme = null;
|
||||
} else {
|
||||
$currentTheme = getSystemTheme();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<div class="themes">
|
||||
{#each $extensions.themes as theme}
|
||||
<ThemeSkeleton {theme} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="m-5">
|
||||
{_t('settings.appearance.moreThemes', { defaultMessage: 'More themes are available as' })}
|
||||
<Link onClick={openThemePlugins}>plugins</Link>
|
||||
<br />
|
||||
{_t('settings.appearance.afterInstalling', {
|
||||
defaultMessage:
|
||||
'After installing theme plugin (try search "theme" in available extensions) new themes will be available here.',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div class="heading">{_t('settings.appearance.editorTheme', { defaultMessage: 'Editor theme' })}</div>
|
||||
|
||||
<div class="flex">
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.appearance.editorTheme', { defaultMessage: 'Theme' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
isNative
|
||||
notSelected={_t('settings.appearance.editorTheme.default', { defaultMessage: '(use theme default)' })}
|
||||
options={EDITOR_THEMES.map(theme => ({ label: theme, value: theme }))}
|
||||
value={$currentEditorTheme}
|
||||
on:change={e => ($currentEditorTheme = e.detail)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.appearance.fontSize', { defaultMessage: 'Font size' })}
|
||||
type="combo"
|
||||
>
|
||||
<SelectField
|
||||
isNative
|
||||
notSelected="(default)"
|
||||
options={FONT_SIZES}
|
||||
value={FONT_SIZES.find(x => x.value == $currentEditorFontSize) ? $currentEditorFontSize : 'custom'}
|
||||
on:change={e => ($currentEditorFontSize = e.detail)}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<FormFieldTemplateLarge
|
||||
label={_t('settings.appearance.customSize', { defaultMessage: 'Custom size' })}
|
||||
type="text"
|
||||
>
|
||||
<TextField
|
||||
value={$currentEditorFontSize == 'custom' ? '' : $currentEditorFontSize}
|
||||
on:change={e => ($currentEditorFontSize = e.target['value'])}
|
||||
disabled={!!FONT_SIZES.find(x => x.value == $currentEditorFontSize) &&
|
||||
$currentEditorFontSize != 'custom'}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
</div>
|
||||
|
||||
<div class="col-3">
|
||||
<FormTextField
|
||||
name="editor.fontFamily"
|
||||
label={_t('settings.appearance.fontFamily', { defaultMessage: 'Editor font family' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor">
|
||||
<SqlEditor value={sqlPreview} readOnly />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="4">
|
||||
<div class="heading">{_t('settings.defaultActions', { defaultMessage: 'Default Actions' })}</div>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.defaultActions.connectionClick', { defaultMessage: 'Connection click' })}
|
||||
name="defaultAction.connectionClick"
|
||||
isNative
|
||||
defaultValue="connect"
|
||||
options={[
|
||||
{
|
||||
value: 'openDetails',
|
||||
label: _t('settings.defaultActions.connectionClick.openDetails', {
|
||||
defaultMessage: 'Edit / open details',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'connect',
|
||||
label: _t('settings.defaultActions.connectionClick.connect', { defaultMessage: 'Connect' }),
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
label: _t('settings.defaultActions.connectionClick.none', { defaultMessage: 'Do nothing' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.defaultActions.databaseClick', { defaultMessage: 'Database click' })}
|
||||
name="defaultAction.databaseClick"
|
||||
isNative
|
||||
defaultValue="switch"
|
||||
options={[
|
||||
{
|
||||
value: 'switch',
|
||||
label: _t('settings.defaultActions.databaseClick.switch', { defaultMessage: 'Switch database' }),
|
||||
},
|
||||
{
|
||||
value: 'none',
|
||||
label: _t('settings.defaultActions.databaseClick.none', { defaultMessage: 'Do nothing' }),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="defaultAction.useLastUsedAction"
|
||||
label={_t('settings.defaultActions.useLastUsedAction', { defaultMessage: 'Use last used action' })}
|
||||
defaultValue={true}
|
||||
/>
|
||||
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.tableClick', { defaultMessage: 'Table click' })}
|
||||
objectTypeField="tables"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.viewClick', { defaultMessage: 'View click' })}
|
||||
objectTypeField="views"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.materializedViewClick', { defaultMessage: 'Materialized view click' })}
|
||||
objectTypeField="matviews"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.procedureClick', { defaultMessage: 'Procedure click' })}
|
||||
objectTypeField="procedures"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.functionClick', { defaultMessage: 'Function click' })}
|
||||
objectTypeField="functions"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
<FormDefaultActionField
|
||||
label={_t('settings.defaultActions.collectionClick', { defaultMessage: 'NoSQL collection click' })}
|
||||
objectTypeField="collections"
|
||||
disabled={values['defaultAction.useLastUsedAction'] !== false}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="5">
|
||||
<div class="heading">{_t('settings.behaviour', { defaultMessage: 'Behaviour' })}</div>
|
||||
|
||||
<FormCheckboxField
|
||||
name="behaviour.useTabPreviewMode"
|
||||
label={_t('settings.behaviour.useTabPreviewMode', { defaultMessage: 'Use tab preview mode' })}
|
||||
defaultValue={true}
|
||||
/>
|
||||
|
||||
<FormCheckboxField
|
||||
name="behaviour.jsonPreviewWrap"
|
||||
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap JSON in preview' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<div class="tip">
|
||||
<FontIcon icon="img tip" />
|
||||
{_t('settings.behaviour.singleClickPreview', {
|
||||
defaultMessage:
|
||||
'When you single-click or select a file in the "Tables, Views, Functions" view, it is shown in a preview mode and reuses an existing tab (preview tab). This is useful if you are quickly browsing tables and don\'t want every visited table to have its own tab. When you start editing the table or use double-click to open the table from the "Tables" view, a new tab is dedicated to that table.',
|
||||
})}
|
||||
</div>
|
||||
|
||||
<FormCheckboxField
|
||||
name="behaviour.openDetailOnArrows"
|
||||
label={_t('settings.behaviour.openDetailOnArrows', {
|
||||
defaultMessage: 'Open detail on keyboard navigation',
|
||||
})}
|
||||
defaultValue={true}
|
||||
disabled={values['behaviour.useTabPreviewMode'] === false}
|
||||
/>
|
||||
|
||||
<div class="heading">{_t('settings.confirmations', { defaultMessage: 'Confirmations' })}</div>
|
||||
|
||||
<FormCheckboxField
|
||||
name="skipConfirm.tableDataSave"
|
||||
label={_t('settings.confirmations.skipConfirm.tableDataSave', {
|
||||
defaultMessage: 'Skip confirmation when saving table data (SQL)',
|
||||
})}
|
||||
/>
|
||||
<FormCheckboxField
|
||||
name="skipConfirm.collectionDataSave"
|
||||
label={_t('settings.confirmations.skipConfirm.collectionDataSave', {
|
||||
defaultMessage: 'Skip confirmation when saving collection data (NoSQL)',
|
||||
})}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="6">
|
||||
<div class="heading">{_t('settings.other', { defaultMessage: 'Other' })}</div>
|
||||
|
||||
<FormTextField
|
||||
name="other.gistCreateToken"
|
||||
label={_t('settings.other.gistCreateToken', { defaultMessage: 'API token for creating error gists' })}
|
||||
defaultValue=""
|
||||
/>
|
||||
|
||||
<FormSelectField
|
||||
label={_t('settings.other.autoUpdateApplication', { defaultMessage: 'Auto update application' })}
|
||||
name="app.autoUpdateMode"
|
||||
isNative
|
||||
defaultValue=""
|
||||
options={[
|
||||
{
|
||||
value: 'skip',
|
||||
label: _t('settings.other.autoUpdateApplication.skip', {
|
||||
defaultMessage: 'Do not check for new versions',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: _t('settings.other.autoUpdateApplication.check', { defaultMessage: 'Check for new versions' }),
|
||||
},
|
||||
{
|
||||
value: 'download',
|
||||
label: _t('settings.other.autoUpdateApplication.download', {
|
||||
defaultMessage: 'Check and download new versions',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
{#if isProApp()}
|
||||
<FormCheckboxField
|
||||
name="ai.allowSendModels"
|
||||
label={_t('settings.other.ai.allowSendModels', {
|
||||
defaultMessage: 'Allow to send DB models and query snippets to AI service',
|
||||
})}
|
||||
defaultValue={false}
|
||||
/>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="7">
|
||||
<div class="heading">{_t('settings.other.license', { defaultMessage: 'License' })}</div>
|
||||
<FormTextAreaField
|
||||
name="other.licenseKey"
|
||||
label={_t('settings.other.licenseKey', { defaultMessage: 'License key' })}
|
||||
rows={7}
|
||||
onChange={async value => {
|
||||
licenseKeyCheckResult = await apiCall('config/check-license', { licenseKey: value });
|
||||
}}
|
||||
/>
|
||||
{#if licenseKeyCheckResult}
|
||||
<div class="m-3 ml-5">
|
||||
{#if licenseKeyCheckResult.status == 'ok'}
|
||||
<div>
|
||||
<FontIcon icon="img ok" />
|
||||
{_t('settings.other.licenseKey.valid', { defaultMessage: 'License key is valid' })}
|
||||
</div>
|
||||
{#if licenseKeyCheckResult.validTo}
|
||||
<div>
|
||||
{_t('settings.other.licenseKey.validTo', { defaultMessage: 'License valid to:' })}
|
||||
{licenseKeyCheckResult.validTo}
|
||||
</div>
|
||||
{/if}
|
||||
{#if licenseKeyCheckResult.expiration}
|
||||
<div>
|
||||
{_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
|
||||
<b>{safeFormatDate(licenseKeyCheckResult.expiration)}</b>
|
||||
</div>
|
||||
{/if}
|
||||
{:else if licenseKeyCheckResult.status == 'error'}
|
||||
<div>
|
||||
<FontIcon icon="img error" />
|
||||
{licenseKeyCheckResult.errorMessage ??
|
||||
_t('settings.other.licenseKey.invalid', { defaultMessage: 'License key is invalid' })}
|
||||
{#if licenseKeyCheckResult.expiration}
|
||||
<div>
|
||||
{_t('settings.other.licenseKey.expiration', { defaultMessage: 'License key expiration:' })}
|
||||
<b>{safeFormatDate(licenseKeyCheckResult.expiration)}</b>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if licenseKeyCheckResult.isExpired}
|
||||
<div class="mt-2">
|
||||
<FormStyledButton
|
||||
value={_t('settings.other.licenseKey.checkForNew', {
|
||||
defaultMessage: 'Check for new license key',
|
||||
})}
|
||||
skipWidth
|
||||
on:click={async () => {
|
||||
licenseKeyCheckResult = await apiCall('config/get-new-license', { oldLicenseKey: licenseKey });
|
||||
if (licenseKeyCheckResult.licenseKey) {
|
||||
apiCall('config/update-settings', { 'other.licenseKey': licenseKeyCheckResult.licenseKey });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="8">
|
||||
<div class="heading">{_t('settings.externalTools', { defaultMessage: 'External Tools' })}</div>
|
||||
<FormTextField
|
||||
name="externalTools.mysqldump"
|
||||
label={_t('settings.other.externalTools.mysqldump', {
|
||||
defaultMessage: 'mysqldump (backup MySQL database)',
|
||||
})}
|
||||
defaultValue="mysqldump"
|
||||
/>
|
||||
<FormTextField
|
||||
name="externalTools.mysql"
|
||||
label={_t('settings.other.externalTools.mysql', { defaultMessage: 'mysql (restore MySQL database)' })}
|
||||
defaultValue="mysql"
|
||||
/>
|
||||
<FormTextField
|
||||
name="externalTools.mysqlPlugins"
|
||||
label={_t('settings.other.externalTools.mysqlPlugins', {
|
||||
defaultMessage:
|
||||
'Folder with mysql plugins (for example for authentication). Set only in case of problems',
|
||||
})}
|
||||
defaultValue=""
|
||||
/>
|
||||
<FormTextField
|
||||
name="externalTools.pg_dump"
|
||||
label={_t('settings.other.externalTools.pg_dump', {
|
||||
defaultMessage: 'pg_dump (backup PostgreSQL database)',
|
||||
})}
|
||||
defaultValue="pg_dump"
|
||||
/>
|
||||
<FormTextField
|
||||
name="externalTools.psql"
|
||||
label={_t('settings.other.externalTools.psql', { defaultMessage: 'psql (restore PostgreSQL database)' })}
|
||||
defaultValue="psql"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="9">
|
||||
<AiSettingsTab {values} />
|
||||
</svelte:fragment>
|
||||
</TabControl>
|
||||
</FormValues>
|
||||
|
||||
<div slot="footer">
|
||||
<!-- <FormSubmit value="OK" on:click={handleOk} /> -->
|
||||
<FormStyledButton value={_t('common.close', { defaultMessage: 'Close' })} on:click={closeCurrentModal} />
|
||||
</div>
|
||||
</ModalBase>
|
||||
</SettingsFormProvider>
|
||||
|
||||
<style>
|
||||
.heading {
|
||||
font-size: 20px;
|
||||
margin: 5px;
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
margin-top: var(--dim-large-form-margin);
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
margin-top: var(--dim-large-form-margin);
|
||||
}
|
||||
|
||||
.themes {
|
||||
overflow-x: scroll;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.editor {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
width: 400px;
|
||||
margin-left: var(--dim-large-form-margin);
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,6 @@
|
||||
import DefaultActionsSettings from "../settings/DefaultActionsSettings.svelte";
|
||||
import BehaviourSettings from "../settings/BehaviourSettings.svelte";
|
||||
import ExternalToolsSettings from "../settings/ExternalToolsSettings.svelte";
|
||||
import OtherSettings from "../settings/OtherSettings.svelte";
|
||||
import LicenseSettings from "../settings/LicenseSettings.svelte";
|
||||
import { isProApp } from "../utility/proTools";
|
||||
import { _t } from "../translations";
|
||||
@@ -20,6 +19,8 @@
|
||||
import SQLEditorSettings from "../settings/SQLEditorSettings.svelte";
|
||||
import AiSettingsTab from "../settings/AiSettingsTab.svelte";
|
||||
|
||||
export let selectedItem = 'general';
|
||||
|
||||
const menuItems = [
|
||||
{
|
||||
label: _t('settings.general', { defaultMessage: 'General' }),
|
||||
@@ -98,16 +99,7 @@
|
||||
props: {},
|
||||
testid: 'settings-ai',
|
||||
},
|
||||
{
|
||||
label: _t('settings.other', { defaultMessage: 'Other' }),
|
||||
identifier: 'other',
|
||||
component: OtherSettings,
|
||||
props: {},
|
||||
testid: 'settings-other',
|
||||
},
|
||||
];
|
||||
|
||||
let selectedItem = 'general';
|
||||
</script>
|
||||
|
||||
<SettingsFormProvider>
|
||||
|
||||
174
packages/web/src/utility/tableRestoreScript.ts
Normal file
174
packages/web/src/utility/tableRestoreScript.ts
Normal file
@@ -0,0 +1,174 @@
|
||||
import _ from 'lodash';
|
||||
import { Condition, dumpSqlInsert, dumpSqlUpdate, Insert, Update, Delete, dumpSqlDelete } from 'dbgate-sqltree';
|
||||
import { TableInfo, SqlDumper } from 'dbgate-types';
|
||||
|
||||
export function createTableRestoreScript(backupTable: TableInfo, originalTable: TableInfo, dmp: SqlDumper) {
|
||||
const bothColumns = _.intersection(
|
||||
backupTable.columns.map(x => x.columnName),
|
||||
originalTable.columns.map(x => x.columnName)
|
||||
);
|
||||
const keyColumns = _.intersection(
|
||||
originalTable.primaryKey?.columns?.map(x => x.columnName) || [],
|
||||
backupTable.columns.map(x => x.columnName)
|
||||
);
|
||||
const valueColumns = _.difference(bothColumns, keyColumns);
|
||||
|
||||
if (keyColumns.length === 0) {
|
||||
throw new Error('Cannot create restore script: no key columns found');
|
||||
}
|
||||
|
||||
function makeColumnCond(colName: string, operator: '=' | '<>' | '<' | '>' | '<=' | '>=' = '='): Condition {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator,
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: { name: originalTable },
|
||||
},
|
||||
right: {
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: { alias: 'bak' },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function makeNullNotNullCond(colName: string, isCond1: boolean): Condition {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
conditionType: 'isNull',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: isCond1 ? { name: originalTable } : { alias: 'bak' },
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionType: 'isNotNull',
|
||||
expr: {
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: isCond1 ? { alias: 'bak' } : { name: originalTable },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function putTitle(title: string) {
|
||||
dmp.putRaw('\n\n');
|
||||
dmp.comment(`******************** ${title} ********************`);
|
||||
dmp.putRaw('\n');
|
||||
}
|
||||
|
||||
dmp.comment(`Restoring data into table ${originalTable.pureName} from backup table ${backupTable.pureName}`);
|
||||
dmp.putRaw('\n');
|
||||
dmp.comment(`Key columns: ${keyColumns.join(', ')}`);
|
||||
dmp.putRaw('\n');
|
||||
dmp.comment(`Value columns: ${valueColumns.join(', ')}`);
|
||||
dmp.putRaw('\n');
|
||||
dmp.comment(`Follows UPDATE, DELETE, INSERT statements to restore data`);
|
||||
dmp.putRaw('\n');
|
||||
|
||||
if (valueColumns.length > 0) {
|
||||
const update: Update = {
|
||||
commandType: 'update',
|
||||
from: { name: originalTable },
|
||||
fields: valueColumns.map(colName => ({
|
||||
exprType: 'select',
|
||||
select: {
|
||||
commandType: 'select',
|
||||
from: { name: backupTable, alias: 'bak' },
|
||||
columns: [
|
||||
{
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: { alias: 'bak' },
|
||||
},
|
||||
],
|
||||
where: {
|
||||
conditionType: 'and',
|
||||
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||
},
|
||||
},
|
||||
targetColumn: colName,
|
||||
})),
|
||||
where: {
|
||||
conditionType: 'exists',
|
||||
subQuery: {
|
||||
commandType: 'select',
|
||||
from: { name: backupTable, alias: 'bak' },
|
||||
selectAll: true,
|
||||
where: {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
...keyColumns.map(keyColName => makeColumnCond(keyColName)),
|
||||
{
|
||||
conditionType: 'or',
|
||||
conditions: valueColumns.flatMap(colName => [
|
||||
makeColumnCond(colName, '<>'),
|
||||
makeNullNotNullCond(colName, true),
|
||||
makeNullNotNullCond(colName, false),
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
putTitle('UPDATE');
|
||||
dumpSqlUpdate(dmp, update);
|
||||
dmp.endCommand();
|
||||
}
|
||||
|
||||
const delcmd: Delete = {
|
||||
commandType: 'delete',
|
||||
from: { name: originalTable },
|
||||
where: {
|
||||
conditionType: 'notExists',
|
||||
subQuery: {
|
||||
commandType: 'select',
|
||||
from: { name: backupTable, alias: 'bak' },
|
||||
selectAll: true,
|
||||
where: {
|
||||
conditionType: 'and',
|
||||
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
putTitle('DELETE');
|
||||
dumpSqlDelete(dmp, delcmd);
|
||||
dmp.endCommand();
|
||||
|
||||
const insert: Insert = {
|
||||
commandType: 'insert',
|
||||
targetTable: originalTable,
|
||||
fields: bothColumns.map(colName => ({
|
||||
targetColumn: colName,
|
||||
exprType: 'column',
|
||||
columnName: colName,
|
||||
source: { alias: 'bak' },
|
||||
})),
|
||||
whereNotExistsSource: { name: backupTable, alias: 'bak' },
|
||||
insertWhereNotExistsCondition: {
|
||||
conditionType: 'and',
|
||||
conditions: keyColumns.map(colName => makeColumnCond(colName)),
|
||||
},
|
||||
};
|
||||
|
||||
putTitle('INSERT');
|
||||
|
||||
const autoinc = originalTable.columns.find(x => x.autoIncrement);
|
||||
if (autoinc) {
|
||||
dmp.allowIdentityInsert(originalTable, true);
|
||||
}
|
||||
dumpSqlInsert(dmp, insert);
|
||||
dmp.endCommand();
|
||||
if (autoinc) {
|
||||
dmp.allowIdentityInsert(originalTable, false);
|
||||
}
|
||||
}
|
||||
@@ -235,7 +235,7 @@
|
||||
|
||||
function getAppObjectGroup(data) {
|
||||
if (data.objectTypeField == 'tables') {
|
||||
if (data.pureName.match(/^_(.*)_\d\d\d\d-\d\d-\d\d-\d\d-\d\d-\d\d$/)) {
|
||||
if (data.pureName.match(databaseObjectAppObject.TABLE_BACKUP_REGEX)) {
|
||||
return _t('dbObject.tableBackups', { defaultMessage: 'Table Backups' });
|
||||
}
|
||||
}
|
||||
@@ -286,7 +286,7 @@
|
||||
icon="img alert"
|
||||
/>
|
||||
<div class="m-1" />
|
||||
<InlineButton on:click={handleRefreshDatabase}>{_t('common.refresh', { defaultMessage: 'Refresh' })}</InlineButton>
|
||||
<InlineButton on:click={handleFullRefreshDatabase}>{_t('common.refresh', { defaultMessage: 'Refresh' })}</InlineButton>
|
||||
{#if driver?.databaseEngineTypes?.includes('sql')}
|
||||
<div class="m-1" />
|
||||
<InlineButton on:click={() => runCommand('new.table')}
|
||||
|
||||
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 3b9ca48888d17d96806820c4e54bb047c18d6278
|
||||
ref: ca69c4857d7d93c4b066018e8a9a0a0ece2300e7
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
Reference in New Issue
Block a user