Merge branch 'master' into feature/duckdb-2

This commit is contained in:
SPRINX0\prochazka
2025-04-02 14:16:38 +02:00
committed by Nybkox
83 changed files with 1418 additions and 312 deletions

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -32,7 +32,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -26,7 +26,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -37,6 +37,11 @@ jobs:
run: |
cd packages/datalib
yarn test:ci
- name: Tools tests
if: always()
run: |
cd packages/tools
yarn test:ci
- uses: tanmen/jest-reporter@v1
if: always()
with:

View File

@@ -8,6 +8,18 @@ Builds:
- linux - application for linux
- win - application for Windows
### 6.3.2
- ADDED: "Use system theme" switch, use changed system theme without restart #1084
- ADDED: "Skip SETNAME instruction" option for Redis #1077
- FIXED: Clickhouse views are now available even for user with limited permissions #1076
- ADDED: Multiple-token search delimited with comma (=OR) in structure search boxes
- CHANGED: When filtering columns in data browser, data view shows only filtered columns
- ADDED: Advanced settings for diagrams (Premium)
- ADDED: Diagrams - zoom with Ctrl+mouse wheel
- FIXED: Scrollable diagram exports + scroll by mouse drag
- FIXED: Fixed many problems in diagrams when zoom is applied
- FIXED: Correctly end connection process after succesful/unsuccesful connect
### 6.3.0
- ADDED: Support for libSQL and Turso (Premium)
- ADDED: Native backup and restore database for MySQL and PostgreSQL (Premium)
@@ -29,7 +41,7 @@ Builds:
- 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)
- CHANGED: Upgraded SQLite engine version
### 6.2.0
- ADDED: Query AI Assistant (Premium)

View File

@@ -357,6 +357,7 @@ function createWindow() {
title: isProApp() ? 'DbGate Premium' : 'DbGate',
frame: useNativeMenu,
titleBarStyle: useNativeMenu ? undefined : 'hidden',
backgroundColor: electron.nativeTheme.shouldUseDarkColors ? '#111111' : undefined,
...bounds,
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
partition: isProApp() ? 'persist:dbgate-premium' : 'persist:dbgate',

View File

@@ -13,16 +13,22 @@ describe('Add connection', () => {
it('adds connection', () => {
// cy.get('[data-testid=ConnectionList_buttonNewConnection]').click();
cy.get('[data-testid=ConnectionDriverFields_connectionType]').select('MySQL');
cy.themeshot('connection');
cy.themeshot('new-connection');
cy.get('[data-testid=ConnectionDriverFields_user]').clear().type('root');
cy.get('[data-testid=ConnectionDriverFields_password]').clear().type('Pwd2020Db');
cy.get('[data-testid=ConnectionDriverFields_port]').clear().type('16004');
cy.get('[data-testid=ConnectionDriverFields_displayName]').clear().type('test-mysql-1');
// test connection
cy.get('[data-testid=ConnectionTab_buttonTest]').click();
cy.testid('ConnectionTab_buttonTest').click();
cy.contains('Connected:');
cy.testid('ConnectionTab_tabSshTunnel').click();
cy.testid('ConnectionTab_tabControlContent').themeshot('connection-sshtunnel-window', { padding: 50 });
cy.testid('ConnectionTab_tabSsl').click();
cy.testid('ConnectionTab_tabControlContent').themeshot('connection-ssl-window', { padding: 50 });
// save and connect
cy.get('[data-testid=ConnectionTab_buttonSave]').click();
cy.get('[data-testid=ConnectionTab_buttonConnect]').click();

View File

@@ -31,7 +31,7 @@ describe('Data browser data', () => {
cy.contains('Finished job script');
cy.contains('Album.csv');
cy.testid('WidgetIconPanel_database').click();
cy.themeshot('exportcsv');
cy.themeshot('configure-export-csv');
});
it('Data archive editor - macros', () => {
@@ -42,7 +42,7 @@ describe('Data browser data', () => {
cy.contains('Out Of Exile').click({ shiftKey: true });
cy.contains('Change text case').click();
cy.contains('AUDIOSLAVE');
cy.themeshot('freetable');
cy.themeshot('data-archive-macros');
});
it('Load table data', () => {
@@ -87,19 +87,19 @@ describe('Data browser data', () => {
cy.contains('Album').click();
cy.testid('DataFilterControl_input_Title').type('Rock{enter}');
cy.contains('Rows: 7');
cy.testid('DataFilterControl_input_AlbumId').type('>10{enter}');
cy.contains('Rows: 5');
cy.testid('DataFilterControl_input_AlbumId').type('>10xxx{enter}');
cy.contains('Rows: 7');
cy.testid('DataFilterControl_filtermenu_Title').click();
cy.themeshot('filter');
// hide what is not needed
cy.testid('WidgetIconPanel_database').click();
cy.testid('DataGrid_itemReferences').click();
cy.themeshot('data-browser-filter');
cy.testid('DataGridCore_button_clearFilters').click();
cy.contains('Rows: 347');
});
it('Data grid screenshots', () => {
cy.contains('MySql-connection').click();
cy.window().then(win => {
win.__changeCurrentTheme('theme-dark');
});
cy.contains('MyChinook').click();
@@ -114,16 +114,25 @@ describe('Data browser data', () => {
cy.contains('PgChinook').click();
cy.contains('customer').click();
cy.contains('Leonie').click();
cy.themeshot('datagrid');
cy.themeshot('common-data-browser');
cy.contains('invoice').click();
cy.contains('invoice_line (invoice_id)').click();
cy.themeshot('masterdetail');
cy.themeshot('data-browser-master-detail');
cy.contains('9, Place Louis Barthou').click();
cy.contains('Switch to form').click();
cy.contains('Switch to table'); // test that we are in form view
cy.themeshot('formview');
cy.themeshot('data-browser-form-view');
});
it.only('Column search', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.contains('Customer').click();
cy.testid('ColumnManager_searchColumns').clear().type('name,id{enter}');
cy.contains('Company').should('not.exist');
cy.themeshot('data-browser-column-search');
});
it('SQL Gen', () => {
@@ -131,7 +140,7 @@ describe('Data browser data', () => {
cy.contains('PgChinook').rightclick();
cy.contains('SQL Generator').click();
cy.contains('Check all').click();
cy.themeshot('sqlgen');
cy.themeshot('sql-generator');
});
it('Macros in DB', () => {
@@ -146,7 +155,7 @@ describe('Data browser data', () => {
cy.testid('DataGrid_itemMacros').click();
cy.contains('Change text case').click();
cy.contains('NIELSEN');
cy.themeshot('macros');
cy.themeshot('data-browser-macros');
});
it('Perspectives', () => {
@@ -162,7 +171,7 @@ describe('Data browser data', () => {
// check track is loaded
cy.contains('Put The Finger On You');
cy.themeshot('perspective1');
cy.themeshot('perspective-designer');
});
it('Query editor - code completion', () => {
@@ -176,7 +185,7 @@ describe('Data browser data', () => {
cy.get('body').realType('select * from Album where Album.');
// code completion
cy.contains('ArtistId');
cy.themeshot('query');
cy.themeshot('query-editor-code-completion');
});
it('Query editor - join wizard', () => {
@@ -189,7 +198,7 @@ describe('Data browser data', () => {
cy.get('body').realPress(['Control', 'j']);
// JOIN wizard
cy.contains('INNER JOIN Customer ON Invoice.CustomerId = Customer.CustomerId');
cy.themeshot('joinwizard');
cy.themeshot('query-editor-join-wizard');
});
it('Mongo JSON data view', () => {
@@ -206,7 +215,7 @@ describe('Data browser data', () => {
cy.testid('WidgetIconPanel_cell-data').click();
// test JSON view
cy.contains('Country: "Brazil"');
cy.themeshot('mongoquery');
cy.themeshot('mongo-query-json-view');
});
it('SQL preview', () => {
@@ -216,7 +225,7 @@ describe('Data browser data', () => {
cy.contains('Show SQL').click();
// index should be part of create script
cy.contains('CREATE INDEX `IFK_CustomerSupportRepId`');
cy.themeshot('sqlpreview');
cy.themeshot('sql-preview-create-index');
});
it('Query designer', () => {
@@ -225,7 +234,7 @@ describe('Data browser data', () => {
cy.testid('WidgetIconPanel_file').click();
cy.contains('customer').click();
// cy.contains('left join').rightclick();
cy.themeshot('querydesigner');
cy.themeshot('query-designer');
});
it('Database diagram', () => {
@@ -236,7 +245,7 @@ describe('Data browser data', () => {
cy.testid('WidgetIconPanel_file').click();
// check diagram is shown
cy.contains('MediaTypeId');
cy.themeshot('diagram');
cy.themeshot('database-diagram');
});
it('Charts', () => {
@@ -245,7 +254,7 @@ describe('Data browser data', () => {
cy.contains('line-chart').click();
cy.testid('TabsPanel_buttonSplit').click();
cy.testid('WidgetIconPanel_file').click();
cy.themeshot('charts');
cy.themeshot('view-split-charts');
});
it('Keyboard configuration', () => {
@@ -253,7 +262,7 @@ describe('Data browser data', () => {
cy.contains('Keyboard shortcuts').click();
cy.contains('dataForm.refresh').click();
cy.testid('CommandModal_keyboardButton').click();
cy.themeshot('keyboard');
cy.themeshot('keyboard-configuration');
});
it('Command palette', () => {
@@ -264,7 +273,7 @@ describe('Data browser data', () => {
// cy.realPress('F1');
cy.realPress('PageDown');
cy.realPress('PageDown');
cy.testid('CommandPalette_main').themeshot('commandpalette', { padding: 50 });
cy.testid('CommandPalette_main').themeshot('command-palette', { padding: 50 });
});
it('Show map', () => {
@@ -277,7 +286,7 @@ describe('Data browser data', () => {
cy.contains('13.9').click({ shiftKey: true });
cy.testid('WidgetIconPanel_cell-data').click();
cy.wait(2000);
cy.themeshot('map');
cy.themeshot('cell-map-view');
});
it('Search in connections', () => {
@@ -289,7 +298,7 @@ describe('Data browser data', () => {
cy.contains('Album').click();
cy.testid('SqlObjectList_searchMenuDropDown').click();
cy.contains('Column name').click();
cy.themeshot('connsearch');
cy.themeshot('search-in-connections');
});
it('Plugin tab', () => {
@@ -299,7 +308,7 @@ describe('Data browser data', () => {
cy.contains('Total white theme');
// wait for load logos
cy.wait(2000);
cy.themeshot('plugin');
cy.themeshot('view-plugin-tab');
});
it('Edit mongo data JSON', () => {
@@ -326,7 +335,7 @@ describe('Data browser data', () => {
cy.contains('Helena').rightclick();
cy.contains('Delete document').click();
cy.contains('Save').click();
cy.themeshot('mongosave');
cy.themeshot('save-changes-mongodb');
});
it('Edit mongo data JSON', () => {
@@ -340,7 +349,7 @@ describe('Data browser data', () => {
cy.testid('ColumnManagerRow_checkbox__id').click();
cy.testid('DataFilterControl_input_countries.1').type('EXISTS{enter}');
cy.testid('WidgetIconPanel_cell-data').click();
cy.themeshot('collection');
cy.themeshot('mongodb-json-cell-view');
});
it('Table structure editor', () => {
@@ -349,10 +358,10 @@ describe('Data browser data', () => {
cy.contains('Customer').rightclick();
cy.contains('Open structure').click();
cy.contains('varchar(40)');
cy.themeshot('structure');
cy.themeshot('table-structure-editor');
cy.contains('EmployeeId').click();
cy.contains('Ref column - Employee');
cy.themeshot('fkeditor');
cy.themeshot('foreign-key-editor');
});
it('Compare database', () => {
@@ -364,10 +373,10 @@ describe('Data browser data', () => {
cy.testid('CompareModelTab_gridObjects_Customer_Customer').click();
cy.testid('WidgetIconPanel_database').click();
cy.testid('CompareModelTab_tabDdl').click();
cy.themeshot('dbcompare');
cy.themeshot('compare-database-models');
cy.contains('Settings').click();
cy.testid('CompareModelTab_tabOperations').click();
cy.themeshot('comparesettings');
cy.themeshot('compare-database-settings');
});
it('Query editor - AI assistant', () => {
@@ -382,7 +391,7 @@ describe('Data browser data', () => {
cy.contains('Use this', { timeout: 10000 }).click();
cy.testid('QueryTab_executeButton').click();
cy.contains('Balls to the Wall');
cy.themeshot('aiassistant');
cy.themeshot('ai-assistant');
});
it('Modify data', () => {
@@ -408,7 +417,7 @@ describe('Data browser data', () => {
cy.contains('INSERT INTO `Employee`');
cy.contains("SET `FirstName`='Jane'");
cy.contains('DELETE FROM `Employee`');
cy.themeshot('modifydata');
cy.themeshot('data-browser-save-changes');
// cy.testid('ConfirmSqlModal_okButton').click();
// cy.contains('Cannot delete or update a parent row')
@@ -430,8 +439,11 @@ describe('Data browser data', () => {
cy.contains('Album').click();
cy.testid('DataFilterControl_input_ArtistId').type('22{enter}');
// cy.contains('Presence').rightclick();
// cy.contains('Coda').rightclick();
// cy.testid('DropDownMenu-container-0').contains('Export').click();
cy.contains('Export').click();
cy.themeshot('simpleexport');
// cy.wait(1000);
cy.themeshot('data-browser-export-menu');
});
it('MySQL native backup', () => {
@@ -439,7 +451,7 @@ describe('Data browser data', () => {
cy.contains('MyChinook').rightclick();
cy.contains('Create database backup').click();
cy.contains('Customer');
cy.themeshot('mysqlbackup');
cy.themeshot('mysql-backup-configuration');
});
it('View table YAML model', () => {
@@ -449,10 +461,11 @@ describe('Data browser data', () => {
cy.testid('ExportDbModelModal_archiveFolder').select('(Create new)');
cy.testid('InputTextModal_value').clear().type('test-model');
cy.testid('InputTextModal_ok').click();
cy.testid('ModalBase_window').themeshot('export-database-model-window', { padding: 50 });
cy.testid('ExportDbModelModal_exportButton').click();
cy.contains('Album').click();
cy.contains('autoIncrement');
cy.themeshot('tableyaml');
cy.themeshot('database-model-table-yaml');
});
it('Data duplicator', () => {
@@ -462,8 +475,8 @@ describe('Data browser data', () => {
cy.contains('chinook-archive').rightclick();
cy.contains('Data duplicator').click();
cy.contains('Dry run').click();
cy.testid("DataDuplicatorTab_importIntoDb").click();
cy.testid('DataDuplicatorTab_importIntoDb').click();
cy.contains('Duplicated Album, inserted 347 rows, mapped 0 rows, missing 0 rows, skipped 0 rows');
cy.themeshot('dataduplicator');
cy.themeshot('data-duplicator');
});
});

View File

@@ -11,21 +11,19 @@ describe('Team edition tests', () => {
cy.testid('AdminMenuWidget_itemConnections').click();
cy.contains('New connection').click();
cy.contains('New connection').click();
cy.contains('New connection').click();
cy.testid('ConnectionDriverFields_connectionType').select('PostgreSQL');
cy.themeshot('connadmin');
cy.themeshot('connection-administration');
cy.testid('AdminMenuWidget_itemRoles').click();
cy.contains('Permissions').click();
cy.themeshot('roleadmin');
cy.contains('logged-user').click();
cy.themeshot('role-administration');
cy.testid('AdminMenuWidget_itemAuthentication').click();
cy.contains('Add authentication').click();
cy.contains('Use database login').click();
cy.contains('Add authentication').click();
cy.contains('OAuth 2.0').click();
cy.themeshot('authadmin');
cy.themeshot('authentication-administration');
});
it('OAuth authentication', () => {
@@ -77,6 +75,5 @@ describe('Team edition tests', () => {
cy.testid('LoginPage_submitLogin').click();
cy.testid('AdminMenuWidget_itemUsers').click();
cy.contains('test@example.com');
cy.contains('Rows: 1');
});
});

View File

View File

@@ -1 +0,0 @@
Folder with screenshots

View File

@@ -1,6 +1,6 @@
{
"private": true,
"version": "6.3.1",
"version": "6.3.2",
"name": "dbgate-all",
"workspaces": [
"packages/*",

View File

@@ -102,12 +102,21 @@ function getPortalCollections() {
trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
}));
for(const conn of connections) {
for(const prop in process.env) {
if (prop.startsWith(`CONNECTION_${conn._id}_`)) {
const name = prop.substring(`CONNECTION_${conn._id}_`.length);
conn[name] = process.env[prop];
}
}
}
logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'Using connections from ENV variables');
const noengine = connections.filter(x => !x.engine);
if (noengine.length > 0) {
logger.warn(
{ connections: noengine.map(x => x._id) },
'Invalid CONNECTIONS configutation, missing ENGINE for connection ID'
'Invalid CONNECTIONS configuration, missing ENGINE for connection ID'
);
}
return connections;

View File

@@ -195,8 +195,8 @@ module.exports = {
},
exportDiagram_meta: true,
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
async exportDiagram({ filePath, html, css, themeType, themeClassName, watermark }) {
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName, watermark));
return true;
},

View File

@@ -38,6 +38,8 @@ function start() {
detail: formatErrorDetail(e, connection),
});
}
process.exit(0);
});
}

View File

@@ -33,6 +33,26 @@ function loadEncryptionKey() {
return _encryptionKey;
}
async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) {
const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
if (!storedValue) {
const generatedKey = crypto.randomBytes(32);
const newKey = generatedKey.toString('hex');
const result = {
encryptionKey: newKey,
};
await setStoredValue(encryptor.encrypt(result));
setEncryptionKey(newKey);
return;
}
const data = encryptor.decrypt(storedValue);
setEncryptionKey(data['encryptionKey']);
}
let _encryptor = null;
function getEncryptor() {
@@ -43,35 +63,32 @@ function getEncryptor() {
return _encryptor;
}
function encryptPasswordField(connection, field) {
if (
connection &&
connection[field] &&
!connection[field].startsWith('crypt:') &&
connection.passwordMode != 'saveRaw'
) {
function encryptObjectPasswordField(obj, field) {
if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
return {
...connection,
[field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
...obj,
[field]: 'crypt:' + getEncryptor().encrypt(obj[field]),
};
}
return connection;
return obj;
}
function decryptPasswordField(connection, field) {
if (connection && connection[field] && connection[field].startsWith('crypt:')) {
function decryptObjectPasswordField(obj, field) {
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
return {
...connection,
[field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
...obj,
[field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)),
};
}
return connection;
return obj;
}
function encryptConnection(connection) {
connection = encryptPasswordField(connection, 'password');
connection = encryptPasswordField(connection, 'sshPassword');
connection = encryptPasswordField(connection, 'sshKeyfilePassword');
if (connection.passwordMode != 'saveRaw') {
connection = encryptObjectPasswordField(connection, 'password');
connection = encryptObjectPasswordField(connection, 'sshPassword');
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
}
return connection;
}
@@ -81,12 +98,24 @@ function maskConnection(connection) {
}
function decryptConnection(connection) {
connection = decryptPasswordField(connection, 'password');
connection = decryptPasswordField(connection, 'sshPassword');
connection = decryptPasswordField(connection, 'sshKeyfilePassword');
connection = decryptObjectPasswordField(connection, 'password');
connection = decryptObjectPasswordField(connection, 'sshPassword');
connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
return connection;
}
function encryptUser(user) {
if (user.encryptPassword) {
user = encryptObjectPasswordField(user, 'password');
}
return user;
}
function decryptUser(user) {
user = decryptObjectPasswordField(user, 'password');
return user;
}
function pickSafeConnectionInfo(connection) {
if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
return connection;
@@ -99,10 +128,18 @@ function pickSafeConnectionInfo(connection) {
});
}
function setEncryptionKey(encryptionKey) {
_encryptionKey = encryptionKey;
_encryptor = null;
}
module.exports = {
loadEncryptionKey,
encryptConnection,
encryptUser,
decryptUser,
decryptConnection,
maskConnection,
pickSafeConnectionInfo,
loadEncryptionKeyFromExternal,
};

View File

@@ -1,4 +1,11 @@
const getDiagramExport = (html, css, themeType, themeClassName) => {
const getDiagramExport = (html, css, themeType, themeClassName, watermark) => {
const watermarkHtml = watermark
? `
<div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-font-2); background-color: var(--theme-bg-2); border-top-left-radius: 5px; border: 1px solid var(--theme-border);">
${watermark}
</div>
`
: '';
return `<html>
<meta charset='utf-8'>
@@ -13,10 +20,44 @@ const getDiagramExport = (html, css, themeType, themeClassName) => {
</style>
<link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
<script>
let lastX = null;
let lastY = null;
const handleMoveDown = e => {
lastX = e.clientX;
lastY = e.clientY;
document.addEventListener('mousemove', handleMoveMove, true);
document.addEventListener('mouseup', handleMoveEnd, true);
};
const handleMoveMove = e => {
e.preventDefault();
document.body.scrollLeft -= e.clientX - lastX;
document.body.scrollTop -= e.clientY - lastY;
lastX = e.clientX;
lastY = e.clientY;
};
const handleMoveEnd = e => {
e.preventDefault();
e.stopPropagation();
lastX = null;
lastY = null;
document.removeEventListener('mousemove', handleMoveMove, true);
document.removeEventListener('mouseup', handleMoveEnd, true);
};
document.addEventListener('mousedown', handleMoveDown);
</script>
</head>
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}' style='user-select:none; cursor:pointer'>
${html}
${watermarkHtml}
</body>
</html>`;

View File

@@ -29,8 +29,8 @@ export interface GridConfig extends GridConfigColumns {
isFormView?: boolean;
formViewRecordNumber?: number;
formFilterColumns: string[];
formColumnFilterText?: string;
multiColumnFilter?: string;
searchInColumns?: string;
}
export interface GridCache {

View File

@@ -196,9 +196,24 @@ export abstract class GridDisplay {
}));
}
setSearchInColumns(searchInColumns: string) {
this.setConfig(cfg => ({
...cfg,
searchInColumns,
}));
}
get hiddenColumnIndexes() {
// console.log('GridDisplay.hiddenColumn', this.config.hiddenColumns);
return (this.config.hiddenColumns || []).map(x => _.findIndex(this.allColumns, y => y.uniqueName == x));
const res = (this.config.hiddenColumns || []).map(x => _.findIndex(this.allColumns, y => y.uniqueName == x));
if (this.config.searchInColumns) {
for (let i = 0; i < this.allColumns.length; i++) {
if (!filterName(this.config.searchInColumns, this.allColumns[i].columnName)) {
res.push(i);
}
}
}
return _.sortBy(_.uniq(res));
}
isColumnChecked(column: DisplayColumn) {

View File

@@ -0,0 +1,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
moduleFileExtensions: ['js'],
};

View File

@@ -17,8 +17,9 @@
"scripts": {
"build": "tsc",
"start": "tsc --watch",
"prepublishOnly": "yarn build",
"test": "jest",
"prepublishOnly": "yarn build"
"test:ci": "jest --json --outputFile=result.json --testLocationInResults"
},
"files": [
"lib"
@@ -26,8 +27,8 @@
"devDependencies": {
"@types/node": "^13.7.0",
"dbgate-types": "^6.0.0-alpha.1",
"jest": "^24.9.0",
"ts-jest": "^25.2.1",
"jest": "^28.1.3",
"ts-jest": "^28.0.7",
"typescript": "^4.4.3"
},
"dependencies": {

View File

@@ -354,6 +354,7 @@ export class DatabaseAnalyser {
logger.error(extractErrorLogData(err, { template }), 'Error running analyser query');
return {
rows: [],
isError: true,
};
}
}

View File

@@ -0,0 +1,63 @@
import { DatabaseInfo, TableInfo } from 'dbgate-types';
import { extendDatabaseInfo } from './structureTools';
import _sortBy from 'lodash/sortBy';
import _uniq from 'lodash/uniq';
import { filterName } from './filterName';
function tableWeight(table: TableInfo, maxRowcount?: number) {
let weight = 0;
const tableDependenciesCount = _uniq(table.dependencies?.map(x => x.pureName) || []).length;
const tableFkCount = _uniq(table.foreignKeys?.map(x => x.refTableName) || []).length;
if (table.primaryKey) weight += 1;
if (tableFkCount) weight += tableFkCount * 1;
if (maxRowcount && table.tableRowCount) {
const rowcount = parseInt(table.tableRowCount as string);
if (rowcount > 0)
weight += Math.log(rowcount) * table.columns.length * (tableFkCount || 1) * (tableDependenciesCount || 1);
} else {
if (table.columns) weight += table.columns.length * 2;
}
if (table.dependencies) weight += tableDependenciesCount * 10;
if (maxRowcount) return weight;
}
export function chooseTopTables(tables: TableInfo[], count: number, tableFilter: string, omitTablesFilter: string) {
const filteredTables = tables.filter(table => {
if (tableFilter) {
if (!filterName(tableFilter, table?.pureName)) return false;
}
if (omitTablesFilter) {
if (filterName(omitTablesFilter, table?.pureName)) return false;
}
return true;
});
if (!(count > 0)) {
return filteredTables;
}
const dbinfo: DatabaseInfo = {
tables: filteredTables,
} as DatabaseInfo;
const extended = extendDatabaseInfo(dbinfo);
const maxRowcount = Math.max(
...extended.tables
.map(x => x.tableRowCount || 0)
.map(x => parseInt(x as string))
.filter(x => x > 0)
);
const sorted = _sortBy(
_sortBy(extended.tables, x => `${x.schemaName}.${x.pureName}`),
table => -tableWeight(table, maxRowcount)
);
return sorted.slice(0, count);
}
export const DIAGRAM_ZOOMS = [0.1, 0.15, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1, 1.25, 1.5, 1.75, 2];
export const DIAGRAM_DEFAULT_WATERMARK = 'Powered by [dbgate.io](https://dbgate.io)';

View File

@@ -0,0 +1,20 @@
const { tokenizeBySearchFilter } = require('./filterName');
test('tokenize single token', () => {
const tokenized = tokenizeBySearchFilter('Album', 'al');
// console.log(JSON.stringify(tokenized, null, 2));
expect(tokenized).toEqual([
{ text: 'Al', isMatch: true },
{ text: 'bum', isMatch: false },
]);
});
test('tokenize two tokens', () => {
const tokenized = tokenizeBySearchFilter('Album', 'al,um');
// console.log(JSON.stringify(tokenized, null, 2));
expect(tokenized).toEqual([
{ text: 'Al', isMatch: true },
{ text: 'b', isMatch: false },
{ text: 'um', isMatch: true },
]);
});

View File

@@ -6,6 +6,29 @@ import _startCase from 'lodash/startCase';
// childName: string;
// }
interface TokenFactor {
tokens: string[];
}
interface TokenTree {
factors: TokenFactor[];
}
function parseTokenTree(filter: string): TokenTree {
const factors = filter
.split(',')
.map(x => x.trim())
.filter(x => x.length > 0);
return {
factors: factors.map(x => ({
tokens: x
.split(' ')
.map(x => x.trim())
.filter(x => x.length > 0),
})),
};
}
function camelMatch(filter: string, text: string): boolean {
if (!text) return false;
if (!filter) return true;
@@ -24,31 +47,27 @@ export function filterName(filter: string, ...names: string[]) {
if (!filter) return true;
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
const tokens = filter.split(' ').map(x => x.trim());
const tree = parseTokenTree(filter);
if (tree.factors.length == 0) return true;
const namesCompacted = _compact(names);
for (const token of tokens) {
const found = namesCompacted.find(name => camelMatch(token, name));
if (!found) return false;
for (const factor of tree.factors) {
let factorOk = true;
for (const token of factor.tokens) {
const found = namesCompacted.find(name => camelMatch(token, name));
if (!found) factorOk = false;
}
if (factorOk) {
return true;
}
}
return true;
return false;
}
export function filterNameCompoud(
filter: string,
namesMain: string[],
namesChild: string[]
): 'main' | 'child' | 'both' | 'none' {
if (!filter) return 'both';
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
const tokens = filter.split(' ').map(x => x.trim());
const namesCompactedMain = _compact(namesMain);
const namesCompactedChild = _compact(namesChild);
function clasifyCompoudCategory(tokens: string[], namesCompactedMain: string[], namesCompactedChild: string[]) {
let isMainOnly = true;
let isChildOnly = true;
@@ -67,10 +86,42 @@ export function filterNameCompoud(
return 'none';
}
export function filterNameCompoud(
filter: string,
namesMain: string[],
namesChild: string[]
): 'main' | 'child' | 'both' | 'none' {
if (!filter) return 'both';
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
const tree = parseTokenTree(filter);
const namesCompactedMain = _compact(namesMain);
const namesCompactedChild = _compact(namesChild);
if (tree.factors.length == 0) return 'both';
const factorRes = [];
for (const factor of tree.factors) {
const category = clasifyCompoudCategory(factor.tokens, namesCompactedMain, namesCompactedChild);
factorRes.push(category);
}
if (factorRes.includes('both')) return 'both';
if (factorRes.includes('main') && factorRes.includes('child')) return 'both';
if (factorRes.includes('main')) return 'main';
if (factorRes.includes('child')) return 'child';
return 'none';
}
export function tokenizeBySearchFilter(text: string, filter: string): { text: string; isMatch: boolean }[] {
const camelTokens = [];
const stdTokens = [];
for (const token of filter.split(' ').map(x => x.trim())) {
for (const token of filter
.split(/[ ,]/)
.map(x => x.trim())
.filter(x => x.length > 0)) {
if (token.replace(/[A-Z]/g, '').length == 0) {
camelTokens.push(token);
} else {

View File

@@ -25,4 +25,5 @@ export * from './detectSqlFilterBehaviour';
export * from './filterBehaviours';
export * from './schemaInfoTools';
export * from './dbKeysLoader';
export * from './rowProgressReporter';
export * from './rowProgressReporter';
export * from './diagramTools';

View File

@@ -9,16 +9,17 @@ import type {
import _flatten from 'lodash/flatten';
import _uniq from 'lodash/uniq';
import _keys from 'lodash/keys';
import _compact from 'lodash/compact';
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
if (!db.tables) {
return db;
}
const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || []));
const allForeignKeys = _flatten(db.tables.map(x => x?.foreignKeys || []));
return {
...db,
tables: db.tables.map(table => ({
tables: _compact(db.tables).map(table => ({
...table,
dependencies: allForeignKeys.filter(x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName),
})),

View File

@@ -306,6 +306,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
command: 'backup' | 'restore'
): { message: string; severity: 'info' | 'error' | 'debug' } | null;
getNativeOperationFormArgs(operation: 'backup' | 'restore'): any[];
getAdvancedConnectionFields(): any[];
analyserClass?: any;
dumperClass?: any;

View File

@@ -58,6 +58,24 @@ body {
.relative {
position: relative;
}
.scroll {
overflow: scroll;
}
.bg-0 {
background-color: var(--theme-bg-0);
}
.bg-1 {
background-color: var(--theme-bg-1);
}
.bg-2 {
background-color: var(--theme-bg-2);
}
.bg-3 {
background-color: var(--theme-bg-3);
}
.bg-4 {
background-color: var(--theme-bg-4);
}
.col-10 {
flex-basis: 83.3333%;

View File

@@ -12,6 +12,7 @@
visibleCommandPalette,
visibleTitleBar,
visibleToolbar,
systemThemeStore,
} from './stores';
import TabsPanel from './tabpanel/TabsPanel.svelte';
import TabRegister from './tabpanel/TabRegister.svelte';
@@ -50,7 +51,7 @@
</div>
<div
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
class={`${$currentTheme ?? $systemThemeStore} ${currentThemeType} root dbgate-screen`}
class:isElectron
use:dragDropFileTarget
on:contextmenu={e => e.preventDefault()}

View File

@@ -5,13 +5,18 @@
export let filter;
export let showDisabled = false;
export let onClearFilter = null;
</script>
{#if filter || showDisabled}
<InlineButton
on:click
on:click={() => {
filter = '';
if (onClearFilter) {
onClearFilter();
} else {
filter = '';
}
}}
title="Clear filter"
disabled={!filter}

View File

@@ -16,7 +16,7 @@
</script>
<div class="wrapper">
<div class="content" class:scrollContent>
<div class="content" class:scrollContent class:isComponentActive>
<slot />
</div>
@@ -41,6 +41,10 @@
max-height: 100%;
}
.content.isComponentActive {
max-height: calc(100% - 30px);
}
.toolstrip {
display: flex;
flex-wrap: wrap;

View File

@@ -14,8 +14,9 @@
function handleClick(e) {
const rect = e.detail.target.getBoundingClientRect();
const left = rect.left;
const top = rect.bottom;
currentDropDownMenu.set({ left, top, items: menu });
const top = rect.top;
// const top = rect.bottom;
currentDropDownMenu.set({ left, bottom: window.innerHeight - top, items: menu });
}
</script>

View File

@@ -619,6 +619,24 @@ registerCommand({
onClick: doLogout,
});
registerCommand({
id: 'app.loggedUserCommands',
category: 'App',
name: 'Logged user',
getSubCommands: () => {
const config = getCurrentConfig();
if (!config) return [];
return [
{
text: 'Logout',
onClick: () => {
doLogout();
},
},
];
},
});
registerCommand({
id: 'app.disconnect',
category: 'App',

View File

@@ -23,6 +23,7 @@
export let isSortDefined = false;
export let allowDefineVirtualReferences = false;
export let setGrouping;
export let seachInColumns = '';
const openReferencedTable = () => {
openDatabaseObjectDetail('TableDataTab', null, {
@@ -86,7 +87,7 @@
{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}
</span>
{/if}
<ColumnLabel {...column} />
<ColumnLabel {...column} filter={seachInColumns} />
{#if _.isString(column.displayedDataType || column.dataType) && !order}
<span class="data-type" title={column.dataType}>

View File

@@ -28,7 +28,6 @@
export let changeSetState: { value: ChangeSet } = null;
export let dispatchChangeSet = null;
let filter;
let domFocusField;
let selectedColumns = [];
@@ -36,7 +35,9 @@
let dragStartColumnIndex = null;
let shiftOriginColumnIndex = null;
$: items = display?.getColumns(filter)?.filter(column => filterName(filter, column.columnName)) || [];
$: currentFilter = display?.config?.searchInColumns;
$: items = display?.getColumns(currentFilter)?.filter(column => filterName(currentFilter, column.columnName)) || [];
function selectColumnIndexCore(index, e) {
const uniqueName = items[index].uniqueName;
@@ -173,8 +174,13 @@
</div>
{/if}
<SearchBoxWrapper>
<SearchInput placeholder="Search columns" bind:value={filter} />
<CloseSearchButton bind:filter />
<SearchInput
placeholder="Search columns"
value={currentFilter}
onChange={value => display.setSearchInColumns(value)}
data-testid="ColumnManager_searchColumns"
/>
<CloseSearchButton filter={currentFilter} onClearFilter={() => display.setSearchInColumns('')} />
{#if isDynamicStructure && !isJsonView}
<InlineButton
on:click={() => {
@@ -235,7 +241,7 @@
{columnIndex}
{allowChangeChangeSetStructure}
isSelected={selectedColumns.includes(column.uniqueName) || currentColumnUniqueName == column.uniqueName}
{filter}
filter={currentFilter}
on:click={() => {
if (domFocusField) domFocusField.focus();
selectedColumns = [column.uniqueName];

View File

@@ -1933,6 +1933,7 @@
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
grouping={display.getGrouping(col.uniqueName)}
{allowDefineVirtualReferences}
seachInColumns={display.config?.searchInColumns}
/>
</td>
{/each}

View File

@@ -195,12 +195,12 @@
{#if designer?.style?.showNullability || designer?.style?.showDataType}
<div class="space" />
{#if designer?.style?.showDataType && column?.dataType}
<div class="ml-2">
<div class="ml-2 data-type">
{(column?.displayedDataType || column?.dataType).toLowerCase()}
</div>
{/if}
{#if designer?.style?.showNullability}
<div class="ml-2">
<div class="ml-2 nullability">
{column?.notNull ? 'NOT NULL' : 'NULL'}
</div>
{/if}
@@ -238,4 +238,12 @@
background: var(--theme-bg-2);
color: var(--theme-font-hover);
}
.nullability {
color: var(--theme-font-4);
}
.data-type {
color: var(--theme-font-4);
}
</style>

View File

@@ -47,10 +47,13 @@
import { showModal } from '../modals/modalTools';
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
import { currentThemeDefinition } from '../stores';
import { extendDatabaseInfoFromApps } from 'dbgate-tools';
import { chooseTopTables, DIAGRAM_DEFAULT_WATERMARK, DIAGRAM_ZOOMS, extendDatabaseInfoFromApps } from 'dbgate-tools';
import SearchInput from '../elements/SearchInput.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import DragColumnMemory from './DragColumnMemory.svelte';
import createRef from '../utility/createRef';
import { isProApp } from '../utility/proTools';
import dragScroll from '../utility/dragScroll';
export let value;
export let onChange;
@@ -59,15 +62,18 @@
export let menu;
export let settings;
export let referenceComponent;
export let onReportCounts = undefined;
export const activator = createActivator('Designer', true);
let domCanvas;
let domWrapper;
let canvasWidth = 3000;
let canvasHeight = 3000;
let dragStartPoint = null;
let dragCurrentPoint = null;
let columnFilter;
export let columnFilter;
export let showColumnFilter = true;
const sourceDragColumn$ = writable(null);
const targetDragColumn$ = writable(null);
@@ -75,14 +81,24 @@
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
$: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
$: tables = value?.tables as any[];
$: references = value?.references as any[];
$: tables =
(value?.tables
? chooseTopTables(
value?.tables,
value?.style?.topTables,
value?.style?.tableFilter,
value?.style?.omitTablesFilter
)
: value?.tables) || ([] as any[]);
$: references = (value?.references || [])?.filter(
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
) as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
$: apps = useUsedApps();
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
const tableRefs = {};
let tableRefs = {};
const referenceRefs = {};
let domTables;
$: {
@@ -132,12 +148,14 @@
onChange(current => {
let newTables = current.tables || [];
for (const table of current.tables || []) {
const dbTable = (db.tables || []).find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
const dbTable = (db.tables || []).find(
x => x?.pureName == table?.pureName && x?.schemaName == table?.schemaName
);
if (
stableStringify(_.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys'])) !=
stableStringify(_.pick(table, ['columns', 'primaryKey', 'foreignKeys']))
) {
newTables = newTables.map(x =>
newTables = _.compact(newTables).map(x =>
x == table
? {
...table,
@@ -152,7 +170,7 @@
if (settings?.useDatabaseReferences) {
references = [];
for (const table of newTables) {
for (const fk of table.foreignKeys) {
for (const fk of table.foreignKeys || []) {
const dst = newTables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
if (!dst) continue;
references.push({
@@ -620,11 +638,19 @@
...current,
tables: (current.tables || []).map(x => {
const domTable = domTables[x.designerId] as any;
const rect = domTable.getRect();
return {
...x,
isSelectedTable: rectanglesHaveIntersection(rect, bounds),
};
if (domTable) {
const rect = domTable.getRect();
const rectZoomed = {
left: rect.left / zoomKoef,
right: rect.right / zoomKoef,
top: rect.top / zoomKoef,
bottom: rect.bottom / zoomKoef,
};
return {
...x,
isSelectedTable: rectanglesHaveIntersection(rectZoomed, bounds),
};
}
}),
}),
true
@@ -637,7 +663,7 @@
function recomputeReferencePositions() {
for (const ref of Object.values(referenceRefs) as any[]) {
if (ref) ref.recomputePosition();
if (ref) ref.recomputePosition(zoomKoef);
}
}
@@ -662,21 +688,26 @@
export function arrange(skipUndoChain = false, arrangeAll = true, circleMiddle = { x: 0, y: 0 }) {
const graph = new GraphDefinition();
for (const table of value?.tables || []) {
for (const table of tables || []) {
const domTable = domTables[table.designerId] as any;
if (!domTable) continue;
const rect = domTable.getRect();
graph.addNode(
table.designerId,
rect.right - rect.left,
rect.bottom - rect.top,
arrangeAll || table.needsArrange ? null : { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 }
(rect.right - rect.left) / zoomKoef,
(rect.bottom - rect.top) / zoomKoef,
arrangeAll || table.needsArrange
? null
: {
x: (rect.left + rect.right) / 2 / zoomKoef,
y: (rect.top + rect.bottom) / 2 / zoomKoef,
}
);
}
for (const reference of settings?.sortAutoLayoutReferences
? settings?.sortAutoLayoutReferences(value?.references)
: value?.references) {
? settings?.sortAutoLayoutReferences(references)
: references) {
graph.addEdge(reference.sourceId, reference.targetId);
}
@@ -710,7 +741,7 @@
current => {
return {
...current,
tables: (current?.tables || []).map(table => {
tables: _.compact(current?.tables || []).map(table => {
const node = layout.nodes[table.designerId];
// console.log('POSITION', position);
return node
@@ -732,6 +763,16 @@
);
}
function getWatermarkHtml() {
const replaceLinks = text => text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: var(--theme-font-link)" target="_blank">$1</a>');
if (value?.style?.omitExportWatermark) return null;
if (value?.style?.exportWatermark) {
return replaceLinks(value?.style?.exportWatermark);
}
return replaceLinks(DIAGRAM_DEFAULT_WATERMARK);
}
export async function exportDiagram() {
const cssLinks = ['global.css', 'build/bundle.css'];
let css = '';
@@ -745,6 +786,7 @@
if (css) css += '\n';
css += $currentThemeDefinition?.themeCss;
}
css += ' body { overflow: scroll; }';
saveFileToDisk(async filePath => {
await apiCall('files/export-diagram', {
filePath,
@@ -752,6 +794,7 @@
css,
themeType: $currentThemeDefinition?.themeType,
themeClassName: $currentThemeDefinition?.themeClassName,
watermark: getWatermarkHtml(),
});
});
}
@@ -773,7 +816,7 @@
menu,
settings?.customizeStyle && [
{ divider: true },
{
isProApp() && {
text: 'Column properties',
submenu: [
{
@@ -786,12 +829,12 @@
},
],
},
{
isProApp() && {
text: `Columns - ${_.startCase(value?.style?.filterColumns || 'all')}`,
submenu: [
{
text: 'All',
onClick: changeStyleFunc('filterColumns', null),
onClick: changeStyleFunc('filterColumns', ''),
},
{
text: 'Primary Key',
@@ -813,56 +856,10 @@
},
{
text: `Zoom - ${(value?.style?.zoomKoef || 1) * 100}%`,
submenu: [
{
text: `10 %`,
onClick: changeStyleFunc('zoomKoef', 0.1),
},
{
text: `15 %`,
onClick: changeStyleFunc('zoomKoef', 0.15),
},
{
text: `20 %`,
onClick: changeStyleFunc('zoomKoef', 0.2),
},
{
text: `40 %`,
onClick: changeStyleFunc('zoomKoef', 0.4),
},
{
text: `60 %`,
onClick: changeStyleFunc('zoomKoef', 0.6),
},
{
text: `80 %`,
onClick: changeStyleFunc('zoomKoef', 0.8),
},
{
text: `100 %`,
onClick: changeStyleFunc('zoomKoef', 1),
},
{
text: `120 %`,
onClick: changeStyleFunc('zoomKoef', 1.2),
},
{
text: `140 %`,
onClick: changeStyleFunc('zoomKoef', 1.4),
},
{
text: `160 %`,
onClick: changeStyleFunc('zoomKoef', 1.6),
},
{
text: `180 %`,
onClick: changeStyleFunc('zoomKoef', 1.8),
},
{
text: `200 %`,
onClick: changeStyleFunc('zoomKoef', 2),
},
],
submenu: DIAGRAM_ZOOMS.map(koef => ({
text: `${koef * 100} %`,
onClick: changeStyleFunc('zoomKoef', koef.toString()),
})),
},
],
];
@@ -875,9 +872,105 @@
recomputeDomTables();
});
}
const oldTopTablesRef = createRef(value?.style?.topTables);
$: {
if (value?.style?.topTables > 0 && oldTopTablesRef.get() != value?.style?.topTables) {
oldTopTablesRef.set(value?.style?.topTables);
tick().then(() => {
arrange();
tick().then(() => {
recomputeReferencePositions();
recomputeDomTables();
});
});
}
}
function handleWheel(event) {
if (event.ctrlKey) {
event.preventDefault();
const zoomIndex = DIAGRAM_ZOOMS.findIndex(x => x == value?.style?.zoomKoef);
if (zoomIndex < 0) DIAGRAM_ZOOMS.findIndex(x => x == 1);
let newZoomIndex = zoomIndex;
if (event.deltaY < 0) {
newZoomIndex += 1;
}
if (event.deltaY > 0) {
newZoomIndex -= 1;
}
if (newZoomIndex < 0) {
newZoomIndex = 0;
}
if (newZoomIndex >= DIAGRAM_ZOOMS.length) {
newZoomIndex = DIAGRAM_ZOOMS.length - 1;
}
const newZoomKoef = DIAGRAM_ZOOMS[newZoomIndex];
callChange(
current => ({
...current,
style: {
...current?.style,
zoomKoef: newZoomKoef.toString(),
},
}),
true
);
}
}
function handleDragScroll(x, y) {
domWrapper.scrollLeft -= x;
domWrapper.scrollTop -= y;
}
const oldZoomKoefRef = createRef(value?.style?.zoomKoef || 1);
$: {
if (
domWrapper &&
value?.style?.zoomKoef != oldZoomKoefRef.get() &&
value?.style?.zoomKoef > 0 &&
oldZoomKoefRef.get() > 0
) {
domWrapper.scrollLeft = Math.round((domWrapper.scrollLeft / oldZoomKoefRef.get()) * value?.style?.zoomKoef);
domWrapper.scrollTop = Math.round((domWrapper.scrollTop / oldZoomKoefRef.get()) * value?.style?.zoomKoef);
}
oldZoomKoefRef.set(value?.style?.zoomKoef);
}
// $: console.log('DESIGNER VALUE', value);
// $: console.log('TABLES ARRAY', tables);
// $: {
// if (value?.tables?.find(x => !x)) {
// console.log('**** INCORRECT DESIGNER VALUE**** ', value);
// }
// }
// $: {
// if (value?.tables?.length < 100) {
// console.log('**** SMALL TABLES**** ', value);
// }
// }
$: if (onReportCounts) {
// console.log('REPORTING COUNTS');
onReportCounts({
all: _.compact(value?.tables || []).length,
filtered: _.compact(tables || []).length,
});
}
</script>
<div class="wrapper noselect" use:contextMenu={createMenu}>
<div
class="wrapper noselect"
use:contextMenu={createMenu}
on:wheel={handleWheel}
bind:this={domWrapper}
use:dragScroll={handleDragScroll}
>
{#if !(tables?.length > 0)}
<div class="empty">Drag &amp; drop tables or views from left panel here</div>
{/if}
@@ -887,8 +980,8 @@
bind:this={domCanvas}
on:dragover={e => e.preventDefault()}
on:drop={handleDrop}
style={`width:${canvasWidth}px;height:${canvasHeight}px;
${settings?.customizeStyle && value?.style?.zoomKoef ? `zoom:${value?.style?.zoomKoef};` : ''}
style={`width:${canvasWidth / zoomKoef}px;height:${canvasHeight / zoomKoef}px;
${settings?.customizeStyle && value?.style?.zoomKoef ? `transform:scale(${value?.style?.zoomKoef});transform-origin: top left;` : ''}
`}
on:mousedown={e => {
if (e.button == 0 && settings?.canSelectTables) {
@@ -913,6 +1006,7 @@
onRemoveReference={removeReference}
designer={value}
{settings}
{zoomKoef}
/>
{/each}
<!--
@@ -971,7 +1065,7 @@
</svg>
{/if}
</div>
{#if tables?.length > 0}
{#if showColumnFilter && tables?.length > 0}
<div class="panel">
<DragColumnMemory {settings} {sourceDragColumn$} {targetDragColumn$} />
<div class="searchbox">

View File

@@ -1,6 +1,9 @@
<script lang="ts">
import Designer from './Designer.svelte';
import DiagramDesignerReference from './DiagramDesignerReference.svelte';
// import QueryDesignerReference from './QueryDesignerReference.svelte';
export let columnFilter;
</script>
<Designer
@@ -18,4 +21,6 @@
arrangeAlg: 'springy',
}}
referenceComponent={DiagramDesignerReference}
showColumnFilter={false}
{columnFilter}
/>

View File

@@ -9,6 +9,8 @@
export let domTables;
export let settings;
export let zoomKoef;
let src = null;
let dst = null;
@@ -19,7 +21,7 @@
const arhi = 12;
const arpad = 3;
export function recomputePosition() {
export function recomputePosition(zoomKoef) {
const { designerId, sourceId, targetId, columns, joinType } = reference;
/** @type {DomTableRef} */
@@ -31,6 +33,17 @@
const targetRect = targetTable.getRect();
if (!sourceRect || !targetRect) return null;
if (zoomKoef > 0) {
sourceRect.left /= zoomKoef;
sourceRect.right /= zoomKoef;
sourceRect.top /= zoomKoef;
sourceRect.bottom /= zoomKoef;
targetRect.left /= zoomKoef;
targetRect.right /= zoomKoef;
targetRect.top /= zoomKoef;
targetRect.bottom /= zoomKoef;
}
src = {
x: (sourceRect.left + sourceRect.right) / 2,
y: (sourceRect.top + sourceRect.bottom) / 2,
@@ -47,7 +60,7 @@
$: {
domTables;
recomputePosition();
recomputePosition(zoomKoef);
}
</script>

View File

@@ -0,0 +1 @@
This is part of DbGate Premium

View File

@@ -8,12 +8,17 @@
$: searchValue = value || '';
export let isDebounced = false;
export let onFocusFilteredList = null;
export let onChange = null;
let domInput;
function handleKeyDown(e) {
if (e.keyCode == keycodes.escape) {
value = '';
if (onChange) {
onChange('');
} else {
value = '';
}
}
if (e.keyCode == keycodes.downArrow || e.keyCode == keycodes.pageDown || e.keyCode == keycodes.enter) {
onFocusFilteredList?.();
@@ -27,7 +32,11 @@
domInput.focus();
if (text) {
domInput.value = text;
value = text;
if (onChange) {
onChange(text);
} else {
value = text;
}
}
}
</script>
@@ -37,8 +46,12 @@
{placeholder}
value={searchValue}
on:input={e => {
if (isDebounced) debouncedSet(domInput.value);
else value = domInput.value;
if (onChange) {
onChange(domInput.value);
} else {
if (isDebounced) debouncedSet(domInput.value);
else value = domInput.value;
}
}}
on:keydown={handleKeyDown}
bind:this={domInput}

View File

@@ -16,6 +16,8 @@
export let isInline = false;
export let containerMaxWidth = undefined;
export let flex1 = true;
export let contentTestId = undefined;
export let inlineTabs = false;
export function setValue(index) {
value = index;
@@ -26,7 +28,7 @@
</script>
<div class="main" class:flex1>
<div class="tabs">
<div class="tabs" class:inlineTabs>
{#each _.compact(tabs) as tab, index}
<div class="tab-item" class:selected={value == index} on:click={() => (value = index)} data-testid={tab.testid}>
<span class="ml-2">
@@ -39,7 +41,7 @@
{/if}
</div>
<div class="content-container">
<div class="content-container" data-testid={contentTestId}>
{#each _.compact(tabs) as tab, index}
<div class="container" class:isInline class:tabVisible={index == value} style:max-width={containerMaxWidth}>
<svelte:component this={tab.component} {...tab.props} tabControlHiddenTab={index != value} />
@@ -77,17 +79,27 @@
height: var(--dim-tabs-height);
min-height: var(--dim-tabs-height);
right: 0;
background-color: var(--theme-bg-2);
overflow-x: auto;
max-width: 100%;
}
.tabs:not(.inlineTabs) {
background-color: var(--theme-bg-2);
}
.tabs.inlineTabs {
border-bottom: 1px solid var(--theme-border);
text-transform: uppercase;
}
.tabs.inlineTabs .tab-item.selected {
border-bottom: 2px solid var(--theme-font-link);
}
.tabs::-webkit-scrollbar {
height: 7px;
}
.tab-item {
border-right: 1px solid var(--theme-border);
padding-left: 15px;
padding-right: 15px;
display: flex;
@@ -95,6 +107,10 @@
cursor: pointer;
}
.tabs:not(.inlineTabs) .tab-item {
border-right: 1px solid var(--theme-border);
}
/* .tab-item:hover {
color: ${props => props.theme.tabs_font_hover};
} */
@@ -123,4 +139,5 @@
.container.isInline:not(.tabVisible) {
display: none;
}
</style>

View File

@@ -31,6 +31,11 @@
export let noCellPadding = false;
export let domTable = undefined;
export let stickyHeader = false;
export let checkedKeys = null;
export let onSetCheckedKeys = null;
export let extractCheckedKey = x => x.id;
const dispatch = createEventDispatcher();
@@ -63,11 +68,15 @@
on:keydown
tabindex={selectable ? -1 : undefined}
on:keydown={handleKeyDown}
class:stickyHeader
>
<thead>
<thead class:stickyHeader>
<tr>
{#if checkedKeys}
<th></th>
{/if}
{#each columnList as col}
<td
<th
class:clickable={col.sortable}
on:click={() => {
if (col.sortable) {
@@ -89,7 +98,7 @@
{#if sortedByField == col.fieldName}
<FontIcon icon={sortOrderIsDesc ? 'img sort-desc' : 'img sort-asc'} padLeft />
{/if}
</td>
</th>
{/each}
</tr>
</thead>
@@ -108,6 +117,18 @@
}
}}
>
{#if checkedKeys}
<td>
<input
type="checkbox"
checked={checkedKeys.includes(extractCheckedKey(row))}
on:change={e => {
if (e.target['checked']) onSetCheckedKeys(_.uniq([...checkedKeys, extractCheckedKey(row)]));
else onSetCheckedKeys(checkedKeys.filter(x => x != extractCheckedKey(row)));
}}
/>
</td>
{/if}
{#each columnList as col}
{@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }}
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)} class:noCellPadding>
@@ -164,7 +185,7 @@
background: var(--theme-bg-hover);
}
thead td {
thead th {
border: 1px solid var(--theme-border);
background-color: var(--theme-bg-1);
padding: 5px;
@@ -184,4 +205,31 @@
td.clickable {
cursor: pointer;
}
thead.stickyHeader {
position: sticky;
top: 0;
z-index: 1;
border-top: 1px solid var(--theme-border);
}
table.stickyHeader th {
border-left: none;
}
thead.stickyHeader :global(tr:first-child) :global(th) {
border-top: 1px solid var(--theme-border);
}
table.stickyHeader td {
border: 0px;
border-bottom: 1px solid var(--theme-border);
border-right: 1px solid var(--theme-border);
}
table.stickyHeader {
border-spacing: 0;
border-collapse: separate;
border-left: 1px solid var(--theme-border);
}
</style>

View File

@@ -0,0 +1,79 @@
<script lang="ts">
export let value;
// use for 3-state checkbox
export let inheritedValue = null;
export let onChange;
export let label;
$: renderedValue = value ?? inheritedValue;
$: isInherited = inheritedValue != null && value == null;
function getNextValue() {
if (inheritedValue != null) {
// 3-state logic
if (isInherited) {
return true;
}
if (renderedValue) {
return false;
}
return null;
}
return !value;
}
</script>
<div
class="wrapper"
on:click|preventDefault|stopPropagation={() => {
onChange(getNextValue());
}}
>
<div class="checkbox" {...$$restProps} class:checked={!!renderedValue} class:isInherited />
<div class="label">
{label}
</div>
</div>
<style>
.wrapper {
display: flex;
align-items: center;
margin: 4px;
cursor: pointer;
}
.label {
user-select: none;
}
.checkbox {
width: 14px !important;
height: 14px !important;
margin: 5px;
-webkit-appearance: none;
-moz-appearance: none;
-o-appearance: none;
appearance: none;
outline: 1px solid var(--theme-border);
box-shadow: none;
font-size: 0.8em;
text-align: center;
line-height: 1em;
background: var(--theme-bg-0);
}
.checked:after {
content: '✔';
color: var(--theme-font-1);
font-weight: bold;
}
.isInherited {
background: var(--theme-bg-2) !important;
}
</style>

View File

@@ -459,7 +459,7 @@
setConfig(x => ({
...x,
// @ts-ignore
formColumnFilterText: (x.formColumnFilterText || '') + event.key,
searchInColumns: (x.searchInColumns || '') + event.key,
}));
} else {
// @ts-ignore
@@ -473,7 +473,7 @@
if (event.keyCode == keycodes.escape) {
setConfig(x => ({
...x,
formColumnFilterText: '',
searchInColumns: '',
}));
}
@@ -541,7 +541,7 @@
columnIndex = incrementFunc(columnIndex);
while (
isInRange(columnIndex) &&
!filterName(display.config.formColumnFilterText, display.formColumns[columnIndex].columnName)
!filterName(display.config.searchInColumns, display.formColumns[columnIndex].columnName)
) {
columnIndex = incrementFunc(columnIndex);
}
@@ -549,7 +549,7 @@
columnIndex = firstInRange;
while (
isInRange(columnIndex) &&
!filterName(display.config.formColumnFilterText, display.formColumns[columnIndex].columnName)
!filterName(display.config.searchInColumns, display.formColumns[columnIndex].columnName)
) {
columnIndex = incrementFunc(columnIndex);
}
@@ -572,7 +572,7 @@
case keycodes.rightArrow:
return moveCurrentCell(currentCell[0], currentCell[1] + 1);
case keycodes.upArrow:
if (currentCell[1] % 2 == 0 && display.config.formColumnFilterText) {
if (currentCell[1] % 2 == 0 && display.config.searchInColumns) {
return findFilteredColumn(
x => x - 1,
x => x >= 0,
@@ -583,7 +583,7 @@
return moveCurrentCell(currentCell[0] - 1, currentCell[1]);
case keycodes.downArrow:
if (currentCell[1] % 2 == 0 && display.config.formColumnFilterText) {
if (currentCell[1] % 2 == 0 && display.config.searchInColumns) {
return findFilteredColumn(
x => x + 1,
x => x < display.formColumns.length,
@@ -631,8 +631,8 @@
data-row={rowIndex}
data-col={chunkIndex * 2}
style={rowHeight > 1 ? `height: ${rowHeight}px` : undefined}
class:columnFiltered={display.config.formColumnFilterText &&
filterName(display.config.formColumnFilterText, col.columnName)}
class:columnFiltered={display.config.searchInColumns &&
filterName(display.config.searchInColumns, col.columnName)}
class:isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
bind:this={domCells[`${rowIndex},${chunkIndex * 2}`]}
>

View File

@@ -39,12 +39,12 @@
<div class="flex">
<input
type="text"
value={display?.config?.formColumnFilterText || ''}
value={display?.config?.searchInColumns || ''}
on:keydown={e => {
if (e.keyCode == keycodes.escape) {
setConfig(x => ({
...x,
formColumnFilterText: '',
searchInColumns: '',
}));
}
}}
@@ -52,7 +52,7 @@
setConfig(x => ({
...x,
// @ts-ignore
formColumnFilterText: e.target.value,
searchInColumns: e.target.value,
}))}
/>
</div>

View File

@@ -217,6 +217,8 @@
'icon autocommit-on': 'mdi mdi-check-circle',
'icon autocommit-off': 'mdi mdi-check-circle-outline',
'icon premium': 'mdi mdi-star',
'img ok': 'mdi mdi-check-circle color-icon-green',
'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green',
'img alert': 'mdi mdi-alert-circle color-icon-blue',

View File

@@ -300,7 +300,10 @@
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),
onConfirm: value => {
setFieldValue(`columns_${row}`, value);
targetEditKey += 1;
},
});
}}
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}

View File

@@ -6,6 +6,7 @@
import _ from 'lodash';
import { copyTextToClipboard } from '../utility/clipboard';
import { openJsonLinesData } from '../utility/openJsonLinesData';
import { useSettings } from '../utility/metadataLoaders';
setContext('json-tree-context-key', {});
@@ -23,6 +24,9 @@
export let isInserted = false;
export let isModified = false;
const settings = useSettings();
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
setContext('json-tree-default-expanded', expandAll);
if (slicedKeyCount) setContext('json-tree-sliced-key-count', slicedKeyCount);
@@ -66,6 +70,7 @@
class:isDeleted
class:isInserted
class:isModified
class:wrap
>
<JSONNode
{key}
@@ -115,6 +120,9 @@
list-style: none;
white-space: nowrap;
}
ul.wrap :global(li) {
white-space: normal;
}
ul,
ul :global(ul) {
padding: 0;

View File

@@ -24,7 +24,7 @@
Build date: <span>{moment(buildTime).format('YYYY-MM-DD')}</span>
</div>
<div class="m-1">
Web: <Link href="https://dbgate.org">dbgate.org</Link>
Web: <Link href="https://dbgate.io">dbgate.io</Link>
</div>
<div class="m-1">
Source codes: <Link href="https://github.com/dbgate/dbgate/">github</Link>
@@ -32,9 +32,6 @@
<div class="m-1">
Docker container: <Link href="https://hub.docker.com/r/dbgate/dbgate">docker hub</Link>
</div>
<div class="m-1">
Online demo: <Link href="https://demo.dbgate.org">demo.dbgate.org</Link>
</div>
<div class="m-1">
Search plugins: <Link href="https://www.npmjs.com/search?q=keywords:dbgateplugin">npmjs.com</Link>
</div>

View File

@@ -1,5 +1,4 @@
<script lang="ts">
import DropDownButton from '../buttons/DropDownButton.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import ColumnMapColumnDropdown from '../elements/ColumnMapColumnDropdown.svelte';
import Link from '../elements/Link.svelte';
@@ -8,8 +7,10 @@
import FormProvider from '../forms/FormProvider.svelte';
import FormSubmit from '../forms/FormSubmit.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools';
import _ from 'lodash';
export let header = 'Configure columns';
export let onConfirm;
@@ -34,6 +35,15 @@
skip: false,
}));
}
if (sourceTableInfo && targetTableInfo) {
return sourceTableInfo.columns
.map(x => ({
src: x.columnName,
dst: targetTableInfo.columns.find(y => y.columnName == x.columnName)?.columnName,
skip: false,
}))
.filter(x => x.dst != null);
}
return [];
}
@@ -51,6 +61,32 @@
$: differentFromReset = !equalValues(value, resetValue);
let value = initialValue?.length > 0 ? initialValue : resetValue;
let validationError;
function validate() {
validationError = null;
if (!value) return;
if (value.length == 0) return;
if (value.some(x => !x.src || !x.dst)) {
validationError = 'Source and target columns must be defined';
return;
}
const duplicates = _.chain(value.map(x => x.dst))
.countBy()
.pickBy(count => count > 1)
.keys()
.value();
if (duplicates.length > 0) {
validationError = 'Target columns must be unique, duplicates found: ' + duplicates.join(', ');
return;
}
}
$: {
value;
validate();
}
</script>
<FormProvider>
@@ -91,8 +127,8 @@
<svelte:fragment slot="2" let:row let:index>
<ColumnMapColumnDropdown
value={row['dst']}
onChange={e =>
(value = (value || []).map((x, i) => (i == index ? { ...x, dst: e.target.value, ignore: false } : x)))}
onChange={column =>
(value = (value || []).map((x, i) => (i == index ? { ...x, dst: column, ignore: false } : x)))}
tableInfo={targetTableInfo}
/>
</svelte:fragment>
@@ -105,9 +141,17 @@
</svelte:fragment>
</TableControl>
{#if validationError}
<div class="error-result">
<FontIcon icon="img error" />
{validationError}
</div>
{/if}
<svelte:fragment slot="footer">
<FormSubmit
value="OK"
disabled={!!validationError}
on:click={() => {
closeCurrentModal();
onConfirm(!value || value.length == 0 || !differentFromReset ? null : value);
@@ -132,3 +176,9 @@
</svelte:fragment>
</ModalBase>
</FormProvider>
<style>
.error-result {
margin: 10px;
}
</style>

View File

@@ -8,15 +8,23 @@
export let message;
export let onConfirm;
export let confirmLabel = 'OK';
export let header = null;
</script>
<FormProvider>
<ModalBase {...$$restProps}>
<svelte:fragment slot="header">
{#if header}
{header}
{/if}
</svelte:fragment>
{message}
<svelte:fragment slot="footer">
<FormSubmit
value="OK"
value={confirmLabel}
on:click={() => {
closeCurrentModal();
onConfirm();

View File

@@ -8,6 +8,7 @@
<DropDownMenu
left={$currentDropDownMenu.left}
top={$currentDropDownMenu.top}
bottom={$currentDropDownMenu.bottom}
items={$currentDropDownMenu.items}
targetElement={$currentDropDownMenu.targetElement}
on:close={() => ($currentDropDownMenu = null)}

View File

@@ -7,29 +7,6 @@
if (side == 'right') return { top: top, left: left + box.width };
return { top: top, left: left };
}
function fixPopupPlacement(element) {
const { width, height } = element.getBoundingClientRect();
let offset = getElementOffset(element);
let newLeft = null;
let newTop = null;
if (offset.left + width > window.innerWidth) {
newLeft = offset.left - width;
if (newLeft < 0) newLeft = 0;
}
if (offset.top + height > window.innerHeight) {
newTop = offset.top - height;
if (newTop < 0) newTop = 0;
}
if (newLeft != null) element.style.left = `${newLeft}px`;
if (newTop != null) element.style.top = `${newTop}px`;
}
</script>
<script>
@@ -43,12 +20,17 @@
export let items;
export let top;
export let bottom;
export let left;
export let onCloseParent;
export let targetElement;
export let submenuLevel = 0;
let element;
let newLeft = undefined;
let newTop = undefined;
let hoverItem;
let hoverOffset;
@@ -57,6 +39,8 @@
let switchIndex = 0;
let submenuKey = 0;
const dispatch = createEventDispatcher();
let closeHandlers = [];
@@ -68,6 +52,25 @@
closeHandlers = [];
}
function fixPopupPlacement(element) {
const { width, height } = element.getBoundingClientRect();
let offset = getElementOffset(element);
if (offset.left + width > window.innerWidth) {
newLeft = offset.left - width;
if (newLeft < 0) newLeft = 0;
}
if (offset.top < 0) {
newTop = 0;
} else if (offset.top + height > window.innerHeight) {
newTop = offset.top - height;
if (newTop < 0) newTop = 0;
}
}
function registerCloseHandler(handler) {
closeHandlers.push(handler);
}
@@ -80,6 +83,7 @@
submenuItem = item;
submenuOffset = hoverOffset;
submenuKey += 1;
return;
}
if (item.switchStore && item.switchValue) {
@@ -109,6 +113,7 @@
const changeActiveSubmenu = _.throttle(() => {
submenuItem = hoverItem;
submenuOffset = hoverOffset;
submenuKey += 1;
}, 500);
$: preparedItems = prepareMenuItems(items, { targetElement, registerCloseHandler }, $commandsCustomized);
@@ -128,7 +133,14 @@
});
</script>
<ul class="dropDownMenuMarker" style={`left: ${left}px; top: ${top}px`} bind:this={element}>
<ul
class="dropDownMenuMarker"
style:top={(newTop ?? top) != null ? `${newTop ?? top}px` : undefined}
style:bottom={newTop == null && bottom != null ? `${bottom}px` : undefined}
style:left={(newLeft ?? left) != null ? `${newLeft ?? left}px` : undefined}
bind:this={element}
data-testid={`DropDownMenu-container-${submenuLevel}`}
>
{#each preparedItems as item}
{#if item.divider}
<li class="divider" />
@@ -172,14 +184,17 @@
{/each}
</ul>
{#if submenuItem?.submenu}
<svelte:self
items={submenuItem?.submenu}
{...submenuOffset}
onCloseParent={() => {
if (onCloseParent) onCloseParent();
dispatchClose();
}}
/>
{#key submenuKey}
<svelte:self
items={submenuItem?.submenu}
{...submenuOffset}
onCloseParent={() => {
if (onCloseParent) onCloseParent();
dispatchClose();
}}
submenuLevel={submenuLevel + 1}
/>
{/key}
{/if}
<style>

View File

@@ -39,7 +39,14 @@
<!-- The Modal -->
<div id="myModal" class="bglayer">
<!-- Modal content -->
<div class="window" class:fullScreen class:simple use:clickOutside on:clickOutside={handleClickOutside}>
<div
class="window"
class:fullScreen
class:simple
use:clickOutside
on:clickOutside={handleClickOutside}
data-testid="ModalBase_window"
>
{#if $$slots.header}
<div class="header" class:fullScreen>
<div><slot name="header" /></div>

View File

@@ -1,13 +1,24 @@
<script lang="ts">
import FormTextField from '../forms/FormTextField.svelte';
import { openedConnections, openedSingleDatabaseConnections } from '../stores';
import { extensions, openedConnections, openedSingleDatabaseConnections } from '../stores';
import { getFormContext } from '../forms/FormProviderCore.svelte';
import FormTextAreaField from '../forms/FormTextAreaField.svelte';
import FormArgumentList from '../forms/FormArgumentList.svelte';
const { values } = getFormContext();
$: engine = $values.engine;
$: driver = $extensions.drivers.find(x => x.engine == engine);
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
$: advancedFields = driver?.getAdvancedConnectionFields ? driver?.getAdvancedConnectionFields() : null;
</script>
<FormTextAreaField label="Allowed databases, one per line" name="allowedDatabases" disabled={isConnected} rows={8} />
<FormTextField label="Allowed databases regular expression" name="allowedDatabasesRegex" disabled={isConnected} />
{#if advancedFields}
<FormArgumentList args={advancedFields} />
{/if}

View File

@@ -28,6 +28,8 @@
selectedWidget,
lockedDatabaseMode,
visibleWidgetSideBar,
currentTheme,
getSystemTheme,
} from '../stores';
import { isMac } from '../utility/common';
import getElectron from '../utility/getElectron';
@@ -280,6 +282,32 @@ ORDER BY
<svelte:fragment slot="3">
<div class="heading">Application theme</div>
<FormFieldTemplateLarge
label="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} />
@@ -403,6 +431,12 @@ ORDER BY
<FormCheckboxField name="behaviour.useTabPreviewMode" label="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" /> 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
@@ -495,16 +529,12 @@ ORDER BY
label="Folder with mysql plugins (for example for authentication). Set only in case of problems"
defaultValue=""
/>
<FormTextField
<FormTextField
name="externalTools.pg_dump"
label="pg_dump (backup PostgreSQL database)"
defaultValue="pg_dump"
/>
<FormTextField
name="externalTools.psql"
label="psql (restore PostgreSQL database)"
defaultValue="psql"
/>
<FormTextField name="externalTools.psql" label="psql (restore PostgreSQL database)" defaultValue="psql" />
</svelte:fragment>
</TabControl>
</FormValues>

View File

@@ -26,15 +26,29 @@ export interface TabDefinition {
focused?: boolean;
}
function getSystemTheme() {
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light';
const darkModeMediaQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
export const systemThemeStore = writable(darkModeMediaQuery?.matches ? 'theme-dark' : 'theme-light');
if (darkModeMediaQuery) {
darkModeMediaQuery.addEventListener('change', e => {
systemThemeStore.set(e.matches ? 'theme-dark' : 'theme-light');
});
}
export function writableWithStorage<T>(defaultValue: T, storageName) {
export function getSystemTheme() {
return darkModeMediaQuery?.matches ? 'theme-dark' : 'theme-light';
}
export function writableWithStorage<T>(defaultValue: T, storageName, removeCondition?: (value: T) => boolean) {
const init = localStorage.getItem(storageName);
const res = writable<T>(init ? safeJsonParse(init, defaultValue, true) : defaultValue);
res.subscribe(value => {
localStorage.setItem(storageName, JSON.stringify(value));
if (removeCondition && removeCondition(value)) {
localStorage.removeItem(storageName);
} else {
localStorage.setItem(storageName, JSON.stringify(value));
}
});
return res;
}
@@ -104,8 +118,8 @@ export const extensions = writable<ExtensionsDirectory>(null);
export const visibleCommandPalette = writable(null);
export const commands = writable({});
export const currentTheme = getElectron()
? writableSettingsValue(getSystemTheme(), 'currentTheme')
: writableWithStorage(getSystemTheme(), 'currentTheme');
? writableSettingsValue(null, 'currentTheme')
: writableWithStorage(null, 'currentTheme', x => x == null);
export const currentEditorTheme = getElectron()
? writableSettingsValue(null, 'currentEditorTheme')
: writableWithStorage(null, 'currentEditorTheme');
@@ -197,12 +211,24 @@ export const connectionAppObjectSearchSettings = writableWithStorage(
'connectionAppObjectSearchSettings2'
);
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
$extensions?.themes?.find(x => x.themeClassName == $currentTheme)
let currentThemeValue = null;
currentTheme.subscribe(value => {
currentThemeValue = value;
});
export const getCurrentTheme = () => currentThemeValue;
export const currentThemeDefinition = derived(
[currentTheme, extensions, systemThemeStore],
([$currentTheme, $extensions, $systemTheme]) => {
const usedTheme = $currentTheme ?? $systemTheme;
return $extensions?.themes?.find(x => x.themeClassName == usedTheme);
}
);
currentThemeDefinition.subscribe(value => {
if (value?.themeType) {
if (value?.themeType && getCurrentTheme()) {
localStorage.setItem('currentThemeType', value?.themeType);
} else {
localStorage.removeItem('currentThemeType');
}
});
export const openedConnectionsWithTemporary = derived(

View File

@@ -41,6 +41,7 @@
export let tabid;
export let conid;
export let connectionStore = undefined;
export let inlineTabs = false;
export let onlyTestButton;
@@ -237,8 +238,10 @@
<div class="wrapper">
<TabControl
isInline
{inlineTabs}
containerMaxWidth="800px"
flex1={false}
contentTestId="ConnectionTab_tabControlContent"
tabs={[
{
label: 'General',

View File

@@ -15,24 +15,29 @@
<script lang="ts">
import useEditorData from '../query/useEditorData';
import { extensions } from '../stores';
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import { registerFileCommands } from '../commands/stdCommands';
import createUndoReducer from '../utility/createUndoReducer';
import _ from 'lodash';
import { findEngineDriver } from 'dbgate-tools';
import createActivator, { getActiveComponent } from '../utility/createActivator';
import DiagramDesigner from '../designer/DiagramDesigner.svelte';
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
import invalidateCommands from '../commands/invalidateCommands';
import ToolStripSaveButton from '../buttons/ToolStripSaveButton.svelte';
import VerticalSplitter from "../elements/VerticalSplitter.svelte";
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
import ToolStripButton from '../buttons/ToolStripButton.svelte';
import DiagramSettings from '../designer/DiagramSettings.svelte';
import { derived } from 'svelte/store';
import { isProApp } from '../utility/proTools';
export let tabid;
export let conid;
export let database;
export let initialArgs;
let tableCounts = {};
export const activator = createActivator('DiagramTab', true);
@@ -87,6 +92,22 @@
references: [],
});
const setStyle = style =>
// @ts-ignore
dispatchModel({
type: 'compute',
compute: v => ({ ...v, style: _.isFunction(style) ? style(v.style) : style }),
});
const styleDerivedStore = derived(modelState, ($modelState: any) =>
$modelState.value ? $modelState.value.style || {} : {}
);
const styleStore = {
...styleDerivedStore,
update: setStyle,
set: setStyle,
};
function createMenu() {
return [
{ command: 'diagram.save' },
@@ -98,20 +119,56 @@
{ command: 'diagram.redo' },
];
}
function handleReportCounts(counts) {
tableCounts = counts;
}
</script>
<ToolStripContainer>
<VerticalSplitter isSplitter={false}>
<HorizontalSplitter isSplitter={isProApp() ? ($styleStore.settingsVisible ?? true) : false} initialSizeRight={300}>
<svelte:fragment slot="1">
<DiagramDesigner value={$modelState.value || {}} {conid} {database} onChange={handleChange} menu={createMenu} />
<DiagramDesigner
value={$modelState.value || {}}
{conid}
{database}
onChange={handleChange}
menu={createMenu}
columnFilter={$styleStore.columnFilter}
onReportCounts={handleReportCounts}
/>
</svelte:fragment>
</VerticalSplitter>
<svelte:fragment slot="2">
<WidgetColumnBar>
<WidgetColumnBarItem
title="Settings"
name="diagramSettings"
storageName="diagramSettingsWidget"
onClose={() => {
styleStore.update(x => ({ ...x, settingsVisible: false }));
}}
>
<WidgetsInnerContainer skipDefineWidth>
<DiagramSettings values={styleStore} {tableCounts} />
</WidgetsInnerContainer>
</WidgetColumnBarItem>
</WidgetColumnBar>
</svelte:fragment>
</HorizontalSplitter>
<svelte:fragment slot="toolstrip">
<ToolStripCommandButton command="designer.arrange" />
<ToolStripSaveButton idPrefix="diagram" />
<ToolStripCommandButton command="diagram.export" />
<ToolStripCommandButton command="diagram.undo" />
<ToolStripCommandButton command="diagram.redo" />
{#if isProApp()}
<ToolStripButton
icon="icon settings"
on:click={() => {
styleStore.update(x => ({ ...x, settingsVisible: !x.settingsVisible }));
}}>Settings</ToolStripButton
>
{/if}
</svelte:fragment>
</ToolStripContainer>

View File

@@ -5,16 +5,26 @@ import { runGroupCommand } from '../commands/runCommand';
import { currentDropDownMenu, visibleCommandPalette } from '../stores';
import getAsArray from './getAsArray';
let isContextMenuSupressed = false;
export function registerMenu(...items) {
const parentMenu = getContext('componentContextMenu');
setContext('componentContextMenu', [parentMenu, ...items]);
}
export function supressContextMenu() {
isContextMenuSupressed = true;
}
export default function contextMenu(node, items: any = []) {
const handleContextMenu = async e => {
e.preventDefault();
e.stopPropagation();
if (isContextMenuSupressed) {
return;
}
await invalidateCommands();
if (items) {
@@ -24,13 +34,19 @@ export default function contextMenu(node, items: any = []) {
}
};
const handleMouseDown = () => {
isContextMenuSupressed = false;
};
if (items == '__no_menu') return;
node.addEventListener('contextmenu', handleContextMenu);
node.addEventListener('mousedown', handleMouseDown);
return {
destroy() {
node.removeEventListener('contextmenu', handleContextMenu);
node.removeEventListener('mousedown', handleMouseDown);
},
update(value) {
items = value;

View File

@@ -0,0 +1,58 @@
import { supressContextMenu } from './contextMenu';
export default function dragScroll(node, onScroll) {
if (!onScroll) return;
let lastX = null;
let lastY = null;
let sumMoved = 0;
const handleMoveDown = e => {
if (e.button != 2) return;
lastX = e.clientX;
lastY = e.clientY;
document.addEventListener('mousemove', handleMoveMove, true);
document.addEventListener('mouseup', handleMoveEnd, true);
};
const handleMoveMove = e => {
// const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault();
sumMoved += Math.abs(e.clientX - lastX) + Math.abs(e.clientY - lastY);
if (sumMoved > 20) {
supressContextMenu();
}
onScroll(e.clientX - lastX, e.clientY - lastY);
lastX = e.clientX;
lastY = e.clientY;
};
const handleMoveEnd = e => {
// const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault();
e.stopPropagation();
lastX = null;
lastY = null;
document.removeEventListener('mousemove', handleMoveMove, true);
document.removeEventListener('mouseup', handleMoveEnd, true);
};
node.addEventListener('mousedown', handleMoveDown);
return {
destroy() {
node.removeEventListener('mousedown', handleMoveDown);
if (lastX != null || lastY != null) {
document.removeEventListener('mousemove', handleMoveMove, true);
document.removeEventListener('mouseup', handleMoveEnd, true);
}
},
};
}

View File

@@ -11,11 +11,16 @@ export default function moveDrag(node, dragEvents) {
const handleMoveDown = e => {
if (e.button != 0) return;
const zoomKoef = window.getComputedStyle(node)['zoom'];
// const zoomKoef = window.getComputedStyle(node)['zoom'];
const clientRect = node.getBoundingClientRect();
clientX = clientRect.left * zoomKoef;
clientY = clientRect.top * zoomKoef;
clientX = clientRect.left;
clientY = clientRect.top;
// console.log('ZOOM', zoomKoef);
// console.log('CLIENT RECT', clientRect);
// console.log('e.clientX', e.clientX);
// console.log('e.clientY', e.clientY);
startX = e.clientX;
startY = e.clientY;
@@ -25,7 +30,7 @@ export default function moveDrag(node, dragEvents) {
};
const handleMoveMove = e => {
const zoomKoef = window.getComputedStyle(node)['zoom'];
// const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault();
const diffX = e.clientX - startX;
@@ -36,7 +41,7 @@ export default function moveDrag(node, dragEvents) {
onMove(diffX, diffY, e.clientX - clientX, e.clientY - clientY);
};
const handleMoveEnd = e => {
const zoomKoef = window.getComputedStyle(node)['zoom'];
// const zoomKoef = window.getComputedStyle(node)['zoom'];
e.preventDefault();
startX = null;

View File

@@ -1,6 +1,8 @@
import { createGridConfig } from 'dbgate-datalib';
import { writable } from 'svelte/store';
import { onDestroy } from 'svelte';
import { getOpenedTabs, openedTabs } from '../stores';
import _ from 'lodash';
function doLoadGridConfigFunc(tabid) {
try {
@@ -17,9 +19,35 @@ function doLoadGridConfigFunc(tabid) {
return createGridConfig();
}
function containsNotEmptyObject(obj) {
for (const key of Object.keys(obj)) {
if (!_.isEmpty(obj[key])) {
return true;
}
}
return false;
}
export default function useGridConfig(tabid) {
const config = writable(doLoadGridConfigFunc(tabid));
const unsubscribe = config.subscribe(value => localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(value)));
const unsubscribe = config.subscribe(value => {
localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(value));
if (containsNotEmptyObject(value)) {
if (getOpenedTabs().find(x => x.tabid == tabid)?.tabPreviewMode) {
openedTabs.update(tabs =>
tabs.map(x =>
x.tabid == tabid
? {
...x,
tabPreviewMode: false,
}
: x
)
);
}
}
});
onDestroy(unsubscribe);
return config;
}

View File

@@ -0,0 +1,80 @@
<script>
import FormStyledButton from '../buttons/FormStyledButton.svelte';
import { openWebLink } from '../utility/simpleTools';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
</script>
<WidgetsInnerContainer>
<h2>Try DbGate Premium</h2>
<p>Upgrade to get exclusive features:</p>
<ul>
<li>Query designer</li>
<li>Compare database models</li>
<li>Synchronize database structure</li>
<li>Backup &amp; restore database</li>
<li>Advanced ER diagram settings</li>
<li>Export database model</li>
<li>AI assistant</li>
<li>libSQL, Turso, CosmosDB, Redshift support</li>
<li>Amazon and Azure identity providers</li>
<li>E-mail support</li>
</ul>
<h2>Download DbGate Premium</h2>
<ul>
<li>Free 30 day trial</li>
<li>DbGate Premium will reuse your connections and files from DbGate Community</li>
</ul>
<div class="center">
<FormStyledButton on:click={() => openWebLink('https://dbgate.io/download')} value="Download" />
</div>
<h2>Purchase DbGate Premium</h2>
<ul>
<li>Use monthly or yearly subscription</li>
</ul>
<div class="center">
<FormStyledButton on:click={() => openWebLink('https://dbgate.io/purchase/premium')} value="Purchase" />
</div>
</WidgetsInnerContainer>
<style>
h2 {
text-align: center;
}
p {
margin: 10px;
}
ul {
margin: 10px;
list-style: none;
padding: 0;
}
li {
position: relative;
padding-left: 1rem;
margin-bottom: 0.2rem;
}
li:before {
content: '\F0E1E';
font-family: 'Material Design Icons';
color: var(--theme-icon-green);
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
}
.center {
text-align: center;
}
</style>

View File

@@ -22,7 +22,7 @@
<style>
@media (prefers-color-scheme: dark) {
#starting-dbgate {
.starting-dbgate {
background-color: #111;
color: #e3e3e3;
}

View File

@@ -16,7 +16,12 @@
visibleCommandPalette,
} from '../stores';
import { getConnectionLabel } from 'dbgate-tools';
import { useConnectionList, useDatabaseServerVersion, useDatabaseStatus } from '../utility/metadataLoaders';
import {
useConfig,
useConnectionList,
useDatabaseServerVersion,
useDatabaseStatus,
} from '../utility/metadataLoaders';
import { findCommand } from '../commands/runCommand';
import { useConnectionColor } from '../utility/useConnectionColor';
import { apiCall } from '../utility/api';
@@ -27,6 +32,7 @@
$: dbid = connection ? { conid: connection._id, database: databaseName } : null;
$: status = useDatabaseStatus(dbid || {});
$: serverVersion = useDatabaseServerVersion(dbid || {});
$: config = useConfig();
$: contextItems = $statusBarTabInfo[$activeTabId] as any[];
$: connectionLabel = getConnectionLabel(connection, { allowExplicitDatabase: false });
@@ -171,6 +177,13 @@
</div>
{/each}
{#if $config?.isUserLoggedIn && $config?.login}
<div class="item clickable" on:click={() => visibleCommandPalette.set(findCommand('app.loggedUserCommands'))}>
<FontIcon icon="icon users" padRight />
{$config?.login}
</div>
{/if}
{#if $appUpdateStatus}
<div class="item">
<FontIcon icon={$appUpdateStatus.icon} padRight />

View File

@@ -8,6 +8,7 @@
import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
import AdminMenuWidget from './AdminMenuWidget.svelte';
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
</script>
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
@@ -33,3 +34,6 @@
{#if $visibleSelectedWidget == 'admin'}
<AdminMenuWidget />
{/if}
{#if $visibleSelectedWidget == 'premium'}
<AdminPremiumPromoWidget />
{/if}

View File

@@ -12,6 +12,7 @@
} from '../stores';
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
import hasPermission from '../utility/hasPermission';
import { isProApp } from '../utility/proTools';
let domSettings;
let domMainMenu;
@@ -61,6 +62,12 @@
name: 'app',
title: 'Application layers',
},
{
icon: 'icon premium',
name: 'premium',
title: 'Premium promo',
isPremiumPromo: true,
},
// {
// icon: 'icon settings',
// name: 'settings',
@@ -104,7 +111,9 @@
<FontIcon icon="icon menu" />
</div>
{/if}
{#each widgets.filter(x => x && hasPermission(`widgets/${x.name}`)) as item}
{#each widgets
.filter(x => x && hasPermission(`widgets/${x.name}`))
.filter(x => !x.isPremiumPromo || !isProApp()) as item}
<div
class="wrapper"
class:selected={item.name == $visibleSelectedWidget}
@@ -112,6 +121,9 @@
on:click={() => handleChangeWidget(item.name)}
>
<FontIcon icon={item.icon} title={item.title} />
{#if item.isPremiumPromo}
<div class="premium-promo">Premium</div>
{/if}
</div>
{/each}
@@ -141,6 +153,7 @@
align-items: center;
justify-content: center;
color: var(--theme-font-inv-2);
position: relative;
}
.wrapper:hover {
color: var(--theme-font-inv-1);
@@ -154,4 +167,15 @@
flex: 1;
flex-direction: column;
}
.premium-promo {
position: absolute;
text-transform: uppercase;
font-size: 6pt;
background: var(--theme-bg-inv-3);
color: var(--theme-font-inv-2);
padding: 1px 3px;
border-radius: 3px;
bottom: 0;
}
</style>

View File

@@ -3,6 +3,7 @@
export let hideContent = false;
export let fixedWidth = 0;
export let skipDefineWidth = false;
export function scrollTop() {
domDiv.scrollTop = 0;
@@ -13,7 +14,7 @@
on:drop
bind:this={domDiv}
class:hideContent
class:leftFixedWidth={!fixedWidth}
class:leftFixedWidth={!fixedWidth && !skipDefineWidth}
data-testid={$$props['data-testid']}
style:width={fixedWidth ? `${fixedWidth}px` : undefined}
>

View File

@@ -34,7 +34,10 @@ class Analyser extends DatabaseAnalyser {
this.feedback({ analysingMessage: 'Loading columns' });
const columns = await this.analyserQuery('columns', ['tables', 'views']);
this.feedback({ analysingMessage: 'Loading views' });
const views = await this.analyserQuery('views', ['views']);
let views = await this.analyserQuery('views', ['views']);
if (views?.isError) {
views = await this.analyserQuery('viewsNoDefinition', ['views']);
}
const res = {
tables: tables.rows.map((table) => ({
@@ -64,7 +67,7 @@ class Analyser extends DatabaseAnalyser {
...col,
...extractDataType(col.dataType),
})),
createSql: `CREATE VIEW "${view.pureName}"\nAS\n${view.viewDefinition}`,
createSql: view.viewDefinition ? `CREATE VIEW "${view.pureName}"\nAS\n${view.viewDefinition}` : '',
})),
};
this.feedback({ analysingMessage: null });

View File

@@ -1,11 +1,13 @@
const columns = require('./columns');
const tables = require('./tables');
const views = require('./views');
const viewsNoDefinition = require('./viewsNoDefinition');
const tableModifications = require('./tableModifications');
module.exports = {
columns,
tables,
views,
viewsNoDefinition,
tableModifications,
};

View File

@@ -0,0 +1,8 @@
module.exports = `
select
tables.name as "pureName",
tables.uuid as "objectId",
tables.metadata_modification_time as "contentHash"
from system.tables
where tables.database='#DATABASE#' and tables.uuid =OBJECT_ID_CONDITION and tables.engine = 'View'
`;

View File

@@ -164,7 +164,7 @@ class Analyser extends DatabaseAnalyser {
this.feedback({ analysingMessage: 'Loading triggers' });
const triggers = await this.analyserQuery('triggers');
this.feedback({ analysingMessage: 'Loading scehduler events' });
this.feedback({ analysingMessage: 'Loading scheduler events' });
const schedulerEvents = await this.analyserQuery('schedulerEvents');
const uniqueNames = await this.analyserQuery('uniqueNames', ['tables']);

View File

@@ -81,12 +81,25 @@ function splitCommandLine(str) {
const driver = {
...driverBase,
analyserClass: Analyser,
async connect({ server, port, user, password, database, useDatabaseUrl, databaseUrl, treeKeySeparator, ssl }) {
async connect({
server,
port,
user,
password,
database,
useDatabaseUrl,
databaseUrl,
treeKeySeparator,
ssl,
skipSetName,
}) {
let db = 0;
let client;
if (useDatabaseUrl) {
client = new Redis(databaseUrl);
await client.client('SETNAME', 'dbgate');
if (!skipSetName) {
await client.client('SETNAME', 'dbgate');
}
} else {
if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2));
if (_.isNumber(database)) db = database;
@@ -96,15 +109,18 @@ const driver = {
passphrase: ssl.password,
};
}
client = new Redis({
const connectionOptions = {
host: server,
port,
username: user,
password,
db,
connectionName: 'dbgate',
tls: ssl,
});
};
if (!skipSetName) {
connectionOptions.connectionName = 'dbgate';
}
client = new Redis(connectionOptions);
}
return {

View File

@@ -82,6 +82,16 @@ const driver = {
}
return ['server', 'port', 'user', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field);
},
getAdvancedConnectionFields() {
return [
{
type: 'checkbox',
name: 'skipSetName',
label: 'Skip SETNAME instruction',
},
];
},
};
module.exports = driver;

View File

@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro

View File

@@ -43,6 +43,12 @@ jobs:
cd packages/datalib
yarn test:ci
- name: Tools tests
if: always()
run: |
cd packages/tools
yarn test:ci
- uses: tanmen/jest-reporter@v1
if: always()
with: