Merge branch 'master' into feature/duckdb-2

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,6 +8,18 @@ Builds:
- linux - application for linux - linux - application for linux
- win - application for Windows - 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 ### 6.3.0
- ADDED: Support for libSQL and Turso (Premium) - ADDED: Support for libSQL and Turso (Premium)
- ADDED: Native backup and restore database for MySQL and PostgreSQL (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 - FIXED: Scroll in XML cell view, XML view respect themes
- REMOVED: armv7l build for Linux (because of problems with glibc compatibility) - REMOVED: armv7l build for Linux (because of problems with glibc compatibility)
- CHANGED: Upgraded to node:22 for docker builds - 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 ### 6.2.0
- ADDED: Query AI Assistant (Premium) - ADDED: Query AI Assistant (Premium)

View File

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

View File

@@ -13,16 +13,22 @@ describe('Add connection', () => {
it('adds connection', () => { it('adds connection', () => {
// cy.get('[data-testid=ConnectionList_buttonNewConnection]').click(); // cy.get('[data-testid=ConnectionList_buttonNewConnection]').click();
cy.get('[data-testid=ConnectionDriverFields_connectionType]').select('MySQL'); 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_user]').clear().type('root');
cy.get('[data-testid=ConnectionDriverFields_password]').clear().type('Pwd2020Db'); cy.get('[data-testid=ConnectionDriverFields_password]').clear().type('Pwd2020Db');
cy.get('[data-testid=ConnectionDriverFields_port]').clear().type('16004'); cy.get('[data-testid=ConnectionDriverFields_port]').clear().type('16004');
cy.get('[data-testid=ConnectionDriverFields_displayName]').clear().type('test-mysql-1'); cy.get('[data-testid=ConnectionDriverFields_displayName]').clear().type('test-mysql-1');
// test connection // test connection
cy.get('[data-testid=ConnectionTab_buttonTest]').click(); cy.testid('ConnectionTab_buttonTest').click();
cy.contains('Connected:'); 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 // save and connect
cy.get('[data-testid=ConnectionTab_buttonSave]').click(); cy.get('[data-testid=ConnectionTab_buttonSave]').click();
cy.get('[data-testid=ConnectionTab_buttonConnect]').click(); cy.get('[data-testid=ConnectionTab_buttonConnect]').click();

View File

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

View File

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

View File

View File

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

View File

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

View File

@@ -102,12 +102,21 @@ function getPortalCollections() {
trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`], 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'); logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'Using connections from ENV variables');
const noengine = connections.filter(x => !x.engine); const noengine = connections.filter(x => !x.engine);
if (noengine.length > 0) { if (noengine.length > 0) {
logger.warn( logger.warn(
{ connections: noengine.map(x => x._id) }, { connections: noengine.map(x => x._id) },
'Invalid CONNECTIONS configutation, missing ENGINE for connection ID' 'Invalid CONNECTIONS configuration, missing ENGINE for connection ID'
); );
} }
return connections; return connections;

View File

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

View File

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

View File

@@ -33,6 +33,26 @@ function loadEncryptionKey() {
return _encryptionKey; 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; let _encryptor = null;
function getEncryptor() { function getEncryptor() {
@@ -43,35 +63,32 @@ function getEncryptor() {
return _encryptor; return _encryptor;
} }
function encryptPasswordField(connection, field) { function encryptObjectPasswordField(obj, field) {
if ( if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
connection &&
connection[field] &&
!connection[field].startsWith('crypt:') &&
connection.passwordMode != 'saveRaw'
) {
return { return {
...connection, ...obj,
[field]: 'crypt:' + getEncryptor().encrypt(connection[field]), [field]: 'crypt:' + getEncryptor().encrypt(obj[field]),
}; };
} }
return connection; return obj;
} }
function decryptPasswordField(connection, field) { function decryptObjectPasswordField(obj, field) {
if (connection && connection[field] && connection[field].startsWith('crypt:')) { if (obj && obj[field] && obj[field].startsWith('crypt:')) {
return { return {
...connection, ...obj,
[field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)), [field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)),
}; };
} }
return connection; return obj;
} }
function encryptConnection(connection) { function encryptConnection(connection) {
connection = encryptPasswordField(connection, 'password'); if (connection.passwordMode != 'saveRaw') {
connection = encryptPasswordField(connection, 'sshPassword'); connection = encryptObjectPasswordField(connection, 'password');
connection = encryptPasswordField(connection, 'sshKeyfilePassword'); connection = encryptObjectPasswordField(connection, 'sshPassword');
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
}
return connection; return connection;
} }
@@ -81,12 +98,24 @@ function maskConnection(connection) {
} }
function decryptConnection(connection) { function decryptConnection(connection) {
connection = decryptPasswordField(connection, 'password'); connection = decryptObjectPasswordField(connection, 'password');
connection = decryptPasswordField(connection, 'sshPassword'); connection = decryptObjectPasswordField(connection, 'sshPassword');
connection = decryptPasswordField(connection, 'sshKeyfilePassword'); connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
return connection; 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) { function pickSafeConnectionInfo(connection) {
if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) { if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
return connection; return connection;
@@ -99,10 +128,18 @@ function pickSafeConnectionInfo(connection) {
}); });
} }
function setEncryptionKey(encryptionKey) {
_encryptionKey = encryptionKey;
_encryptor = null;
}
module.exports = { module.exports = {
loadEncryptionKey, loadEncryptionKey,
encryptConnection, encryptConnection,
encryptUser,
decryptUser,
decryptConnection, decryptConnection,
maskConnection, maskConnection,
pickSafeConnectionInfo, pickSafeConnectionInfo,
loadEncryptionKeyFromExternal,
}; };

View File

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

View File

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

View File

@@ -196,9 +196,24 @@ export abstract class GridDisplay {
})); }));
} }
setSearchInColumns(searchInColumns: string) {
this.setConfig(cfg => ({
...cfg,
searchInColumns,
}));
}
get hiddenColumnIndexes() { get hiddenColumnIndexes() {
// console.log('GridDisplay.hiddenColumn', this.config.hiddenColumns); // 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) { isColumnChecked(column: DisplayColumn) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,29 @@ import _startCase from 'lodash/startCase';
// childName: string; // 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 { function camelMatch(filter: string, text: string): boolean {
if (!text) return false; if (!text) return false;
if (!filter) return true; if (!filter) return true;
@@ -24,31 +47,27 @@ export function filterName(filter: string, ...names: string[]) {
if (!filter) return true; if (!filter) return true;
// const camelVariants = [name.replace(/[^A-Z]/g, '')] // 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); const namesCompacted = _compact(names);
for (const token of tokens) { for (const factor of tree.factors) {
const found = namesCompacted.find(name => camelMatch(token, name)); let factorOk = true;
if (!found) return false; 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( function clasifyCompoudCategory(tokens: string[], namesCompactedMain: string[], namesCompactedChild: string[]) {
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);
let isMainOnly = true; let isMainOnly = true;
let isChildOnly = true; let isChildOnly = true;
@@ -67,10 +86,42 @@ export function filterNameCompoud(
return 'none'; 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 }[] { export function tokenizeBySearchFilter(text: string, filter: string): { text: string; isMatch: boolean }[] {
const camelTokens = []; const camelTokens = [];
const stdTokens = []; 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) { if (token.replace(/[A-Z]/g, '').length == 0) {
camelTokens.push(token); camelTokens.push(token);
} else { } else {

View File

@@ -26,3 +26,4 @@ export * from './filterBehaviours';
export * from './schemaInfoTools'; export * from './schemaInfoTools';
export * from './dbKeysLoader'; export * from './dbKeysLoader';
export * from './rowProgressReporter'; export * from './rowProgressReporter';
export * from './diagramTools';

View File

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

View File

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

View File

@@ -58,6 +58,24 @@ body {
.relative { .relative {
position: 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 { .col-10 {
flex-basis: 83.3333%; flex-basis: 83.3333%;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -300,7 +300,10 @@
initialValue: $values[`columns_${row}`], initialValue: $values[`columns_${row}`],
sourceTableInfo: $sourceDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == row?.toLowerCase()), sourceTableInfo: $sourceDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == row?.toLowerCase()),
targetTableInfo: $targetDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == targetNameLower), targetTableInfo: $targetDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == targetNameLower),
onConfirm: value => setFieldValue(`columns_${row}`, value), onConfirm: value => {
setFieldValue(`columns_${row}`, value);
targetEditKey += 1;
},
}); });
}} }}
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'} >{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,6 +28,8 @@
selectedWidget, selectedWidget,
lockedDatabaseMode, lockedDatabaseMode,
visibleWidgetSideBar, visibleWidgetSideBar,
currentTheme,
getSystemTheme,
} from '../stores'; } from '../stores';
import { isMac } from '../utility/common'; import { isMac } from '../utility/common';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
@@ -280,6 +282,32 @@ ORDER BY
<svelte:fragment slot="3"> <svelte:fragment slot="3">
<div class="heading">Application theme</div> <div class="heading">Application theme</div>
<FormFieldTemplateLarge
label="Use system theme"
type="checkbox"
labelProps={{
onClick: () => {
if ($currentTheme) {
$currentTheme = null;
} else {
$currentTheme = getSystemTheme();
}
},
}}
>
<CheckboxField
checked={!$currentTheme}
on:change={e => {
if (e.target['checked']) {
$currentTheme = null;
} else {
$currentTheme = getSystemTheme();
}
}}
/>
</FormFieldTemplateLarge>
<div class="themes"> <div class="themes">
{#each $extensions.themes as theme} {#each $extensions.themes as theme}
<ThemeSkeleton {theme} /> <ThemeSkeleton {theme} />
@@ -403,6 +431,12 @@ ORDER BY
<FormCheckboxField name="behaviour.useTabPreviewMode" label="Use tab preview mode" defaultValue={true} /> <FormCheckboxField name="behaviour.useTabPreviewMode" label="Use tab preview mode" defaultValue={true} />
<FormCheckboxField
name="behaviour.jsonPreviewWrap"
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap json in preview' })}
defaultValue={false}
/>
<div class="tip"> <div class="tip">
<FontIcon icon="img tip" /> When you single-click or select a file in the "Tables, Views, Functions" view, it <FontIcon icon="img tip" /> When you single-click or select a file in the "Tables, Views, Functions" view, it
is shown in a preview mode and reuses an existing tab (preview tab). This is useful if you are quickly browsing 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" label="Folder with mysql plugins (for example for authentication). Set only in case of problems"
defaultValue="" defaultValue=""
/> />
<FormTextField <FormTextField
name="externalTools.pg_dump" name="externalTools.pg_dump"
label="pg_dump (backup PostgreSQL database)" label="pg_dump (backup PostgreSQL database)"
defaultValue="pg_dump" defaultValue="pg_dump"
/> />
<FormTextField <FormTextField name="externalTools.psql" label="psql (restore PostgreSQL database)" defaultValue="psql" />
name="externalTools.psql"
label="psql (restore PostgreSQL database)"
defaultValue="psql"
/>
</svelte:fragment> </svelte:fragment>
</TabControl> </TabControl>
</FormValues> </FormValues>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
import { createGridConfig } from 'dbgate-datalib'; import { createGridConfig } from 'dbgate-datalib';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { getOpenedTabs, openedTabs } from '../stores';
import _ from 'lodash';
function doLoadGridConfigFunc(tabid) { function doLoadGridConfigFunc(tabid) {
try { try {
@@ -17,9 +19,35 @@ function doLoadGridConfigFunc(tabid) {
return createGridConfig(); 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) { export default function useGridConfig(tabid) {
const config = writable(doLoadGridConfigFunc(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); onDestroy(unsubscribe);
return config; return config;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -34,7 +34,10 @@ class Analyser extends DatabaseAnalyser {
this.feedback({ analysingMessage: 'Loading columns' }); this.feedback({ analysingMessage: 'Loading columns' });
const columns = await this.analyserQuery('columns', ['tables', 'views']); const columns = await this.analyserQuery('columns', ['tables', 'views']);
this.feedback({ analysingMessage: 'Loading 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 = { const res = {
tables: tables.rows.map((table) => ({ tables: tables.rows.map((table) => ({
@@ -64,7 +67,7 @@ class Analyser extends DatabaseAnalyser {
...col, ...col,
...extractDataType(col.dataType), ...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 }); this.feedback({ analysingMessage: null });

View File

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

View File

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

View File

@@ -164,7 +164,7 @@ class Analyser extends DatabaseAnalyser {
this.feedback({ analysingMessage: 'Loading triggers' }); this.feedback({ analysingMessage: 'Loading triggers' });
const triggers = await this.analyserQuery('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 schedulerEvents = await this.analyserQuery('schedulerEvents');
const uniqueNames = await this.analyserQuery('uniqueNames', ['tables']); const uniqueNames = await this.analyserQuery('uniqueNames', ['tables']);

View File

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

View File

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

View File

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

View File

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