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}
-
+
{#if $$slots.header}
diff --git a/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte b/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte index eec4164e4..ddacf3375 100644 --- a/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte +++ b/packages/web/src/settings/ConnectionAdvancedDriverFields.svelte @@ -1,13 +1,24 @@ + +{#if advancedFields} + +{/if} diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index e91874400..2dba84f96 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -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
Application theme
+ + { + if ($currentTheme) { + $currentTheme = null; + } else { + $currentTheme = getSystemTheme(); + } + }, + }} + > + { + if (e.target['checked']) { + $currentTheme = null; + } else { + $currentTheme = getSystemTheme(); + } + }} + /> + +
{#each $extensions.themes as theme} @@ -403,6 +431,12 @@ ORDER BY + +
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="" /> - - + diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index fbcb73cb2..dac8c4f02 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -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(defaultValue: T, storageName) { +export function getSystemTheme() { + return darkModeMediaQuery?.matches ? 'theme-dark' : 'theme-light'; +} + +export function writableWithStorage(defaultValue: T, storageName, removeCondition?: (value: T) => boolean) { const init = localStorage.getItem(storageName); const res = writable(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(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( diff --git a/packages/web/src/tabs/ConnectionTab.svelte b/packages/web/src/tabs/ConnectionTab.svelte index 20604f363..8c1da2f5c 100644 --- a/packages/web/src/tabs/ConnectionTab.svelte +++ b/packages/web/src/tabs/ConnectionTab.svelte @@ -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 @@
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; + } - + - + - - + + + { + styleStore.update(x => ({ ...x, settingsVisible: false })); + }} + > + + + + + + + + + {#if isProApp()} + { + styleStore.update(x => ({ ...x, settingsVisible: !x.settingsVisible })); + }}>Settings + {/if} diff --git a/packages/web/src/utility/contextMenu.ts b/packages/web/src/utility/contextMenu.ts index 20a5c6b91..d5da4d202 100644 --- a/packages/web/src/utility/contextMenu.ts +++ b/packages/web/src/utility/contextMenu.ts @@ -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; diff --git a/packages/web/src/utility/dragScroll.ts b/packages/web/src/utility/dragScroll.ts new file mode 100644 index 000000000..2bd897d65 --- /dev/null +++ b/packages/web/src/utility/dragScroll.ts @@ -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); + } + }, + }; +} diff --git a/packages/web/src/utility/moveDrag.ts b/packages/web/src/utility/moveDrag.ts index 8712398b5..858ce875f 100644 --- a/packages/web/src/utility/moveDrag.ts +++ b/packages/web/src/utility/moveDrag.ts @@ -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; diff --git a/packages/web/src/utility/useGridConfig.ts b/packages/web/src/utility/useGridConfig.ts index af453a42c..73653767b 100644 --- a/packages/web/src/utility/useGridConfig.ts +++ b/packages/web/src/utility/useGridConfig.ts @@ -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; } diff --git a/packages/web/src/widgets/AdminPremiumPromoWidget.svelte b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte new file mode 100644 index 000000000..b7560eeb8 --- /dev/null +++ b/packages/web/src/widgets/AdminPremiumPromoWidget.svelte @@ -0,0 +1,80 @@ + + + +

Try DbGate Premium

+ +

Upgrade to get exclusive features:

+ +
    +
  • Query designer
  • +
  • Compare database models
  • +
  • Synchronize database structure
  • +
  • Backup & restore database
  • +
  • Advanced ER diagram settings
  • +
  • Export database model
  • +
  • AI assistant
  • +
  • libSQL, Turso, CosmosDB, Redshift support
  • +
  • Amazon and Azure identity providers
  • +
  • E-mail support
  • +
+ +

Download DbGate Premium

+
    +
  • Free 30 day trial
  • +
  • DbGate Premium will reuse your connections and files from DbGate Community
  • +
+ +
+ openWebLink('https://dbgate.io/download')} value="Download" /> +
+ +

Purchase DbGate Premium

+
    +
  • Use monthly or yearly subscription
  • +
+ +
+ openWebLink('https://dbgate.io/purchase/premium')} value="Purchase" /> +
+
+ + diff --git a/packages/web/src/widgets/AppStartInfo.svelte b/packages/web/src/widgets/AppStartInfo.svelte index 0597874da..8b8e8ba31 100644 --- a/packages/web/src/widgets/AppStartInfo.svelte +++ b/packages/web/src/widgets/AppStartInfo.svelte @@ -22,7 +22,7 @@ diff --git a/packages/web/src/widgets/WidgetsInnerContainer.svelte b/packages/web/src/widgets/WidgetsInnerContainer.svelte index 5682b1d8e..ad8ff1a98 100644 --- a/packages/web/src/widgets/WidgetsInnerContainer.svelte +++ b/packages/web/src/widgets/WidgetsInnerContainer.svelte @@ -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} > diff --git a/plugins/dbgate-plugin-clickhouse/src/backend/Analyser.js b/plugins/dbgate-plugin-clickhouse/src/backend/Analyser.js index 774784494..08bd453f8 100644 --- a/plugins/dbgate-plugin-clickhouse/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-clickhouse/src/backend/Analyser.js @@ -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 }); diff --git a/plugins/dbgate-plugin-clickhouse/src/backend/sql/index.js b/plugins/dbgate-plugin-clickhouse/src/backend/sql/index.js index 6465f821b..1a5545817 100644 --- a/plugins/dbgate-plugin-clickhouse/src/backend/sql/index.js +++ b/plugins/dbgate-plugin-clickhouse/src/backend/sql/index.js @@ -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, }; diff --git a/plugins/dbgate-plugin-clickhouse/src/backend/sql/viewsNoDefinition.js b/plugins/dbgate-plugin-clickhouse/src/backend/sql/viewsNoDefinition.js new file mode 100644 index 000000000..9d3706822 --- /dev/null +++ b/plugins/dbgate-plugin-clickhouse/src/backend/sql/viewsNoDefinition.js @@ -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' +`; diff --git a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js index c51bd310a..27402eca8 100644 --- a/plugins/dbgate-plugin-mysql/src/backend/Analyser.js +++ b/plugins/dbgate-plugin-mysql/src/backend/Analyser.js @@ -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']); diff --git a/plugins/dbgate-plugin-redis/src/backend/driver.js b/plugins/dbgate-plugin-redis/src/backend/driver.js index 0ce950c9c..aab53f939 100644 --- a/plugins/dbgate-plugin-redis/src/backend/driver.js +++ b/plugins/dbgate-plugin-redis/src/backend/driver.js @@ -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 { diff --git a/plugins/dbgate-plugin-redis/src/frontend/driver.js b/plugins/dbgate-plugin-redis/src/frontend/driver.js index e3bc599d0..d440ad2e7 100644 --- a/plugins/dbgate-plugin-redis/src/frontend/driver.js +++ b/plugins/dbgate-plugin-redis/src/frontend/driver.js @@ -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; diff --git a/workflow-templates/includes.tpl.yaml b/workflow-templates/includes.tpl.yaml index 39af10a30..c0e16e508 100644 --- a/workflow-templates/includes.tpl.yaml +++ b/workflow-templates/includes.tpl.yaml @@ -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 diff --git a/workflow-templates/run-tests.yaml b/workflow-templates/run-tests.yaml index c774e7a9c..a8eac8a2f 100644 --- a/workflow-templates/run-tests.yaml +++ b/workflow-templates/run-tests.yaml @@ -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: