diff --git a/.github/workflows/build-app-pro-beta.yaml b/.github/workflows/build-app-pro-beta.yaml
index a817cd5bb..8644fe3ea 100644
--- a/.github/workflows/build-app-pro-beta.yaml
+++ b/.github/workflows/build-app-pro-beta.yaml
@@ -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
diff --git a/.github/workflows/build-app-pro.yaml b/.github/workflows/build-app-pro.yaml
index 98a1dabd0..c0467a241 100644
--- a/.github/workflows/build-app-pro.yaml
+++ b/.github/workflows/build-app-pro.yaml
@@ -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
diff --git a/.github/workflows/build-cloud-pro.yaml b/.github/workflows/build-cloud-pro.yaml
index f716e2fef..54e618268 100644
--- a/.github/workflows/build-cloud-pro.yaml
+++ b/.github/workflows/build-cloud-pro.yaml
@@ -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
diff --git a/.github/workflows/build-docker-pro.yaml b/.github/workflows/build-docker-pro.yaml
index 1325ba639..cebdfc7de 100644
--- a/.github/workflows/build-docker-pro.yaml
+++ b/.github/workflows/build-docker-pro.yaml
@@ -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
diff --git a/.github/workflows/build-npm-pro.yaml b/.github/workflows/build-npm-pro.yaml
index 30b5d785d..037b4a4fe 100644
--- a/.github/workflows/build-npm-pro.yaml
+++ b/.github/workflows/build-npm-pro.yaml
@@ -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
diff --git a/.github/workflows/e2e-pro.yaml b/.github/workflows/e2e-pro.yaml
index 499fe4c2e..d9b674a4b 100644
--- a/.github/workflows/e2e-pro.yaml
+++ b/.github/workflows/e2e-pro.yaml
@@ -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
diff --git a/.github/workflows/run-tests.yaml b/.github/workflows/run-tests.yaml
index 57eaaee99..6010cdb05 100644
--- a/.github/workflows/run-tests.yaml
+++ b/.github/workflows/run-tests.yaml
@@ -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:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f20a1ad35..78c548998 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
diff --git a/app/src/electron.js b/app/src/electron.js
index fd1a8a3d5..6340a2b74 100644
--- a/app/src/electron.js
+++ b/app/src/electron.js
@@ -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',
diff --git a/e2e-tests/cypress/e2e/add-connection.cy.js b/e2e-tests/cypress/e2e/add-connection.cy.js
index 3b71770fb..06b9916f1 100644
--- a/e2e-tests/cypress/e2e/add-connection.cy.js
+++ b/e2e-tests/cypress/e2e/add-connection.cy.js
@@ -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();
diff --git a/e2e-tests/cypress/e2e/browse-data.cy.js b/e2e-tests/cypress/e2e/browse-data.cy.js
index e9ccb870b..6bcb59f04 100644
--- a/e2e-tests/cypress/e2e/browse-data.cy.js
+++ b/e2e-tests/cypress/e2e/browse-data.cy.js
@@ -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');
});
});
diff --git a/e2e-tests/cypress/e2e/team.cy.js b/e2e-tests/cypress/e2e/team.cy.js
index a1f03a2d4..5cb3dfe8f 100644
--- a/e2e-tests/cypress/e2e/team.cy.js
+++ b/e2e-tests/cypress/e2e/team.cy.js
@@ -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');
});
});
diff --git a/e2e-tests/screenshots/.gitkeep b/e2e-tests/screenshots/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/e2e-tests/screenshots/dummy.txt b/e2e-tests/screenshots/dummy.txt
deleted file mode 100644
index 59633d601..000000000
--- a/e2e-tests/screenshots/dummy.txt
+++ /dev/null
@@ -1 +0,0 @@
-Folder with screenshots
\ No newline at end of file
diff --git a/package.json b/package.json
index 2797b6839..596175081 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"private": true,
- "version": "6.3.1",
+ "version": "6.3.2",
"name": "dbgate-all",
"workspaces": [
"packages/*",
diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js
index 0894e60fa..bf6b86544 100644
--- a/packages/api/src/controllers/connections.js
+++ b/packages/api/src/controllers/connections.js
@@ -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;
diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js
index 49ec040cc..d44fb2863 100644
--- a/packages/api/src/controllers/files.js
+++ b/packages/api/src/controllers/files.js
@@ -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;
},
diff --git a/packages/api/src/proc/connectProcess.js b/packages/api/src/proc/connectProcess.js
index 39560e6bf..0b6990ef0 100644
--- a/packages/api/src/proc/connectProcess.js
+++ b/packages/api/src/proc/connectProcess.js
@@ -38,6 +38,8 @@ function start() {
detail: formatErrorDetail(e, connection),
});
}
+
+ process.exit(0);
});
}
diff --git a/packages/api/src/utility/crypting.js b/packages/api/src/utility/crypting.js
index f24ede6a7..95220e6d6 100644
--- a/packages/api/src/utility/crypting.js
+++ b/packages/api/src/utility/crypting.js
@@ -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,
};
diff --git a/packages/api/src/utility/getDiagramExport.js b/packages/api/src/utility/getDiagramExport.js
index 3e45fa459..1dfb49392 100644
--- a/packages/api/src/utility/getDiagramExport.js
+++ b/packages/api/src/utility/getDiagramExport.js
@@ -1,4 +1,11 @@
-const getDiagramExport = (html, css, themeType, themeClassName) => {
+const getDiagramExport = (html, css, themeType, themeClassName, watermark) => {
+ const watermarkHtml = watermark
+ ? `
+
+ ${watermark}
+
+ `
+ : '';
return `
@@ -13,10 +20,44 @@ const getDiagramExport = (html, css, themeType, themeClassName) => {
+
+
-
+
${html}
+ ${watermarkHtml}
`;
diff --git a/packages/datalib/src/GridConfig.ts b/packages/datalib/src/GridConfig.ts
index 48dcf39b5..650e88217 100644
--- a/packages/datalib/src/GridConfig.ts
+++ b/packages/datalib/src/GridConfig.ts
@@ -29,8 +29,8 @@ export interface GridConfig extends GridConfigColumns {
isFormView?: boolean;
formViewRecordNumber?: number;
formFilterColumns: string[];
- formColumnFilterText?: string;
multiColumnFilter?: string;
+ searchInColumns?: string;
}
export interface GridCache {
diff --git a/packages/datalib/src/GridDisplay.ts b/packages/datalib/src/GridDisplay.ts
index 48d91910e..5243f4967 100644
--- a/packages/datalib/src/GridDisplay.ts
+++ b/packages/datalib/src/GridDisplay.ts
@@ -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) {
diff --git a/packages/tools/jest.config.js b/packages/tools/jest.config.js
new file mode 100644
index 000000000..790050941
--- /dev/null
+++ b/packages/tools/jest.config.js
@@ -0,0 +1,5 @@
+module.exports = {
+ preset: 'ts-jest',
+ testEnvironment: 'node',
+ moduleFileExtensions: ['js'],
+};
diff --git a/packages/tools/package.json b/packages/tools/package.json
index ae25b21b5..8865a9f37 100644
--- a/packages/tools/package.json
+++ b/packages/tools/package.json
@@ -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": {
diff --git a/packages/tools/src/DatabaseAnalyser.ts b/packages/tools/src/DatabaseAnalyser.ts
index 31c86d93a..0ebb69472 100644
--- a/packages/tools/src/DatabaseAnalyser.ts
+++ b/packages/tools/src/DatabaseAnalyser.ts
@@ -354,6 +354,7 @@ export class DatabaseAnalyser {
logger.error(extractErrorLogData(err, { template }), 'Error running analyser query');
return {
rows: [],
+ isError: true,
};
}
}
diff --git a/packages/tools/src/diagramTools.ts b/packages/tools/src/diagramTools.ts
new file mode 100644
index 000000000..8fded94c2
--- /dev/null
+++ b/packages/tools/src/diagramTools.ts
@@ -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)';
diff --git a/packages/tools/src/filterName.test.ts b/packages/tools/src/filterName.test.ts
new file mode 100644
index 000000000..4ac4ba448
--- /dev/null
+++ b/packages/tools/src/filterName.test.ts
@@ -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 },
+ ]);
+});
diff --git a/packages/tools/src/filterName.ts b/packages/tools/src/filterName.ts
index a0be15a1d..07db7a4ff 100644
--- a/packages/tools/src/filterName.ts
+++ b/packages/tools/src/filterName.ts
@@ -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 {
diff --git a/packages/tools/src/index.ts b/packages/tools/src/index.ts
index 48c137d11..0369abc59 100644
--- a/packages/tools/src/index.ts
+++ b/packages/tools/src/index.ts
@@ -25,4 +25,5 @@ export * from './detectSqlFilterBehaviour';
export * from './filterBehaviours';
export * from './schemaInfoTools';
export * from './dbKeysLoader';
-export * from './rowProgressReporter';
\ No newline at end of file
+export * from './rowProgressReporter';
+export * from './diagramTools';
diff --git a/packages/tools/src/structureTools.ts b/packages/tools/src/structureTools.ts
index 96150dec3..342922b71 100644
--- a/packages/tools/src/structureTools.ts
+++ b/packages/tools/src/structureTools.ts
@@ -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),
})),
diff --git a/packages/types/engines.d.ts b/packages/types/engines.d.ts
index e2d6abfb0..a6fc762b6 100644
--- a/packages/types/engines.d.ts
+++ b/packages/types/engines.d.ts
@@ -306,6 +306,7 @@ export interface EngineDriver extends FilterBehaviourProvider {
command: 'backup' | 'restore'
): { message: string; severity: 'info' | 'error' | 'debug' } | null;
getNativeOperationFormArgs(operation: 'backup' | 'restore'): any[];
+ getAdvancedConnectionFields(): any[];
analyserClass?: any;
dumperClass?: any;
diff --git a/packages/web/public/global.css b/packages/web/public/global.css
index f4f427d87..d39a035f9 100644
--- a/packages/web/public/global.css
+++ b/packages/web/public/global.css
@@ -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%;
diff --git a/packages/web/src/Screen.svelte b/packages/web/src/Screen.svelte
index 497412c64..b7262d489 100644
--- a/packages/web/src/Screen.svelte
+++ b/packages/web/src/Screen.svelte
@@ -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 @@
e.preventDefault()}
diff --git a/packages/web/src/buttons/CloseSearchButton.svelte b/packages/web/src/buttons/CloseSearchButton.svelte
index 693054d22..de9ee17ef 100644
--- a/packages/web/src/buttons/CloseSearchButton.svelte
+++ b/packages/web/src/buttons/CloseSearchButton.svelte
@@ -5,13 +5,18 @@
export let filter;
export let showDisabled = false;
+ export let onClearFilter = null;
{#if filter || showDisabled}
{
- filter = '';
+ if (onClearFilter) {
+ onClearFilter();
+ } else {
+ filter = '';
+ }
}}
title="Clear filter"
disabled={!filter}
diff --git a/packages/web/src/buttons/ToolStripContainer.svelte b/packages/web/src/buttons/ToolStripContainer.svelte
index e0a36466d..496af1729 100644
--- a/packages/web/src/buttons/ToolStripContainer.svelte
+++ b/packages/web/src/buttons/ToolStripContainer.svelte
@@ -16,7 +16,7 @@
-
+
@@ -41,6 +41,10 @@
max-height: 100%;
}
+ .content.isComponentActive {
+ max-height: calc(100% - 30px);
+ }
+
.toolstrip {
display: flex;
flex-wrap: wrap;
diff --git a/packages/web/src/buttons/ToolStripDropDownButton.svelte b/packages/web/src/buttons/ToolStripDropDownButton.svelte
index 5a06ab9ab..b84536163 100644
--- a/packages/web/src/buttons/ToolStripDropDownButton.svelte
+++ b/packages/web/src/buttons/ToolStripDropDownButton.svelte
@@ -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 });
}
diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts
index ba6d3e43d..8b99b6584 100644
--- a/packages/web/src/commands/stdCommands.ts
+++ b/packages/web/src/commands/stdCommands.ts
@@ -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',
diff --git a/packages/web/src/datagrid/ColumnHeaderControl.svelte b/packages/web/src/datagrid/ColumnHeaderControl.svelte
index 6083c680d..183d9d346 100644
--- a/packages/web/src/datagrid/ColumnHeaderControl.svelte
+++ b/packages/web/src/datagrid/ColumnHeaderControl.svelte
@@ -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()}
{/if}
-
+
{#if _.isString(column.displayedDataType || column.dataType) && !order}
diff --git a/packages/web/src/datagrid/ColumnManager.svelte b/packages/web/src/datagrid/ColumnManager.svelte
index 76031e8ac..fbc024457 100644
--- a/packages/web/src/datagrid/ColumnManager.svelte
+++ b/packages/web/src/datagrid/ColumnManager.svelte
@@ -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 @@
{/if}
-
-
+ display.setSearchInColumns(value)}
+ data-testid="ColumnManager_searchColumns"
+ />
+ display.setSearchInColumns('')} />
{#if isDynamicStructure && !isJsonView}
{
@@ -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];
diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte
index d1d619723..a24acd674 100644
--- a/packages/web/src/datagrid/DataGridCore.svelte
+++ b/packages/web/src/datagrid/DataGridCore.svelte
@@ -1933,6 +1933,7 @@
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
grouping={display.getGrouping(col.uniqueName)}
{allowDefineVirtualReferences}
+ seachInColumns={display.config?.searchInColumns}
/>
{/each}
diff --git a/packages/web/src/designer/ColumnLine.svelte b/packages/web/src/designer/ColumnLine.svelte
index 26065169d..09e2ca5e7 100644
--- a/packages/web/src/designer/ColumnLine.svelte
+++ b/packages/web/src/designer/ColumnLine.svelte
@@ -195,12 +195,12 @@
{#if designer?.style?.showNullability || designer?.style?.showDataType}
{#if designer?.style?.showDataType && column?.dataType}
-
+
{(column?.displayedDataType || column?.dataType).toLowerCase()}
{/if}
{#if designer?.style?.showNullability}
-
+
{column?.notNull ? 'NOT NULL' : 'NULL'}
{/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);
+ }
diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte
index da5542992..ed457d629 100644
--- a/packages/web/src/designer/Designer.svelte
+++ b/packages/web/src/designer/Designer.svelte
@@ -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, '
$1');
+
+ 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,
+ });
+ }
-
+
{#if !(tables?.length > 0)}
Drag & drop tables or views from left panel here
{/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}