mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 22:36:01 +00:00
Merge branch 'master' into feature/duckdb-2
This commit is contained in:
2
.github/workflows/build-app-pro-beta.yaml
vendored
2
.github/workflows/build-app-pro-beta.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-app-pro.yaml
vendored
2
.github/workflows/build-app-pro.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-cloud-pro.yaml
vendored
2
.github/workflows/build-cloud-pro.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-docker-pro.yaml
vendored
2
.github/workflows/build-docker-pro.yaml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/build-npm-pro.yaml
vendored
2
.github/workflows/build-npm-pro.yaml
vendored
@@ -32,7 +32,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
2
.github/workflows/e2e-pro.yaml
vendored
2
.github/workflows/e2e-pro.yaml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
5
.github/workflows/run-tests.yaml
vendored
5
.github/workflows/run-tests.yaml
vendored
@@ -37,6 +37,11 @@ jobs:
|
||||
run: |
|
||||
cd packages/datalib
|
||||
yarn test:ci
|
||||
- name: Tools tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/tools
|
||||
yarn test:ci
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
|
||||
14
CHANGELOG.md
14
CHANGELOG.md
@@ -8,6 +8,18 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
### 6.3.2
|
||||
- ADDED: "Use system theme" switch, use changed system theme without restart #1084
|
||||
- ADDED: "Skip SETNAME instruction" option for Redis #1077
|
||||
- FIXED: Clickhouse views are now available even for user with limited permissions #1076
|
||||
- ADDED: Multiple-token search delimited with comma (=OR) in structure search boxes
|
||||
- CHANGED: When filtering columns in data browser, data view shows only filtered columns
|
||||
- ADDED: Advanced settings for diagrams (Premium)
|
||||
- ADDED: Diagrams - zoom with Ctrl+mouse wheel
|
||||
- FIXED: Scrollable diagram exports + scroll by mouse drag
|
||||
- FIXED: Fixed many problems in diagrams when zoom is applied
|
||||
- FIXED: Correctly end connection process after succesful/unsuccesful connect
|
||||
|
||||
### 6.3.0
|
||||
- ADDED: Support for libSQL and Turso (Premium)
|
||||
- ADDED: Native backup and restore database for MySQL and PostgreSQL (Premium)
|
||||
@@ -29,7 +41,7 @@ Builds:
|
||||
- FIXED: Scroll in XML cell view, XML view respect themes
|
||||
- REMOVED: armv7l build for Linux (because of problems with glibc compatibility)
|
||||
- CHANGED: Upgraded to node:22 for docker builds
|
||||
- CHANGED: Upgraded SQLite engine version (better-sqlite3@11.8.1)
|
||||
- CHANGED: Upgraded SQLite engine version
|
||||
|
||||
### 6.2.0
|
||||
- ADDED: Query AI Assistant (Premium)
|
||||
|
||||
@@ -357,6 +357,7 @@ function createWindow() {
|
||||
title: isProApp() ? 'DbGate Premium' : 'DbGate',
|
||||
frame: useNativeMenu,
|
||||
titleBarStyle: useNativeMenu ? undefined : 'hidden',
|
||||
backgroundColor: electron.nativeTheme.shouldUseDarkColors ? '#111111' : undefined,
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
partition: isProApp() ? 'persist:dbgate-premium' : 'persist:dbgate',
|
||||
|
||||
@@ -13,16 +13,22 @@ describe('Add connection', () => {
|
||||
it('adds connection', () => {
|
||||
// cy.get('[data-testid=ConnectionList_buttonNewConnection]').click();
|
||||
cy.get('[data-testid=ConnectionDriverFields_connectionType]').select('MySQL');
|
||||
cy.themeshot('connection');
|
||||
cy.themeshot('new-connection');
|
||||
cy.get('[data-testid=ConnectionDriverFields_user]').clear().type('root');
|
||||
cy.get('[data-testid=ConnectionDriverFields_password]').clear().type('Pwd2020Db');
|
||||
cy.get('[data-testid=ConnectionDriverFields_port]').clear().type('16004');
|
||||
cy.get('[data-testid=ConnectionDriverFields_displayName]').clear().type('test-mysql-1');
|
||||
|
||||
// test connection
|
||||
cy.get('[data-testid=ConnectionTab_buttonTest]').click();
|
||||
cy.testid('ConnectionTab_buttonTest').click();
|
||||
cy.contains('Connected:');
|
||||
|
||||
cy.testid('ConnectionTab_tabSshTunnel').click();
|
||||
cy.testid('ConnectionTab_tabControlContent').themeshot('connection-sshtunnel-window', { padding: 50 });
|
||||
|
||||
cy.testid('ConnectionTab_tabSsl').click();
|
||||
cy.testid('ConnectionTab_tabControlContent').themeshot('connection-ssl-window', { padding: 50 });
|
||||
|
||||
// save and connect
|
||||
cy.get('[data-testid=ConnectionTab_buttonSave]').click();
|
||||
cy.get('[data-testid=ConnectionTab_buttonConnect]').click();
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Finished job script');
|
||||
cy.contains('Album.csv');
|
||||
cy.testid('WidgetIconPanel_database').click();
|
||||
cy.themeshot('exportcsv');
|
||||
cy.themeshot('configure-export-csv');
|
||||
});
|
||||
|
||||
it('Data archive editor - macros', () => {
|
||||
@@ -42,7 +42,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Out Of Exile').click({ shiftKey: true });
|
||||
cy.contains('Change text case').click();
|
||||
cy.contains('AUDIOSLAVE');
|
||||
cy.themeshot('freetable');
|
||||
cy.themeshot('data-archive-macros');
|
||||
});
|
||||
|
||||
it('Load table data', () => {
|
||||
@@ -87,19 +87,19 @@ describe('Data browser data', () => {
|
||||
cy.contains('Album').click();
|
||||
cy.testid('DataFilterControl_input_Title').type('Rock{enter}');
|
||||
cy.contains('Rows: 7');
|
||||
cy.testid('DataFilterControl_input_AlbumId').type('>10{enter}');
|
||||
cy.contains('Rows: 5');
|
||||
cy.testid('DataFilterControl_input_AlbumId').type('>10xxx{enter}');
|
||||
cy.contains('Rows: 7');
|
||||
cy.testid('DataFilterControl_filtermenu_Title').click();
|
||||
cy.themeshot('filter');
|
||||
// hide what is not needed
|
||||
cy.testid('WidgetIconPanel_database').click();
|
||||
cy.testid('DataGrid_itemReferences').click();
|
||||
cy.themeshot('data-browser-filter');
|
||||
cy.testid('DataGridCore_button_clearFilters').click();
|
||||
cy.contains('Rows: 347');
|
||||
});
|
||||
|
||||
it('Data grid screenshots', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.window().then(win => {
|
||||
win.__changeCurrentTheme('theme-dark');
|
||||
});
|
||||
|
||||
cy.contains('MyChinook').click();
|
||||
|
||||
@@ -114,16 +114,25 @@ describe('Data browser data', () => {
|
||||
cy.contains('PgChinook').click();
|
||||
cy.contains('customer').click();
|
||||
cy.contains('Leonie').click();
|
||||
cy.themeshot('datagrid');
|
||||
cy.themeshot('common-data-browser');
|
||||
|
||||
cy.contains('invoice').click();
|
||||
cy.contains('invoice_line (invoice_id)').click();
|
||||
cy.themeshot('masterdetail');
|
||||
cy.themeshot('data-browser-master-detail');
|
||||
|
||||
cy.contains('9, Place Louis Barthou').click();
|
||||
cy.contains('Switch to form').click();
|
||||
cy.contains('Switch to table'); // test that we are in form view
|
||||
cy.themeshot('formview');
|
||||
cy.themeshot('data-browser-form-view');
|
||||
});
|
||||
|
||||
it.only('Column search', () => {
|
||||
cy.contains('MySql-connection').click();
|
||||
cy.contains('MyChinook').click();
|
||||
cy.contains('Customer').click();
|
||||
cy.testid('ColumnManager_searchColumns').clear().type('name,id{enter}');
|
||||
cy.contains('Company').should('not.exist');
|
||||
cy.themeshot('data-browser-column-search');
|
||||
});
|
||||
|
||||
it('SQL Gen', () => {
|
||||
@@ -131,7 +140,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('PgChinook').rightclick();
|
||||
cy.contains('SQL Generator').click();
|
||||
cy.contains('Check all').click();
|
||||
cy.themeshot('sqlgen');
|
||||
cy.themeshot('sql-generator');
|
||||
});
|
||||
|
||||
it('Macros in DB', () => {
|
||||
@@ -146,7 +155,7 @@ describe('Data browser data', () => {
|
||||
cy.testid('DataGrid_itemMacros').click();
|
||||
cy.contains('Change text case').click();
|
||||
cy.contains('NIELSEN');
|
||||
cy.themeshot('macros');
|
||||
cy.themeshot('data-browser-macros');
|
||||
});
|
||||
|
||||
it('Perspectives', () => {
|
||||
@@ -162,7 +171,7 @@ describe('Data browser data', () => {
|
||||
// check track is loaded
|
||||
cy.contains('Put The Finger On You');
|
||||
|
||||
cy.themeshot('perspective1');
|
||||
cy.themeshot('perspective-designer');
|
||||
});
|
||||
|
||||
it('Query editor - code completion', () => {
|
||||
@@ -176,7 +185,7 @@ describe('Data browser data', () => {
|
||||
cy.get('body').realType('select * from Album where Album.');
|
||||
// code completion
|
||||
cy.contains('ArtistId');
|
||||
cy.themeshot('query');
|
||||
cy.themeshot('query-editor-code-completion');
|
||||
});
|
||||
|
||||
it('Query editor - join wizard', () => {
|
||||
@@ -189,7 +198,7 @@ describe('Data browser data', () => {
|
||||
cy.get('body').realPress(['Control', 'j']);
|
||||
// JOIN wizard
|
||||
cy.contains('INNER JOIN Customer ON Invoice.CustomerId = Customer.CustomerId');
|
||||
cy.themeshot('joinwizard');
|
||||
cy.themeshot('query-editor-join-wizard');
|
||||
});
|
||||
|
||||
it('Mongo JSON data view', () => {
|
||||
@@ -206,7 +215,7 @@ describe('Data browser data', () => {
|
||||
cy.testid('WidgetIconPanel_cell-data').click();
|
||||
// test JSON view
|
||||
cy.contains('Country: "Brazil"');
|
||||
cy.themeshot('mongoquery');
|
||||
cy.themeshot('mongo-query-json-view');
|
||||
});
|
||||
|
||||
it('SQL preview', () => {
|
||||
@@ -216,7 +225,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Show SQL').click();
|
||||
// index should be part of create script
|
||||
cy.contains('CREATE INDEX `IFK_CustomerSupportRepId`');
|
||||
cy.themeshot('sqlpreview');
|
||||
cy.themeshot('sql-preview-create-index');
|
||||
});
|
||||
|
||||
it('Query designer', () => {
|
||||
@@ -225,7 +234,7 @@ describe('Data browser data', () => {
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
cy.contains('customer').click();
|
||||
// cy.contains('left join').rightclick();
|
||||
cy.themeshot('querydesigner');
|
||||
cy.themeshot('query-designer');
|
||||
});
|
||||
|
||||
it('Database diagram', () => {
|
||||
@@ -236,7 +245,7 @@ describe('Data browser data', () => {
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
// check diagram is shown
|
||||
cy.contains('MediaTypeId');
|
||||
cy.themeshot('diagram');
|
||||
cy.themeshot('database-diagram');
|
||||
});
|
||||
|
||||
it('Charts', () => {
|
||||
@@ -245,7 +254,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('line-chart').click();
|
||||
cy.testid('TabsPanel_buttonSplit').click();
|
||||
cy.testid('WidgetIconPanel_file').click();
|
||||
cy.themeshot('charts');
|
||||
cy.themeshot('view-split-charts');
|
||||
});
|
||||
|
||||
it('Keyboard configuration', () => {
|
||||
@@ -253,7 +262,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Keyboard shortcuts').click();
|
||||
cy.contains('dataForm.refresh').click();
|
||||
cy.testid('CommandModal_keyboardButton').click();
|
||||
cy.themeshot('keyboard');
|
||||
cy.themeshot('keyboard-configuration');
|
||||
});
|
||||
|
||||
it('Command palette', () => {
|
||||
@@ -264,7 +273,7 @@ describe('Data browser data', () => {
|
||||
// cy.realPress('F1');
|
||||
cy.realPress('PageDown');
|
||||
cy.realPress('PageDown');
|
||||
cy.testid('CommandPalette_main').themeshot('commandpalette', { padding: 50 });
|
||||
cy.testid('CommandPalette_main').themeshot('command-palette', { padding: 50 });
|
||||
});
|
||||
|
||||
it('Show map', () => {
|
||||
@@ -277,7 +286,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('13.9').click({ shiftKey: true });
|
||||
cy.testid('WidgetIconPanel_cell-data').click();
|
||||
cy.wait(2000);
|
||||
cy.themeshot('map');
|
||||
cy.themeshot('cell-map-view');
|
||||
});
|
||||
|
||||
it('Search in connections', () => {
|
||||
@@ -289,7 +298,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Album').click();
|
||||
cy.testid('SqlObjectList_searchMenuDropDown').click();
|
||||
cy.contains('Column name').click();
|
||||
cy.themeshot('connsearch');
|
||||
cy.themeshot('search-in-connections');
|
||||
});
|
||||
|
||||
it('Plugin tab', () => {
|
||||
@@ -299,7 +308,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Total white theme');
|
||||
// wait for load logos
|
||||
cy.wait(2000);
|
||||
cy.themeshot('plugin');
|
||||
cy.themeshot('view-plugin-tab');
|
||||
});
|
||||
|
||||
it('Edit mongo data JSON', () => {
|
||||
@@ -326,7 +335,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Helena').rightclick();
|
||||
cy.contains('Delete document').click();
|
||||
cy.contains('Save').click();
|
||||
cy.themeshot('mongosave');
|
||||
cy.themeshot('save-changes-mongodb');
|
||||
});
|
||||
|
||||
it('Edit mongo data JSON', () => {
|
||||
@@ -340,7 +349,7 @@ describe('Data browser data', () => {
|
||||
cy.testid('ColumnManagerRow_checkbox__id').click();
|
||||
cy.testid('DataFilterControl_input_countries.1').type('EXISTS{enter}');
|
||||
cy.testid('WidgetIconPanel_cell-data').click();
|
||||
cy.themeshot('collection');
|
||||
cy.themeshot('mongodb-json-cell-view');
|
||||
});
|
||||
|
||||
it('Table structure editor', () => {
|
||||
@@ -349,10 +358,10 @@ describe('Data browser data', () => {
|
||||
cy.contains('Customer').rightclick();
|
||||
cy.contains('Open structure').click();
|
||||
cy.contains('varchar(40)');
|
||||
cy.themeshot('structure');
|
||||
cy.themeshot('table-structure-editor');
|
||||
cy.contains('EmployeeId').click();
|
||||
cy.contains('Ref column - Employee');
|
||||
cy.themeshot('fkeditor');
|
||||
cy.themeshot('foreign-key-editor');
|
||||
});
|
||||
|
||||
it('Compare database', () => {
|
||||
@@ -364,10 +373,10 @@ describe('Data browser data', () => {
|
||||
cy.testid('CompareModelTab_gridObjects_Customer_Customer').click();
|
||||
cy.testid('WidgetIconPanel_database').click();
|
||||
cy.testid('CompareModelTab_tabDdl').click();
|
||||
cy.themeshot('dbcompare');
|
||||
cy.themeshot('compare-database-models');
|
||||
cy.contains('Settings').click();
|
||||
cy.testid('CompareModelTab_tabOperations').click();
|
||||
cy.themeshot('comparesettings');
|
||||
cy.themeshot('compare-database-settings');
|
||||
});
|
||||
|
||||
it('Query editor - AI assistant', () => {
|
||||
@@ -382,7 +391,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('Use this', { timeout: 10000 }).click();
|
||||
cy.testid('QueryTab_executeButton').click();
|
||||
cy.contains('Balls to the Wall');
|
||||
cy.themeshot('aiassistant');
|
||||
cy.themeshot('ai-assistant');
|
||||
});
|
||||
|
||||
it('Modify data', () => {
|
||||
@@ -408,7 +417,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('INSERT INTO `Employee`');
|
||||
cy.contains("SET `FirstName`='Jane'");
|
||||
cy.contains('DELETE FROM `Employee`');
|
||||
cy.themeshot('modifydata');
|
||||
cy.themeshot('data-browser-save-changes');
|
||||
|
||||
// cy.testid('ConfirmSqlModal_okButton').click();
|
||||
// cy.contains('Cannot delete or update a parent row')
|
||||
@@ -430,8 +439,11 @@ describe('Data browser data', () => {
|
||||
cy.contains('Album').click();
|
||||
cy.testid('DataFilterControl_input_ArtistId').type('22{enter}');
|
||||
// cy.contains('Presence').rightclick();
|
||||
// cy.contains('Coda').rightclick();
|
||||
// cy.testid('DropDownMenu-container-0').contains('Export').click();
|
||||
cy.contains('Export').click();
|
||||
cy.themeshot('simpleexport');
|
||||
// cy.wait(1000);
|
||||
cy.themeshot('data-browser-export-menu');
|
||||
});
|
||||
|
||||
it('MySQL native backup', () => {
|
||||
@@ -439,7 +451,7 @@ describe('Data browser data', () => {
|
||||
cy.contains('MyChinook').rightclick();
|
||||
cy.contains('Create database backup').click();
|
||||
cy.contains('Customer');
|
||||
cy.themeshot('mysqlbackup');
|
||||
cy.themeshot('mysql-backup-configuration');
|
||||
});
|
||||
|
||||
it('View table YAML model', () => {
|
||||
@@ -449,10 +461,11 @@ describe('Data browser data', () => {
|
||||
cy.testid('ExportDbModelModal_archiveFolder').select('(Create new)');
|
||||
cy.testid('InputTextModal_value').clear().type('test-model');
|
||||
cy.testid('InputTextModal_ok').click();
|
||||
cy.testid('ModalBase_window').themeshot('export-database-model-window', { padding: 50 });
|
||||
cy.testid('ExportDbModelModal_exportButton').click();
|
||||
cy.contains('Album').click();
|
||||
cy.contains('autoIncrement');
|
||||
cy.themeshot('tableyaml');
|
||||
cy.themeshot('database-model-table-yaml');
|
||||
});
|
||||
|
||||
it('Data duplicator', () => {
|
||||
@@ -462,8 +475,8 @@ describe('Data browser data', () => {
|
||||
cy.contains('chinook-archive').rightclick();
|
||||
cy.contains('Data duplicator').click();
|
||||
cy.contains('Dry run').click();
|
||||
cy.testid("DataDuplicatorTab_importIntoDb").click();
|
||||
cy.testid('DataDuplicatorTab_importIntoDb').click();
|
||||
cy.contains('Duplicated Album, inserted 347 rows, mapped 0 rows, missing 0 rows, skipped 0 rows');
|
||||
cy.themeshot('dataduplicator');
|
||||
cy.themeshot('data-duplicator');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,21 +11,19 @@ describe('Team edition tests', () => {
|
||||
|
||||
cy.testid('AdminMenuWidget_itemConnections').click();
|
||||
cy.contains('New connection').click();
|
||||
cy.contains('New connection').click();
|
||||
cy.contains('New connection').click();
|
||||
cy.testid('ConnectionDriverFields_connectionType').select('PostgreSQL');
|
||||
cy.themeshot('connadmin');
|
||||
cy.themeshot('connection-administration');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemRoles').click();
|
||||
cy.contains('Permissions').click();
|
||||
cy.themeshot('roleadmin');
|
||||
cy.contains('logged-user').click();
|
||||
cy.themeshot('role-administration');
|
||||
|
||||
cy.testid('AdminMenuWidget_itemAuthentication').click();
|
||||
cy.contains('Add authentication').click();
|
||||
cy.contains('Use database login').click();
|
||||
cy.contains('Add authentication').click();
|
||||
cy.contains('OAuth 2.0').click();
|
||||
cy.themeshot('authadmin');
|
||||
cy.themeshot('authentication-administration');
|
||||
});
|
||||
|
||||
it('OAuth authentication', () => {
|
||||
@@ -77,6 +75,5 @@ describe('Team edition tests', () => {
|
||||
cy.testid('LoginPage_submitLogin').click();
|
||||
cy.testid('AdminMenuWidget_itemUsers').click();
|
||||
cy.contains('test@example.com');
|
||||
cy.contains('Rows: 1');
|
||||
});
|
||||
});
|
||||
|
||||
0
e2e-tests/screenshots/.gitkeep
Normal file
0
e2e-tests/screenshots/.gitkeep
Normal file
@@ -1 +0,0 @@
|
||||
Folder with screenshots
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "6.3.1",
|
||||
"version": "6.3.2",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
|
||||
@@ -102,12 +102,21 @@ function getPortalCollections() {
|
||||
trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
|
||||
}));
|
||||
|
||||
for(const conn of connections) {
|
||||
for(const prop in process.env) {
|
||||
if (prop.startsWith(`CONNECTION_${conn._id}_`)) {
|
||||
const name = prop.substring(`CONNECTION_${conn._id}_`.length);
|
||||
conn[name] = process.env[prop];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.info({ connections: connections.map(pickSafeConnectionInfo) }, 'Using connections from ENV variables');
|
||||
const noengine = connections.filter(x => !x.engine);
|
||||
if (noengine.length > 0) {
|
||||
logger.warn(
|
||||
{ connections: noengine.map(x => x._id) },
|
||||
'Invalid CONNECTIONS configutation, missing ENGINE for connection ID'
|
||||
'Invalid CONNECTIONS configuration, missing ENGINE for connection ID'
|
||||
);
|
||||
}
|
||||
return connections;
|
||||
|
||||
@@ -195,8 +195,8 @@ module.exports = {
|
||||
},
|
||||
|
||||
exportDiagram_meta: true,
|
||||
async exportDiagram({ filePath, html, css, themeType, themeClassName }) {
|
||||
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName));
|
||||
async exportDiagram({ filePath, html, css, themeType, themeClassName, watermark }) {
|
||||
await fs.writeFile(filePath, getDiagramExport(html, css, themeType, themeClassName, watermark));
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
@@ -38,6 +38,8 @@ function start() {
|
||||
detail: formatErrorDetail(e, connection),
|
||||
});
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,26 @@ function loadEncryptionKey() {
|
||||
return _encryptionKey;
|
||||
}
|
||||
|
||||
async function loadEncryptionKeyFromExternal(storedValue, setStoredValue) {
|
||||
const encryptor = simpleEncryptor.createEncryptor(defaultEncryptionKey);
|
||||
|
||||
if (!storedValue) {
|
||||
const generatedKey = crypto.randomBytes(32);
|
||||
const newKey = generatedKey.toString('hex');
|
||||
const result = {
|
||||
encryptionKey: newKey,
|
||||
};
|
||||
await setStoredValue(encryptor.encrypt(result));
|
||||
|
||||
setEncryptionKey(newKey);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const data = encryptor.decrypt(storedValue);
|
||||
setEncryptionKey(data['encryptionKey']);
|
||||
}
|
||||
|
||||
let _encryptor = null;
|
||||
|
||||
function getEncryptor() {
|
||||
@@ -43,35 +63,32 @@ function getEncryptor() {
|
||||
return _encryptor;
|
||||
}
|
||||
|
||||
function encryptPasswordField(connection, field) {
|
||||
if (
|
||||
connection &&
|
||||
connection[field] &&
|
||||
!connection[field].startsWith('crypt:') &&
|
||||
connection.passwordMode != 'saveRaw'
|
||||
) {
|
||||
function encryptObjectPasswordField(obj, field) {
|
||||
if (obj && obj[field] && !obj[field].startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
[field]: 'crypt:' + getEncryptor().encrypt(connection[field]),
|
||||
...obj,
|
||||
[field]: 'crypt:' + getEncryptor().encrypt(obj[field]),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function decryptPasswordField(connection, field) {
|
||||
if (connection && connection[field] && connection[field].startsWith('crypt:')) {
|
||||
function decryptObjectPasswordField(obj, field) {
|
||||
if (obj && obj[field] && obj[field].startsWith('crypt:')) {
|
||||
return {
|
||||
...connection,
|
||||
[field]: getEncryptor().decrypt(connection[field].substring('crypt:'.length)),
|
||||
...obj,
|
||||
[field]: getEncryptor().decrypt(obj[field].substring('crypt:'.length)),
|
||||
};
|
||||
}
|
||||
return connection;
|
||||
return obj;
|
||||
}
|
||||
|
||||
function encryptConnection(connection) {
|
||||
connection = encryptPasswordField(connection, 'password');
|
||||
connection = encryptPasswordField(connection, 'sshPassword');
|
||||
connection = encryptPasswordField(connection, 'sshKeyfilePassword');
|
||||
if (connection.passwordMode != 'saveRaw') {
|
||||
connection = encryptObjectPasswordField(connection, 'password');
|
||||
connection = encryptObjectPasswordField(connection, 'sshPassword');
|
||||
connection = encryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
@@ -81,12 +98,24 @@ function maskConnection(connection) {
|
||||
}
|
||||
|
||||
function decryptConnection(connection) {
|
||||
connection = decryptPasswordField(connection, 'password');
|
||||
connection = decryptPasswordField(connection, 'sshPassword');
|
||||
connection = decryptPasswordField(connection, 'sshKeyfilePassword');
|
||||
connection = decryptObjectPasswordField(connection, 'password');
|
||||
connection = decryptObjectPasswordField(connection, 'sshPassword');
|
||||
connection = decryptObjectPasswordField(connection, 'sshKeyfilePassword');
|
||||
return connection;
|
||||
}
|
||||
|
||||
function encryptUser(user) {
|
||||
if (user.encryptPassword) {
|
||||
user = encryptObjectPasswordField(user, 'password');
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
function decryptUser(user) {
|
||||
user = decryptObjectPasswordField(user, 'password');
|
||||
return user;
|
||||
}
|
||||
|
||||
function pickSafeConnectionInfo(connection) {
|
||||
if (process.env.LOG_CONNECTION_SENSITIVE_VALUES) {
|
||||
return connection;
|
||||
@@ -99,10 +128,18 @@ function pickSafeConnectionInfo(connection) {
|
||||
});
|
||||
}
|
||||
|
||||
function setEncryptionKey(encryptionKey) {
|
||||
_encryptionKey = encryptionKey;
|
||||
_encryptor = null;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
loadEncryptionKey,
|
||||
encryptConnection,
|
||||
encryptUser,
|
||||
decryptUser,
|
||||
decryptConnection,
|
||||
maskConnection,
|
||||
pickSafeConnectionInfo,
|
||||
loadEncryptionKeyFromExternal,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
const getDiagramExport = (html, css, themeType, themeClassName) => {
|
||||
const getDiagramExport = (html, css, themeType, themeClassName, watermark) => {
|
||||
const watermarkHtml = watermark
|
||||
? `
|
||||
<div style="position: fixed; bottom: 0; right: 0; padding: 5px; font-size: 12px; color: var(--theme-font-2); background-color: var(--theme-bg-2); border-top-left-radius: 5px; border: 1px solid var(--theme-border);">
|
||||
${watermark}
|
||||
</div>
|
||||
`
|
||||
: '';
|
||||
return `<html>
|
||||
<meta charset='utf-8'>
|
||||
|
||||
@@ -13,10 +20,44 @@ const getDiagramExport = (html, css, themeType, themeClassName) => {
|
||||
</style>
|
||||
|
||||
<link rel="stylesheet" href='https://cdn.jsdelivr.net/npm/@mdi/font@6.5.95/css/materialdesignicons.css' />
|
||||
|
||||
<script>
|
||||
let lastX = null;
|
||||
let lastY = null;
|
||||
|
||||
const handleMoveDown = e => {
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
document.addEventListener('mousemove', handleMoveMove, true);
|
||||
document.addEventListener('mouseup', handleMoveEnd, true);
|
||||
};
|
||||
|
||||
const handleMoveMove = e => {
|
||||
e.preventDefault();
|
||||
|
||||
document.body.scrollLeft -= e.clientX - lastX;
|
||||
document.body.scrollTop -= e.clientY - lastY;
|
||||
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
};
|
||||
const handleMoveEnd = e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
lastX = null;
|
||||
lastY = null;
|
||||
document.removeEventListener('mousemove', handleMoveMove, true);
|
||||
document.removeEventListener('mouseup', handleMoveEnd, true);
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleMoveDown);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}'>
|
||||
<body class='${themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light'} ${themeClassName}' style='user-select:none; cursor:pointer'>
|
||||
${html}
|
||||
${watermarkHtml}
|
||||
</body>
|
||||
|
||||
</html>`;
|
||||
|
||||
@@ -29,8 +29,8 @@ export interface GridConfig extends GridConfigColumns {
|
||||
isFormView?: boolean;
|
||||
formViewRecordNumber?: number;
|
||||
formFilterColumns: string[];
|
||||
formColumnFilterText?: string;
|
||||
multiColumnFilter?: string;
|
||||
searchInColumns?: string;
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
|
||||
@@ -196,9 +196,24 @@ export abstract class GridDisplay {
|
||||
}));
|
||||
}
|
||||
|
||||
setSearchInColumns(searchInColumns: string) {
|
||||
this.setConfig(cfg => ({
|
||||
...cfg,
|
||||
searchInColumns,
|
||||
}));
|
||||
}
|
||||
|
||||
get hiddenColumnIndexes() {
|
||||
// console.log('GridDisplay.hiddenColumn', this.config.hiddenColumns);
|
||||
return (this.config.hiddenColumns || []).map(x => _.findIndex(this.allColumns, y => y.uniqueName == x));
|
||||
const res = (this.config.hiddenColumns || []).map(x => _.findIndex(this.allColumns, y => y.uniqueName == x));
|
||||
if (this.config.searchInColumns) {
|
||||
for (let i = 0; i < this.allColumns.length; i++) {
|
||||
if (!filterName(this.config.searchInColumns, this.allColumns[i].columnName)) {
|
||||
res.push(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.sortBy(_.uniq(res));
|
||||
}
|
||||
|
||||
isColumnChecked(column: DisplayColumn) {
|
||||
|
||||
5
packages/tools/jest.config.js
Normal file
5
packages/tools/jest.config.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js'],
|
||||
};
|
||||
@@ -17,8 +17,9 @@
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"prepublishOnly": "yarn build",
|
||||
"test": "jest",
|
||||
"prepublishOnly": "yarn build"
|
||||
"test:ci": "jest --json --outputFile=result.json --testLocationInResults"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
@@ -26,8 +27,8 @@
|
||||
"devDependencies": {
|
||||
"@types/node": "^13.7.0",
|
||||
"dbgate-types": "^6.0.0-alpha.1",
|
||||
"jest": "^24.9.0",
|
||||
"ts-jest": "^25.2.1",
|
||||
"jest": "^28.1.3",
|
||||
"ts-jest": "^28.0.7",
|
||||
"typescript": "^4.4.3"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -354,6 +354,7 @@ export class DatabaseAnalyser {
|
||||
logger.error(extractErrorLogData(err, { template }), 'Error running analyser query');
|
||||
return {
|
||||
rows: [],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
63
packages/tools/src/diagramTools.ts
Normal file
63
packages/tools/src/diagramTools.ts
Normal 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)';
|
||||
20
packages/tools/src/filterName.test.ts
Normal file
20
packages/tools/src/filterName.test.ts
Normal 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 },
|
||||
]);
|
||||
});
|
||||
@@ -6,6 +6,29 @@ import _startCase from 'lodash/startCase';
|
||||
// childName: string;
|
||||
// }
|
||||
|
||||
interface TokenFactor {
|
||||
tokens: string[];
|
||||
}
|
||||
|
||||
interface TokenTree {
|
||||
factors: TokenFactor[];
|
||||
}
|
||||
|
||||
function parseTokenTree(filter: string): TokenTree {
|
||||
const factors = filter
|
||||
.split(',')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0);
|
||||
return {
|
||||
factors: factors.map(x => ({
|
||||
tokens: x
|
||||
.split(' ')
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
function camelMatch(filter: string, text: string): boolean {
|
||||
if (!text) return false;
|
||||
if (!filter) return true;
|
||||
@@ -24,31 +47,27 @@ export function filterName(filter: string, ...names: string[]) {
|
||||
if (!filter) return true;
|
||||
|
||||
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
|
||||
const tokens = filter.split(' ').map(x => x.trim());
|
||||
const tree = parseTokenTree(filter);
|
||||
|
||||
if (tree.factors.length == 0) return true;
|
||||
|
||||
const namesCompacted = _compact(names);
|
||||
|
||||
for (const token of tokens) {
|
||||
const found = namesCompacted.find(name => camelMatch(token, name));
|
||||
if (!found) return false;
|
||||
for (const factor of tree.factors) {
|
||||
let factorOk = true;
|
||||
for (const token of factor.tokens) {
|
||||
const found = namesCompacted.find(name => camelMatch(token, name));
|
||||
if (!found) factorOk = false;
|
||||
}
|
||||
if (factorOk) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
export function filterNameCompoud(
|
||||
filter: string,
|
||||
namesMain: string[],
|
||||
namesChild: string[]
|
||||
): 'main' | 'child' | 'both' | 'none' {
|
||||
if (!filter) return 'both';
|
||||
|
||||
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
|
||||
const tokens = filter.split(' ').map(x => x.trim());
|
||||
|
||||
const namesCompactedMain = _compact(namesMain);
|
||||
const namesCompactedChild = _compact(namesChild);
|
||||
|
||||
function clasifyCompoudCategory(tokens: string[], namesCompactedMain: string[], namesCompactedChild: string[]) {
|
||||
let isMainOnly = true;
|
||||
let isChildOnly = true;
|
||||
|
||||
@@ -67,10 +86,42 @@ export function filterNameCompoud(
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export function filterNameCompoud(
|
||||
filter: string,
|
||||
namesMain: string[],
|
||||
namesChild: string[]
|
||||
): 'main' | 'child' | 'both' | 'none' {
|
||||
if (!filter) return 'both';
|
||||
|
||||
// const camelVariants = [name.replace(/[^A-Z]/g, '')]
|
||||
const tree = parseTokenTree(filter);
|
||||
|
||||
const namesCompactedMain = _compact(namesMain);
|
||||
const namesCompactedChild = _compact(namesChild);
|
||||
|
||||
if (tree.factors.length == 0) return 'both';
|
||||
|
||||
const factorRes = [];
|
||||
|
||||
for (const factor of tree.factors) {
|
||||
const category = clasifyCompoudCategory(factor.tokens, namesCompactedMain, namesCompactedChild);
|
||||
factorRes.push(category);
|
||||
}
|
||||
|
||||
if (factorRes.includes('both')) return 'both';
|
||||
if (factorRes.includes('main') && factorRes.includes('child')) return 'both';
|
||||
if (factorRes.includes('main')) return 'main';
|
||||
if (factorRes.includes('child')) return 'child';
|
||||
return 'none';
|
||||
}
|
||||
|
||||
export function tokenizeBySearchFilter(text: string, filter: string): { text: string; isMatch: boolean }[] {
|
||||
const camelTokens = [];
|
||||
const stdTokens = [];
|
||||
for (const token of filter.split(' ').map(x => x.trim())) {
|
||||
for (const token of filter
|
||||
.split(/[ ,]/)
|
||||
.map(x => x.trim())
|
||||
.filter(x => x.length > 0)) {
|
||||
if (token.replace(/[A-Z]/g, '').length == 0) {
|
||||
camelTokens.push(token);
|
||||
} else {
|
||||
|
||||
@@ -25,4 +25,5 @@ export * from './detectSqlFilterBehaviour';
|
||||
export * from './filterBehaviours';
|
||||
export * from './schemaInfoTools';
|
||||
export * from './dbKeysLoader';
|
||||
export * from './rowProgressReporter';
|
||||
export * from './rowProgressReporter';
|
||||
export * from './diagramTools';
|
||||
|
||||
@@ -9,16 +9,17 @@ import type {
|
||||
import _flatten from 'lodash/flatten';
|
||||
import _uniq from 'lodash/uniq';
|
||||
import _keys from 'lodash/keys';
|
||||
import _compact from 'lodash/compact';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
if (!db.tables) {
|
||||
return db;
|
||||
}
|
||||
|
||||
const allForeignKeys = _flatten(db.tables.map(x => x.foreignKeys || []));
|
||||
const allForeignKeys = _flatten(db.tables.map(x => x?.foreignKeys || []));
|
||||
return {
|
||||
...db,
|
||||
tables: db.tables.map(table => ({
|
||||
tables: _compact(db.tables).map(table => ({
|
||||
...table,
|
||||
dependencies: allForeignKeys.filter(x => x.refSchemaName == table.schemaName && x.refTableName == table.pureName),
|
||||
})),
|
||||
|
||||
1
packages/types/engines.d.ts
vendored
1
packages/types/engines.d.ts
vendored
@@ -306,6 +306,7 @@ export interface EngineDriver<TClient = any> extends FilterBehaviourProvider {
|
||||
command: 'backup' | 'restore'
|
||||
): { message: string; severity: 'info' | 'error' | 'debug' } | null;
|
||||
getNativeOperationFormArgs(operation: 'backup' | 'restore'): any[];
|
||||
getAdvancedConnectionFields(): any[];
|
||||
|
||||
analyserClass?: any;
|
||||
dumperClass?: any;
|
||||
|
||||
@@ -58,6 +58,24 @@ body {
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.scroll {
|
||||
overflow: scroll;
|
||||
}
|
||||
.bg-0 {
|
||||
background-color: var(--theme-bg-0);
|
||||
}
|
||||
.bg-1 {
|
||||
background-color: var(--theme-bg-1);
|
||||
}
|
||||
.bg-2 {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
.bg-3 {
|
||||
background-color: var(--theme-bg-3);
|
||||
}
|
||||
.bg-4 {
|
||||
background-color: var(--theme-bg-4);
|
||||
}
|
||||
|
||||
.col-10 {
|
||||
flex-basis: 83.3333%;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
visibleCommandPalette,
|
||||
visibleTitleBar,
|
||||
visibleToolbar,
|
||||
systemThemeStore,
|
||||
} from './stores';
|
||||
import TabsPanel from './tabpanel/TabsPanel.svelte';
|
||||
import TabRegister from './tabpanel/TabRegister.svelte';
|
||||
@@ -50,7 +51,7 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
||||
class={`${$currentTheme ?? $systemThemeStore} ${currentThemeType} root dbgate-screen`}
|
||||
class:isElectron
|
||||
use:dragDropFileTarget
|
||||
on:contextmenu={e => e.preventDefault()}
|
||||
|
||||
@@ -5,13 +5,18 @@
|
||||
|
||||
export let filter;
|
||||
export let showDisabled = false;
|
||||
export let onClearFilter = null;
|
||||
</script>
|
||||
|
||||
{#if filter || showDisabled}
|
||||
<InlineButton
|
||||
on:click
|
||||
on:click={() => {
|
||||
filter = '';
|
||||
if (onClearFilter) {
|
||||
onClearFilter();
|
||||
} else {
|
||||
filter = '';
|
||||
}
|
||||
}}
|
||||
title="Clear filter"
|
||||
disabled={!filter}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
<div class="content" class:scrollContent>
|
||||
<div class="content" class:scrollContent class:isComponentActive>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.content.isComponentActive {
|
||||
max-height: calc(100% - 30px);
|
||||
}
|
||||
|
||||
.toolstrip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -14,8 +14,9 @@
|
||||
function handleClick(e) {
|
||||
const rect = e.detail.target.getBoundingClientRect();
|
||||
const left = rect.left;
|
||||
const top = rect.bottom;
|
||||
currentDropDownMenu.set({ left, top, items: menu });
|
||||
const top = rect.top;
|
||||
// const top = rect.bottom;
|
||||
currentDropDownMenu.set({ left, bottom: window.innerHeight - top, items: menu });
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -619,6 +619,24 @@ registerCommand({
|
||||
onClick: doLogout,
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.loggedUserCommands',
|
||||
category: 'App',
|
||||
name: 'Logged user',
|
||||
getSubCommands: () => {
|
||||
const config = getCurrentConfig();
|
||||
if (!config) return [];
|
||||
return [
|
||||
{
|
||||
text: 'Logout',
|
||||
onClick: () => {
|
||||
doLogout();
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'app.disconnect',
|
||||
category: 'App',
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
export let isSortDefined = false;
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
export let seachInColumns = '';
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
@@ -86,7 +87,7 @@
|
||||
{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}
|
||||
</span>
|
||||
{/if}
|
||||
<ColumnLabel {...column} />
|
||||
<ColumnLabel {...column} filter={seachInColumns} />
|
||||
|
||||
{#if _.isString(column.displayedDataType || column.dataType) && !order}
|
||||
<span class="data-type" title={column.dataType}>
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
export let changeSetState: { value: ChangeSet } = null;
|
||||
export let dispatchChangeSet = null;
|
||||
|
||||
let filter;
|
||||
let domFocusField;
|
||||
|
||||
let selectedColumns = [];
|
||||
@@ -36,7 +35,9 @@
|
||||
let dragStartColumnIndex = null;
|
||||
let shiftOriginColumnIndex = null;
|
||||
|
||||
$: items = display?.getColumns(filter)?.filter(column => filterName(filter, column.columnName)) || [];
|
||||
$: currentFilter = display?.config?.searchInColumns;
|
||||
|
||||
$: items = display?.getColumns(currentFilter)?.filter(column => filterName(currentFilter, column.columnName)) || [];
|
||||
|
||||
function selectColumnIndexCore(index, e) {
|
||||
const uniqueName = items[index].uniqueName;
|
||||
@@ -173,8 +174,13 @@
|
||||
</div>
|
||||
{/if}
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search columns" bind:value={filter} />
|
||||
<CloseSearchButton bind:filter />
|
||||
<SearchInput
|
||||
placeholder="Search columns"
|
||||
value={currentFilter}
|
||||
onChange={value => display.setSearchInColumns(value)}
|
||||
data-testid="ColumnManager_searchColumns"
|
||||
/>
|
||||
<CloseSearchButton filter={currentFilter} onClearFilter={() => display.setSearchInColumns('')} />
|
||||
{#if isDynamicStructure && !isJsonView}
|
||||
<InlineButton
|
||||
on:click={() => {
|
||||
@@ -235,7 +241,7 @@
|
||||
{columnIndex}
|
||||
{allowChangeChangeSetStructure}
|
||||
isSelected={selectedColumns.includes(column.uniqueName) || currentColumnUniqueName == column.uniqueName}
|
||||
{filter}
|
||||
filter={currentFilter}
|
||||
on:click={() => {
|
||||
if (domFocusField) domFocusField.focus();
|
||||
selectedColumns = [column.uniqueName];
|
||||
|
||||
@@ -1933,6 +1933,7 @@
|
||||
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
{allowDefineVirtualReferences}
|
||||
seachInColumns={display.config?.searchInColumns}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
@@ -195,12 +195,12 @@
|
||||
{#if designer?.style?.showNullability || designer?.style?.showDataType}
|
||||
<div class="space" />
|
||||
{#if designer?.style?.showDataType && column?.dataType}
|
||||
<div class="ml-2">
|
||||
<div class="ml-2 data-type">
|
||||
{(column?.displayedDataType || column?.dataType).toLowerCase()}
|
||||
</div>
|
||||
{/if}
|
||||
{#if designer?.style?.showNullability}
|
||||
<div class="ml-2">
|
||||
<div class="ml-2 nullability">
|
||||
{column?.notNull ? 'NOT NULL' : 'NULL'}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -238,4 +238,12 @@
|
||||
background: var(--theme-bg-2);
|
||||
color: var(--theme-font-hover);
|
||||
}
|
||||
|
||||
.nullability {
|
||||
color: var(--theme-font-4);
|
||||
}
|
||||
|
||||
.data-type {
|
||||
color: var(--theme-font-4);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -47,10 +47,13 @@
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
|
||||
import { currentThemeDefinition } from '../stores';
|
||||
import { extendDatabaseInfoFromApps } from 'dbgate-tools';
|
||||
import { chooseTopTables, DIAGRAM_DEFAULT_WATERMARK, DIAGRAM_ZOOMS, extendDatabaseInfoFromApps } from 'dbgate-tools';
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
|
||||
import DragColumnMemory from './DragColumnMemory.svelte';
|
||||
import createRef from '../utility/createRef';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import dragScroll from '../utility/dragScroll';
|
||||
|
||||
export let value;
|
||||
export let onChange;
|
||||
@@ -59,15 +62,18 @@
|
||||
export let menu;
|
||||
export let settings;
|
||||
export let referenceComponent;
|
||||
export let onReportCounts = undefined;
|
||||
|
||||
export const activator = createActivator('Designer', true);
|
||||
|
||||
let domCanvas;
|
||||
let domWrapper;
|
||||
let canvasWidth = 3000;
|
||||
let canvasHeight = 3000;
|
||||
let dragStartPoint = null;
|
||||
let dragCurrentPoint = null;
|
||||
let columnFilter;
|
||||
export let columnFilter;
|
||||
export let showColumnFilter = true;
|
||||
|
||||
const sourceDragColumn$ = writable(null);
|
||||
const targetDragColumn$ = writable(null);
|
||||
@@ -75,14 +81,24 @@
|
||||
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
||||
$: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
|
||||
|
||||
$: tables = value?.tables as any[];
|
||||
$: references = value?.references as any[];
|
||||
$: tables =
|
||||
(value?.tables
|
||||
? chooseTopTables(
|
||||
value?.tables,
|
||||
value?.style?.topTables,
|
||||
value?.style?.tableFilter,
|
||||
value?.style?.omitTablesFilter
|
||||
)
|
||||
: value?.tables) || ([] as any[]);
|
||||
$: references = (value?.references || [])?.filter(
|
||||
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
|
||||
) as any[];
|
||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||
$: apps = useUsedApps();
|
||||
|
||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||
|
||||
const tableRefs = {};
|
||||
let tableRefs = {};
|
||||
const referenceRefs = {};
|
||||
let domTables;
|
||||
$: {
|
||||
@@ -132,12 +148,14 @@
|
||||
onChange(current => {
|
||||
let newTables = current.tables || [];
|
||||
for (const table of current.tables || []) {
|
||||
const dbTable = (db.tables || []).find(x => x.pureName == table.pureName && x.schemaName == table.schemaName);
|
||||
const dbTable = (db.tables || []).find(
|
||||
x => x?.pureName == table?.pureName && x?.schemaName == table?.schemaName
|
||||
);
|
||||
if (
|
||||
stableStringify(_.pick(dbTable, ['columns', 'primaryKey', 'foreignKeys'])) !=
|
||||
stableStringify(_.pick(table, ['columns', 'primaryKey', 'foreignKeys']))
|
||||
) {
|
||||
newTables = newTables.map(x =>
|
||||
newTables = _.compact(newTables).map(x =>
|
||||
x == table
|
||||
? {
|
||||
...table,
|
||||
@@ -152,7 +170,7 @@
|
||||
if (settings?.useDatabaseReferences) {
|
||||
references = [];
|
||||
for (const table of newTables) {
|
||||
for (const fk of table.foreignKeys) {
|
||||
for (const fk of table.foreignKeys || []) {
|
||||
const dst = newTables.find(x => x.pureName == fk.refTableName && x.schemaName == fk.refSchemaName);
|
||||
if (!dst) continue;
|
||||
references.push({
|
||||
@@ -620,11 +638,19 @@
|
||||
...current,
|
||||
tables: (current.tables || []).map(x => {
|
||||
const domTable = domTables[x.designerId] as any;
|
||||
const rect = domTable.getRect();
|
||||
return {
|
||||
...x,
|
||||
isSelectedTable: rectanglesHaveIntersection(rect, bounds),
|
||||
};
|
||||
if (domTable) {
|
||||
const rect = domTable.getRect();
|
||||
const rectZoomed = {
|
||||
left: rect.left / zoomKoef,
|
||||
right: rect.right / zoomKoef,
|
||||
top: rect.top / zoomKoef,
|
||||
bottom: rect.bottom / zoomKoef,
|
||||
};
|
||||
return {
|
||||
...x,
|
||||
isSelectedTable: rectanglesHaveIntersection(rectZoomed, bounds),
|
||||
};
|
||||
}
|
||||
}),
|
||||
}),
|
||||
true
|
||||
@@ -637,7 +663,7 @@
|
||||
|
||||
function recomputeReferencePositions() {
|
||||
for (const ref of Object.values(referenceRefs) as any[]) {
|
||||
if (ref) ref.recomputePosition();
|
||||
if (ref) ref.recomputePosition(zoomKoef);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,21 +688,26 @@
|
||||
|
||||
export function arrange(skipUndoChain = false, arrangeAll = true, circleMiddle = { x: 0, y: 0 }) {
|
||||
const graph = new GraphDefinition();
|
||||
for (const table of value?.tables || []) {
|
||||
for (const table of tables || []) {
|
||||
const domTable = domTables[table.designerId] as any;
|
||||
if (!domTable) continue;
|
||||
const rect = domTable.getRect();
|
||||
graph.addNode(
|
||||
table.designerId,
|
||||
rect.right - rect.left,
|
||||
rect.bottom - rect.top,
|
||||
arrangeAll || table.needsArrange ? null : { x: (rect.left + rect.right) / 2, y: (rect.top + rect.bottom) / 2 }
|
||||
(rect.right - rect.left) / zoomKoef,
|
||||
(rect.bottom - rect.top) / zoomKoef,
|
||||
arrangeAll || table.needsArrange
|
||||
? null
|
||||
: {
|
||||
x: (rect.left + rect.right) / 2 / zoomKoef,
|
||||
y: (rect.top + rect.bottom) / 2 / zoomKoef,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
for (const reference of settings?.sortAutoLayoutReferences
|
||||
? settings?.sortAutoLayoutReferences(value?.references)
|
||||
: value?.references) {
|
||||
? settings?.sortAutoLayoutReferences(references)
|
||||
: references) {
|
||||
graph.addEdge(reference.sourceId, reference.targetId);
|
||||
}
|
||||
|
||||
@@ -710,7 +741,7 @@
|
||||
current => {
|
||||
return {
|
||||
...current,
|
||||
tables: (current?.tables || []).map(table => {
|
||||
tables: _.compact(current?.tables || []).map(table => {
|
||||
const node = layout.nodes[table.designerId];
|
||||
// console.log('POSITION', position);
|
||||
return node
|
||||
@@ -732,6 +763,16 @@
|
||||
);
|
||||
}
|
||||
|
||||
function getWatermarkHtml() {
|
||||
const replaceLinks = text => text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" style="color: var(--theme-font-link)" target="_blank">$1</a>');
|
||||
|
||||
if (value?.style?.omitExportWatermark) return null;
|
||||
if (value?.style?.exportWatermark) {
|
||||
return replaceLinks(value?.style?.exportWatermark);
|
||||
}
|
||||
return replaceLinks(DIAGRAM_DEFAULT_WATERMARK);
|
||||
}
|
||||
|
||||
export async function exportDiagram() {
|
||||
const cssLinks = ['global.css', 'build/bundle.css'];
|
||||
let css = '';
|
||||
@@ -745,6 +786,7 @@
|
||||
if (css) css += '\n';
|
||||
css += $currentThemeDefinition?.themeCss;
|
||||
}
|
||||
css += ' body { overflow: scroll; }';
|
||||
saveFileToDisk(async filePath => {
|
||||
await apiCall('files/export-diagram', {
|
||||
filePath,
|
||||
@@ -752,6 +794,7 @@
|
||||
css,
|
||||
themeType: $currentThemeDefinition?.themeType,
|
||||
themeClassName: $currentThemeDefinition?.themeClassName,
|
||||
watermark: getWatermarkHtml(),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -773,7 +816,7 @@
|
||||
menu,
|
||||
settings?.customizeStyle && [
|
||||
{ divider: true },
|
||||
{
|
||||
isProApp() && {
|
||||
text: 'Column properties',
|
||||
submenu: [
|
||||
{
|
||||
@@ -786,12 +829,12 @@
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
isProApp() && {
|
||||
text: `Columns - ${_.startCase(value?.style?.filterColumns || 'all')}`,
|
||||
submenu: [
|
||||
{
|
||||
text: 'All',
|
||||
onClick: changeStyleFunc('filterColumns', null),
|
||||
onClick: changeStyleFunc('filterColumns', ''),
|
||||
},
|
||||
{
|
||||
text: 'Primary Key',
|
||||
@@ -813,56 +856,10 @@
|
||||
},
|
||||
{
|
||||
text: `Zoom - ${(value?.style?.zoomKoef || 1) * 100}%`,
|
||||
submenu: [
|
||||
{
|
||||
text: `10 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.1),
|
||||
},
|
||||
{
|
||||
text: `15 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.15),
|
||||
},
|
||||
{
|
||||
text: `20 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.2),
|
||||
},
|
||||
{
|
||||
text: `40 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.4),
|
||||
},
|
||||
{
|
||||
text: `60 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.6),
|
||||
},
|
||||
{
|
||||
text: `80 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 0.8),
|
||||
},
|
||||
{
|
||||
text: `100 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 1),
|
||||
},
|
||||
{
|
||||
text: `120 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 1.2),
|
||||
},
|
||||
{
|
||||
text: `140 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 1.4),
|
||||
},
|
||||
{
|
||||
text: `160 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 1.6),
|
||||
},
|
||||
{
|
||||
text: `180 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 1.8),
|
||||
},
|
||||
{
|
||||
text: `200 %`,
|
||||
onClick: changeStyleFunc('zoomKoef', 2),
|
||||
},
|
||||
],
|
||||
submenu: DIAGRAM_ZOOMS.map(koef => ({
|
||||
text: `${koef * 100} %`,
|
||||
onClick: changeStyleFunc('zoomKoef', koef.toString()),
|
||||
})),
|
||||
},
|
||||
],
|
||||
];
|
||||
@@ -875,9 +872,105 @@
|
||||
recomputeDomTables();
|
||||
});
|
||||
}
|
||||
|
||||
const oldTopTablesRef = createRef(value?.style?.topTables);
|
||||
$: {
|
||||
if (value?.style?.topTables > 0 && oldTopTablesRef.get() != value?.style?.topTables) {
|
||||
oldTopTablesRef.set(value?.style?.topTables);
|
||||
tick().then(() => {
|
||||
arrange();
|
||||
tick().then(() => {
|
||||
recomputeReferencePositions();
|
||||
recomputeDomTables();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(event) {
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault();
|
||||
const zoomIndex = DIAGRAM_ZOOMS.findIndex(x => x == value?.style?.zoomKoef);
|
||||
if (zoomIndex < 0) DIAGRAM_ZOOMS.findIndex(x => x == 1);
|
||||
|
||||
let newZoomIndex = zoomIndex;
|
||||
if (event.deltaY < 0) {
|
||||
newZoomIndex += 1;
|
||||
}
|
||||
if (event.deltaY > 0) {
|
||||
newZoomIndex -= 1;
|
||||
}
|
||||
if (newZoomIndex < 0) {
|
||||
newZoomIndex = 0;
|
||||
}
|
||||
if (newZoomIndex >= DIAGRAM_ZOOMS.length) {
|
||||
newZoomIndex = DIAGRAM_ZOOMS.length - 1;
|
||||
}
|
||||
const newZoomKoef = DIAGRAM_ZOOMS[newZoomIndex];
|
||||
|
||||
callChange(
|
||||
current => ({
|
||||
...current,
|
||||
style: {
|
||||
...current?.style,
|
||||
zoomKoef: newZoomKoef.toString(),
|
||||
},
|
||||
}),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDragScroll(x, y) {
|
||||
domWrapper.scrollLeft -= x;
|
||||
domWrapper.scrollTop -= y;
|
||||
}
|
||||
|
||||
const oldZoomKoefRef = createRef(value?.style?.zoomKoef || 1);
|
||||
$: {
|
||||
if (
|
||||
domWrapper &&
|
||||
value?.style?.zoomKoef != oldZoomKoefRef.get() &&
|
||||
value?.style?.zoomKoef > 0 &&
|
||||
oldZoomKoefRef.get() > 0
|
||||
) {
|
||||
domWrapper.scrollLeft = Math.round((domWrapper.scrollLeft / oldZoomKoefRef.get()) * value?.style?.zoomKoef);
|
||||
domWrapper.scrollTop = Math.round((domWrapper.scrollTop / oldZoomKoefRef.get()) * value?.style?.zoomKoef);
|
||||
}
|
||||
oldZoomKoefRef.set(value?.style?.zoomKoef);
|
||||
}
|
||||
|
||||
// $: console.log('DESIGNER VALUE', value);
|
||||
|
||||
// $: console.log('TABLES ARRAY', tables);
|
||||
|
||||
// $: {
|
||||
// if (value?.tables?.find(x => !x)) {
|
||||
// console.log('**** INCORRECT DESIGNER VALUE**** ', value);
|
||||
// }
|
||||
// }
|
||||
// $: {
|
||||
// if (value?.tables?.length < 100) {
|
||||
// console.log('**** SMALL TABLES**** ', value);
|
||||
// }
|
||||
// }
|
||||
|
||||
$: if (onReportCounts) {
|
||||
// console.log('REPORTING COUNTS');
|
||||
onReportCounts({
|
||||
all: _.compact(value?.tables || []).length,
|
||||
filtered: _.compact(tables || []).length,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="wrapper noselect" use:contextMenu={createMenu}>
|
||||
<div
|
||||
class="wrapper noselect"
|
||||
use:contextMenu={createMenu}
|
||||
on:wheel={handleWheel}
|
||||
bind:this={domWrapper}
|
||||
use:dragScroll={handleDragScroll}
|
||||
>
|
||||
{#if !(tables?.length > 0)}
|
||||
<div class="empty">Drag & drop tables or views from left panel here</div>
|
||||
{/if}
|
||||
@@ -887,8 +980,8 @@
|
||||
bind:this={domCanvas}
|
||||
on:dragover={e => e.preventDefault()}
|
||||
on:drop={handleDrop}
|
||||
style={`width:${canvasWidth}px;height:${canvasHeight}px;
|
||||
${settings?.customizeStyle && value?.style?.zoomKoef ? `zoom:${value?.style?.zoomKoef};` : ''}
|
||||
style={`width:${canvasWidth / zoomKoef}px;height:${canvasHeight / zoomKoef}px;
|
||||
${settings?.customizeStyle && value?.style?.zoomKoef ? `transform:scale(${value?.style?.zoomKoef});transform-origin: top left;` : ''}
|
||||
`}
|
||||
on:mousedown={e => {
|
||||
if (e.button == 0 && settings?.canSelectTables) {
|
||||
@@ -913,6 +1006,7 @@
|
||||
onRemoveReference={removeReference}
|
||||
designer={value}
|
||||
{settings}
|
||||
{zoomKoef}
|
||||
/>
|
||||
{/each}
|
||||
<!--
|
||||
@@ -971,7 +1065,7 @@
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
{#if tables?.length > 0}
|
||||
{#if showColumnFilter && tables?.length > 0}
|
||||
<div class="panel">
|
||||
<DragColumnMemory {settings} {sourceDragColumn$} {targetDragColumn$} />
|
||||
<div class="searchbox">
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<script lang="ts">
|
||||
import Designer from './Designer.svelte';
|
||||
import DiagramDesignerReference from './DiagramDesignerReference.svelte';
|
||||
// import QueryDesignerReference from './QueryDesignerReference.svelte';
|
||||
|
||||
export let columnFilter;
|
||||
</script>
|
||||
|
||||
<Designer
|
||||
@@ -18,4 +21,6 @@
|
||||
arrangeAlg: 'springy',
|
||||
}}
|
||||
referenceComponent={DiagramDesignerReference}
|
||||
showColumnFilter={false}
|
||||
{columnFilter}
|
||||
/>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
export let domTables;
|
||||
export let settings;
|
||||
|
||||
export let zoomKoef;
|
||||
|
||||
let src = null;
|
||||
let dst = null;
|
||||
|
||||
@@ -19,7 +21,7 @@
|
||||
const arhi = 12;
|
||||
const arpad = 3;
|
||||
|
||||
export function recomputePosition() {
|
||||
export function recomputePosition(zoomKoef) {
|
||||
const { designerId, sourceId, targetId, columns, joinType } = reference;
|
||||
|
||||
/** @type {DomTableRef} */
|
||||
@@ -31,6 +33,17 @@
|
||||
const targetRect = targetTable.getRect();
|
||||
if (!sourceRect || !targetRect) return null;
|
||||
|
||||
if (zoomKoef > 0) {
|
||||
sourceRect.left /= zoomKoef;
|
||||
sourceRect.right /= zoomKoef;
|
||||
sourceRect.top /= zoomKoef;
|
||||
sourceRect.bottom /= zoomKoef;
|
||||
targetRect.left /= zoomKoef;
|
||||
targetRect.right /= zoomKoef;
|
||||
targetRect.top /= zoomKoef;
|
||||
targetRect.bottom /= zoomKoef;
|
||||
}
|
||||
|
||||
src = {
|
||||
x: (sourceRect.left + sourceRect.right) / 2,
|
||||
y: (sourceRect.top + sourceRect.bottom) / 2,
|
||||
@@ -47,7 +60,7 @@
|
||||
|
||||
$: {
|
||||
domTables;
|
||||
recomputePosition();
|
||||
recomputePosition(zoomKoef);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
1
packages/web/src/designer/DiagramSettings.svelte
Normal file
1
packages/web/src/designer/DiagramSettings.svelte
Normal file
@@ -0,0 +1 @@
|
||||
This is part of DbGate Premium
|
||||
@@ -8,12 +8,17 @@
|
||||
$: searchValue = value || '';
|
||||
export let isDebounced = false;
|
||||
export let onFocusFilteredList = null;
|
||||
export let onChange = null;
|
||||
|
||||
let domInput;
|
||||
|
||||
function handleKeyDown(e) {
|
||||
if (e.keyCode == keycodes.escape) {
|
||||
value = '';
|
||||
if (onChange) {
|
||||
onChange('');
|
||||
} else {
|
||||
value = '';
|
||||
}
|
||||
}
|
||||
if (e.keyCode == keycodes.downArrow || e.keyCode == keycodes.pageDown || e.keyCode == keycodes.enter) {
|
||||
onFocusFilteredList?.();
|
||||
@@ -27,7 +32,11 @@
|
||||
domInput.focus();
|
||||
if (text) {
|
||||
domInput.value = text;
|
||||
value = text;
|
||||
if (onChange) {
|
||||
onChange(text);
|
||||
} else {
|
||||
value = text;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -37,8 +46,12 @@
|
||||
{placeholder}
|
||||
value={searchValue}
|
||||
on:input={e => {
|
||||
if (isDebounced) debouncedSet(domInput.value);
|
||||
else value = domInput.value;
|
||||
if (onChange) {
|
||||
onChange(domInput.value);
|
||||
} else {
|
||||
if (isDebounced) debouncedSet(domInput.value);
|
||||
else value = domInput.value;
|
||||
}
|
||||
}}
|
||||
on:keydown={handleKeyDown}
|
||||
bind:this={domInput}
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
export let isInline = false;
|
||||
export let containerMaxWidth = undefined;
|
||||
export let flex1 = true;
|
||||
export let contentTestId = undefined;
|
||||
export let inlineTabs = false;
|
||||
|
||||
export function setValue(index) {
|
||||
value = index;
|
||||
@@ -26,7 +28,7 @@
|
||||
</script>
|
||||
|
||||
<div class="main" class:flex1>
|
||||
<div class="tabs">
|
||||
<div class="tabs" class:inlineTabs>
|
||||
{#each _.compact(tabs) as tab, index}
|
||||
<div class="tab-item" class:selected={value == index} on:click={() => (value = index)} data-testid={tab.testid}>
|
||||
<span class="ml-2">
|
||||
@@ -39,7 +41,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
<div class="content-container" data-testid={contentTestId}>
|
||||
{#each _.compact(tabs) as tab, index}
|
||||
<div class="container" class:isInline class:tabVisible={index == value} style:max-width={containerMaxWidth}>
|
||||
<svelte:component this={tab.component} {...tab.props} tabControlHiddenTab={index != value} />
|
||||
@@ -77,17 +79,27 @@
|
||||
height: var(--dim-tabs-height);
|
||||
min-height: var(--dim-tabs-height);
|
||||
right: 0;
|
||||
background-color: var(--theme-bg-2);
|
||||
overflow-x: auto;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tabs:not(.inlineTabs) {
|
||||
background-color: var(--theme-bg-2);
|
||||
}
|
||||
|
||||
.tabs.inlineTabs {
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.tabs.inlineTabs .tab-item.selected {
|
||||
border-bottom: 2px solid var(--theme-font-link);
|
||||
}
|
||||
.tabs::-webkit-scrollbar {
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
border-right: 1px solid var(--theme-border);
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
display: flex;
|
||||
@@ -95,6 +107,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tabs:not(.inlineTabs) .tab-item {
|
||||
border-right: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
/* .tab-item:hover {
|
||||
color: ${props => props.theme.tabs_font_hover};
|
||||
} */
|
||||
@@ -123,4 +139,5 @@
|
||||
.container.isInline:not(.tabVisible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -31,6 +31,11 @@
|
||||
export let noCellPadding = false;
|
||||
|
||||
export let domTable = undefined;
|
||||
export let stickyHeader = false;
|
||||
|
||||
export let checkedKeys = null;
|
||||
export let onSetCheckedKeys = null;
|
||||
export let extractCheckedKey = x => x.id;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -63,11 +68,15 @@
|
||||
on:keydown
|
||||
tabindex={selectable ? -1 : undefined}
|
||||
on:keydown={handleKeyDown}
|
||||
class:stickyHeader
|
||||
>
|
||||
<thead>
|
||||
<thead class:stickyHeader>
|
||||
<tr>
|
||||
{#if checkedKeys}
|
||||
<th></th>
|
||||
{/if}
|
||||
{#each columnList as col}
|
||||
<td
|
||||
<th
|
||||
class:clickable={col.sortable}
|
||||
on:click={() => {
|
||||
if (col.sortable) {
|
||||
@@ -89,7 +98,7 @@
|
||||
{#if sortedByField == col.fieldName}
|
||||
<FontIcon icon={sortOrderIsDesc ? 'img sort-desc' : 'img sort-asc'} padLeft />
|
||||
{/if}
|
||||
</td>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -108,6 +117,18 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
{#if checkedKeys}
|
||||
<td>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checkedKeys.includes(extractCheckedKey(row))}
|
||||
on:change={e => {
|
||||
if (e.target['checked']) onSetCheckedKeys(_.uniq([...checkedKeys, extractCheckedKey(row)]));
|
||||
else onSetCheckedKeys(checkedKeys.filter(x => x != extractCheckedKey(row)));
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
{/if}
|
||||
{#each columnList as col}
|
||||
{@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }}
|
||||
<td class:isHighlighted={col.isHighlighted && col.isHighlighted(row)} class:noCellPadding>
|
||||
@@ -164,7 +185,7 @@
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
thead td {
|
||||
thead th {
|
||||
border: 1px solid var(--theme-border);
|
||||
background-color: var(--theme-bg-1);
|
||||
padding: 5px;
|
||||
@@ -184,4 +205,31 @@
|
||||
td.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
thead.stickyHeader {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table.stickyHeader th {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
thead.stickyHeader :global(tr:first-child) :global(th) {
|
||||
border-top: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table.stickyHeader td {
|
||||
border: 0px;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
border-right: 1px solid var(--theme-border);
|
||||
}
|
||||
|
||||
table.stickyHeader {
|
||||
border-spacing: 0;
|
||||
border-collapse: separate;
|
||||
border-left: 1px solid var(--theme-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
79
packages/web/src/forms/ExtendedCheckBoxField.svelte
Normal file
79
packages/web/src/forms/ExtendedCheckBoxField.svelte
Normal 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>
|
||||
@@ -459,7 +459,7 @@
|
||||
setConfig(x => ({
|
||||
...x,
|
||||
// @ts-ignore
|
||||
formColumnFilterText: (x.formColumnFilterText || '') + event.key,
|
||||
searchInColumns: (x.searchInColumns || '') + event.key,
|
||||
}));
|
||||
} else {
|
||||
// @ts-ignore
|
||||
@@ -473,7 +473,7 @@
|
||||
if (event.keyCode == keycodes.escape) {
|
||||
setConfig(x => ({
|
||||
...x,
|
||||
formColumnFilterText: '',
|
||||
searchInColumns: '',
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -541,7 +541,7 @@
|
||||
columnIndex = incrementFunc(columnIndex);
|
||||
while (
|
||||
isInRange(columnIndex) &&
|
||||
!filterName(display.config.formColumnFilterText, display.formColumns[columnIndex].columnName)
|
||||
!filterName(display.config.searchInColumns, display.formColumns[columnIndex].columnName)
|
||||
) {
|
||||
columnIndex = incrementFunc(columnIndex);
|
||||
}
|
||||
@@ -549,7 +549,7 @@
|
||||
columnIndex = firstInRange;
|
||||
while (
|
||||
isInRange(columnIndex) &&
|
||||
!filterName(display.config.formColumnFilterText, display.formColumns[columnIndex].columnName)
|
||||
!filterName(display.config.searchInColumns, display.formColumns[columnIndex].columnName)
|
||||
) {
|
||||
columnIndex = incrementFunc(columnIndex);
|
||||
}
|
||||
@@ -572,7 +572,7 @@
|
||||
case keycodes.rightArrow:
|
||||
return moveCurrentCell(currentCell[0], currentCell[1] + 1);
|
||||
case keycodes.upArrow:
|
||||
if (currentCell[1] % 2 == 0 && display.config.formColumnFilterText) {
|
||||
if (currentCell[1] % 2 == 0 && display.config.searchInColumns) {
|
||||
return findFilteredColumn(
|
||||
x => x - 1,
|
||||
x => x >= 0,
|
||||
@@ -583,7 +583,7 @@
|
||||
|
||||
return moveCurrentCell(currentCell[0] - 1, currentCell[1]);
|
||||
case keycodes.downArrow:
|
||||
if (currentCell[1] % 2 == 0 && display.config.formColumnFilterText) {
|
||||
if (currentCell[1] % 2 == 0 && display.config.searchInColumns) {
|
||||
return findFilteredColumn(
|
||||
x => x + 1,
|
||||
x => x < display.formColumns.length,
|
||||
@@ -631,8 +631,8 @@
|
||||
data-row={rowIndex}
|
||||
data-col={chunkIndex * 2}
|
||||
style={rowHeight > 1 ? `height: ${rowHeight}px` : undefined}
|
||||
class:columnFiltered={display.config.formColumnFilterText &&
|
||||
filterName(display.config.formColumnFilterText, col.columnName)}
|
||||
class:columnFiltered={display.config.searchInColumns &&
|
||||
filterName(display.config.searchInColumns, col.columnName)}
|
||||
class:isSelected={currentCell[0] == rowIndex && currentCell[1] == chunkIndex * 2}
|
||||
bind:this={domCells[`${rowIndex},${chunkIndex * 2}`]}
|
||||
>
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
value={display?.config?.formColumnFilterText || ''}
|
||||
value={display?.config?.searchInColumns || ''}
|
||||
on:keydown={e => {
|
||||
if (e.keyCode == keycodes.escape) {
|
||||
setConfig(x => ({
|
||||
...x,
|
||||
formColumnFilterText: '',
|
||||
searchInColumns: '',
|
||||
}));
|
||||
}
|
||||
}}
|
||||
@@ -52,7 +52,7 @@
|
||||
setConfig(x => ({
|
||||
...x,
|
||||
// @ts-ignore
|
||||
formColumnFilterText: e.target.value,
|
||||
searchInColumns: e.target.value,
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -217,6 +217,8 @@
|
||||
'icon autocommit-on': 'mdi mdi-check-circle',
|
||||
'icon autocommit-off': 'mdi mdi-check-circle-outline',
|
||||
|
||||
'icon premium': 'mdi mdi-star',
|
||||
|
||||
'img ok': 'mdi mdi-check-circle color-icon-green',
|
||||
'img ok-inv': 'mdi mdi-check-circle color-icon-inv-green',
|
||||
'img alert': 'mdi mdi-alert-circle color-icon-blue',
|
||||
|
||||
@@ -300,7 +300,10 @@
|
||||
initialValue: $values[`columns_${row}`],
|
||||
sourceTableInfo: $sourceDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == row?.toLowerCase()),
|
||||
targetTableInfo: $targetDbinfo?.tables?.find(x => x.pureName?.toLowerCase() == targetNameLower),
|
||||
onConfirm: value => setFieldValue(`columns_${row}`, value),
|
||||
onConfirm: value => {
|
||||
setFieldValue(`columns_${row}`, value);
|
||||
targetEditKey += 1;
|
||||
},
|
||||
});
|
||||
}}
|
||||
>{columnCount > 0 ? `(${columnCount} columns)` : '(copy from source)'}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import _ from 'lodash';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import { openJsonLinesData } from '../utility/openJsonLinesData';
|
||||
import { useSettings } from '../utility/metadataLoaders';
|
||||
|
||||
setContext('json-tree-context-key', {});
|
||||
|
||||
@@ -23,6 +24,9 @@
|
||||
export let isInserted = false;
|
||||
export let isModified = false;
|
||||
|
||||
const settings = useSettings();
|
||||
$: wrap = $settings?.['behaviour.jsonPreviewWrap'];
|
||||
|
||||
setContext('json-tree-default-expanded', expandAll);
|
||||
if (slicedKeyCount) setContext('json-tree-sliced-key-count', slicedKeyCount);
|
||||
|
||||
@@ -66,6 +70,7 @@
|
||||
class:isDeleted
|
||||
class:isInserted
|
||||
class:isModified
|
||||
class:wrap
|
||||
>
|
||||
<JSONNode
|
||||
{key}
|
||||
@@ -115,6 +120,9 @@
|
||||
list-style: none;
|
||||
white-space: nowrap;
|
||||
}
|
||||
ul.wrap :global(li) {
|
||||
white-space: normal;
|
||||
}
|
||||
ul,
|
||||
ul :global(ul) {
|
||||
padding: 0;
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
Build date: <span>{moment(buildTime).format('YYYY-MM-DD')}</span>
|
||||
</div>
|
||||
<div class="m-1">
|
||||
Web: <Link href="https://dbgate.org">dbgate.org</Link>
|
||||
Web: <Link href="https://dbgate.io">dbgate.io</Link>
|
||||
</div>
|
||||
<div class="m-1">
|
||||
Source codes: <Link href="https://github.com/dbgate/dbgate/">github</Link>
|
||||
@@ -32,9 +32,6 @@
|
||||
<div class="m-1">
|
||||
Docker container: <Link href="https://hub.docker.com/r/dbgate/dbgate">docker hub</Link>
|
||||
</div>
|
||||
<div class="m-1">
|
||||
Online demo: <Link href="https://demo.dbgate.org">demo.dbgate.org</Link>
|
||||
</div>
|
||||
<div class="m-1">
|
||||
Search plugins: <Link href="https://www.npmjs.com/search?q=keywords:dbgateplugin">npmjs.com</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import ColumnMapColumnDropdown from '../elements/ColumnMapColumnDropdown.svelte';
|
||||
import Link from '../elements/Link.svelte';
|
||||
@@ -8,8 +7,10 @@
|
||||
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
import _ from 'lodash';
|
||||
|
||||
export let header = 'Configure columns';
|
||||
export let onConfirm;
|
||||
@@ -34,6 +35,15 @@
|
||||
skip: false,
|
||||
}));
|
||||
}
|
||||
if (sourceTableInfo && targetTableInfo) {
|
||||
return sourceTableInfo.columns
|
||||
.map(x => ({
|
||||
src: x.columnName,
|
||||
dst: targetTableInfo.columns.find(y => y.columnName == x.columnName)?.columnName,
|
||||
skip: false,
|
||||
}))
|
||||
.filter(x => x.dst != null);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -51,6 +61,32 @@
|
||||
$: differentFromReset = !equalValues(value, resetValue);
|
||||
|
||||
let value = initialValue?.length > 0 ? initialValue : resetValue;
|
||||
|
||||
let validationError;
|
||||
|
||||
function validate() {
|
||||
validationError = null;
|
||||
if (!value) return;
|
||||
if (value.length == 0) return;
|
||||
if (value.some(x => !x.src || !x.dst)) {
|
||||
validationError = 'Source and target columns must be defined';
|
||||
return;
|
||||
}
|
||||
const duplicates = _.chain(value.map(x => x.dst))
|
||||
.countBy()
|
||||
.pickBy(count => count > 1)
|
||||
.keys()
|
||||
.value();
|
||||
if (duplicates.length > 0) {
|
||||
validationError = 'Target columns must be unique, duplicates found: ' + duplicates.join(', ');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
value;
|
||||
validate();
|
||||
}
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
@@ -91,8 +127,8 @@
|
||||
<svelte:fragment slot="2" let:row let:index>
|
||||
<ColumnMapColumnDropdown
|
||||
value={row['dst']}
|
||||
onChange={e =>
|
||||
(value = (value || []).map((x, i) => (i == index ? { ...x, dst: e.target.value, ignore: false } : x)))}
|
||||
onChange={column =>
|
||||
(value = (value || []).map((x, i) => (i == index ? { ...x, dst: column, ignore: false } : x)))}
|
||||
tableInfo={targetTableInfo}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
@@ -105,9 +141,17 @@
|
||||
</svelte:fragment>
|
||||
</TableControl>
|
||||
|
||||
{#if validationError}
|
||||
<div class="error-result">
|
||||
<FontIcon icon="img error" />
|
||||
{validationError}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value="OK"
|
||||
disabled={!!validationError}
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
onConfirm(!value || value.length == 0 || !differentFromReset ? null : value);
|
||||
@@ -132,3 +176,9 @@
|
||||
</svelte:fragment>
|
||||
</ModalBase>
|
||||
</FormProvider>
|
||||
|
||||
<style>
|
||||
.error-result {
|
||||
margin: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,15 +8,23 @@
|
||||
|
||||
export let message;
|
||||
export let onConfirm;
|
||||
export let confirmLabel = 'OK';
|
||||
export let header = null;
|
||||
</script>
|
||||
|
||||
<FormProvider>
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">
|
||||
{#if header}
|
||||
{header}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{message}
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value="OK"
|
||||
value={confirmLabel}
|
||||
on:click={() => {
|
||||
closeCurrentModal();
|
||||
onConfirm();
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<DropDownMenu
|
||||
left={$currentDropDownMenu.left}
|
||||
top={$currentDropDownMenu.top}
|
||||
bottom={$currentDropDownMenu.bottom}
|
||||
items={$currentDropDownMenu.items}
|
||||
targetElement={$currentDropDownMenu.targetElement}
|
||||
on:close={() => ($currentDropDownMenu = null)}
|
||||
|
||||
@@ -7,29 +7,6 @@
|
||||
if (side == 'right') return { top: top, left: left + box.width };
|
||||
return { top: top, left: left };
|
||||
}
|
||||
|
||||
function fixPopupPlacement(element) {
|
||||
const { width, height } = element.getBoundingClientRect();
|
||||
let offset = getElementOffset(element);
|
||||
|
||||
let newLeft = null;
|
||||
let newTop = null;
|
||||
|
||||
if (offset.left + width > window.innerWidth) {
|
||||
newLeft = offset.left - width;
|
||||
|
||||
if (newLeft < 0) newLeft = 0;
|
||||
}
|
||||
|
||||
if (offset.top + height > window.innerHeight) {
|
||||
newTop = offset.top - height;
|
||||
|
||||
if (newTop < 0) newTop = 0;
|
||||
}
|
||||
|
||||
if (newLeft != null) element.style.left = `${newLeft}px`;
|
||||
if (newTop != null) element.style.top = `${newTop}px`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
@@ -43,12 +20,17 @@
|
||||
|
||||
export let items;
|
||||
export let top;
|
||||
export let bottom;
|
||||
export let left;
|
||||
export let onCloseParent;
|
||||
export let targetElement;
|
||||
export let submenuLevel = 0;
|
||||
|
||||
let element;
|
||||
|
||||
let newLeft = undefined;
|
||||
let newTop = undefined;
|
||||
|
||||
let hoverItem;
|
||||
let hoverOffset;
|
||||
|
||||
@@ -57,6 +39,8 @@
|
||||
|
||||
let switchIndex = 0;
|
||||
|
||||
let submenuKey = 0;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let closeHandlers = [];
|
||||
|
||||
@@ -68,6 +52,25 @@
|
||||
closeHandlers = [];
|
||||
}
|
||||
|
||||
function fixPopupPlacement(element) {
|
||||
const { width, height } = element.getBoundingClientRect();
|
||||
let offset = getElementOffset(element);
|
||||
|
||||
if (offset.left + width > window.innerWidth) {
|
||||
newLeft = offset.left - width;
|
||||
|
||||
if (newLeft < 0) newLeft = 0;
|
||||
}
|
||||
|
||||
if (offset.top < 0) {
|
||||
newTop = 0;
|
||||
} else if (offset.top + height > window.innerHeight) {
|
||||
newTop = offset.top - height;
|
||||
|
||||
if (newTop < 0) newTop = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function registerCloseHandler(handler) {
|
||||
closeHandlers.push(handler);
|
||||
}
|
||||
@@ -80,6 +83,7 @@
|
||||
|
||||
submenuItem = item;
|
||||
submenuOffset = hoverOffset;
|
||||
submenuKey += 1;
|
||||
return;
|
||||
}
|
||||
if (item.switchStore && item.switchValue) {
|
||||
@@ -109,6 +113,7 @@
|
||||
const changeActiveSubmenu = _.throttle(() => {
|
||||
submenuItem = hoverItem;
|
||||
submenuOffset = hoverOffset;
|
||||
submenuKey += 1;
|
||||
}, 500);
|
||||
|
||||
$: preparedItems = prepareMenuItems(items, { targetElement, registerCloseHandler }, $commandsCustomized);
|
||||
@@ -128,7 +133,14 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<ul class="dropDownMenuMarker" style={`left: ${left}px; top: ${top}px`} bind:this={element}>
|
||||
<ul
|
||||
class="dropDownMenuMarker"
|
||||
style:top={(newTop ?? top) != null ? `${newTop ?? top}px` : undefined}
|
||||
style:bottom={newTop == null && bottom != null ? `${bottom}px` : undefined}
|
||||
style:left={(newLeft ?? left) != null ? `${newLeft ?? left}px` : undefined}
|
||||
bind:this={element}
|
||||
data-testid={`DropDownMenu-container-${submenuLevel}`}
|
||||
>
|
||||
{#each preparedItems as item}
|
||||
{#if item.divider}
|
||||
<li class="divider" />
|
||||
@@ -172,14 +184,17 @@
|
||||
{/each}
|
||||
</ul>
|
||||
{#if submenuItem?.submenu}
|
||||
<svelte:self
|
||||
items={submenuItem?.submenu}
|
||||
{...submenuOffset}
|
||||
onCloseParent={() => {
|
||||
if (onCloseParent) onCloseParent();
|
||||
dispatchClose();
|
||||
}}
|
||||
/>
|
||||
{#key submenuKey}
|
||||
<svelte:self
|
||||
items={submenuItem?.submenu}
|
||||
{...submenuOffset}
|
||||
onCloseParent={() => {
|
||||
if (onCloseParent) onCloseParent();
|
||||
dispatchClose();
|
||||
}}
|
||||
submenuLevel={submenuLevel + 1}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
||||
@@ -39,7 +39,14 @@
|
||||
<!-- The Modal -->
|
||||
<div id="myModal" class="bglayer">
|
||||
<!-- Modal content -->
|
||||
<div class="window" class:fullScreen class:simple use:clickOutside on:clickOutside={handleClickOutside}>
|
||||
<div
|
||||
class="window"
|
||||
class:fullScreen
|
||||
class:simple
|
||||
use:clickOutside
|
||||
on:clickOutside={handleClickOutside}
|
||||
data-testid="ModalBase_window"
|
||||
>
|
||||
{#if $$slots.header}
|
||||
<div class="header" class:fullScreen>
|
||||
<div><slot name="header" /></div>
|
||||
|
||||
@@ -1,13 +1,24 @@
|
||||
<script lang="ts">
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
import { openedConnections, openedSingleDatabaseConnections } from '../stores';
|
||||
import { extensions, openedConnections, openedSingleDatabaseConnections } from '../stores';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
import FormTextAreaField from '../forms/FormTextAreaField.svelte';
|
||||
import FormArgumentList from '../forms/FormArgumentList.svelte';
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
$: engine = $values.engine;
|
||||
|
||||
$: driver = $extensions.drivers.find(x => x.engine == engine);
|
||||
|
||||
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
|
||||
|
||||
$: advancedFields = driver?.getAdvancedConnectionFields ? driver?.getAdvancedConnectionFields() : null;
|
||||
</script>
|
||||
|
||||
<FormTextAreaField label="Allowed databases, one per line" name="allowedDatabases" disabled={isConnected} rows={8} />
|
||||
<FormTextField label="Allowed databases regular expression" name="allowedDatabasesRegex" disabled={isConnected} />
|
||||
|
||||
{#if advancedFields}
|
||||
<FormArgumentList args={advancedFields} />
|
||||
{/if}
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
selectedWidget,
|
||||
lockedDatabaseMode,
|
||||
visibleWidgetSideBar,
|
||||
currentTheme,
|
||||
getSystemTheme,
|
||||
} from '../stores';
|
||||
import { isMac } from '../utility/common';
|
||||
import getElectron from '../utility/getElectron';
|
||||
@@ -280,6 +282,32 @@ ORDER BY
|
||||
|
||||
<svelte:fragment slot="3">
|
||||
<div class="heading">Application theme</div>
|
||||
|
||||
<FormFieldTemplateLarge
|
||||
label="Use system theme"
|
||||
type="checkbox"
|
||||
labelProps={{
|
||||
onClick: () => {
|
||||
if ($currentTheme) {
|
||||
$currentTheme = null;
|
||||
} else {
|
||||
$currentTheme = getSystemTheme();
|
||||
}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<CheckboxField
|
||||
checked={!$currentTheme}
|
||||
on:change={e => {
|
||||
if (e.target['checked']) {
|
||||
$currentTheme = null;
|
||||
} else {
|
||||
$currentTheme = getSystemTheme();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FormFieldTemplateLarge>
|
||||
|
||||
<div class="themes">
|
||||
{#each $extensions.themes as theme}
|
||||
<ThemeSkeleton {theme} />
|
||||
@@ -403,6 +431,12 @@ ORDER BY
|
||||
|
||||
<FormCheckboxField name="behaviour.useTabPreviewMode" label="Use tab preview mode" defaultValue={true} />
|
||||
|
||||
<FormCheckboxField
|
||||
name="behaviour.jsonPreviewWrap"
|
||||
label={_t('settings.behaviour.jsonPreviewWrap', { defaultMessage: 'Wrap json in preview' })}
|
||||
defaultValue={false}
|
||||
/>
|
||||
|
||||
<div class="tip">
|
||||
<FontIcon icon="img tip" /> When you single-click or select a file in the "Tables, Views, Functions" view, it
|
||||
is shown in a preview mode and reuses an existing tab (preview tab). This is useful if you are quickly browsing
|
||||
@@ -495,16 +529,12 @@ ORDER BY
|
||||
label="Folder with mysql plugins (for example for authentication). Set only in case of problems"
|
||||
defaultValue=""
|
||||
/>
|
||||
<FormTextField
|
||||
<FormTextField
|
||||
name="externalTools.pg_dump"
|
||||
label="pg_dump (backup PostgreSQL database)"
|
||||
defaultValue="pg_dump"
|
||||
/>
|
||||
<FormTextField
|
||||
name="externalTools.psql"
|
||||
label="psql (restore PostgreSQL database)"
|
||||
defaultValue="psql"
|
||||
/>
|
||||
<FormTextField name="externalTools.psql" label="psql (restore PostgreSQL database)" defaultValue="psql" />
|
||||
</svelte:fragment>
|
||||
</TabControl>
|
||||
</FormValues>
|
||||
|
||||
@@ -26,15 +26,29 @@ export interface TabDefinition {
|
||||
focused?: boolean;
|
||||
}
|
||||
|
||||
function getSystemTheme() {
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'theme-dark' : 'theme-light';
|
||||
const darkModeMediaQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
|
||||
|
||||
export const systemThemeStore = writable(darkModeMediaQuery?.matches ? 'theme-dark' : 'theme-light');
|
||||
|
||||
if (darkModeMediaQuery) {
|
||||
darkModeMediaQuery.addEventListener('change', e => {
|
||||
systemThemeStore.set(e.matches ? 'theme-dark' : 'theme-light');
|
||||
});
|
||||
}
|
||||
|
||||
export function writableWithStorage<T>(defaultValue: T, storageName) {
|
||||
export function getSystemTheme() {
|
||||
return darkModeMediaQuery?.matches ? 'theme-dark' : 'theme-light';
|
||||
}
|
||||
|
||||
export function writableWithStorage<T>(defaultValue: T, storageName, removeCondition?: (value: T) => boolean) {
|
||||
const init = localStorage.getItem(storageName);
|
||||
const res = writable<T>(init ? safeJsonParse(init, defaultValue, true) : defaultValue);
|
||||
res.subscribe(value => {
|
||||
localStorage.setItem(storageName, JSON.stringify(value));
|
||||
if (removeCondition && removeCondition(value)) {
|
||||
localStorage.removeItem(storageName);
|
||||
} else {
|
||||
localStorage.setItem(storageName, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
return res;
|
||||
}
|
||||
@@ -104,8 +118,8 @@ export const extensions = writable<ExtensionsDirectory>(null);
|
||||
export const visibleCommandPalette = writable(null);
|
||||
export const commands = writable({});
|
||||
export const currentTheme = getElectron()
|
||||
? writableSettingsValue(getSystemTheme(), 'currentTheme')
|
||||
: writableWithStorage(getSystemTheme(), 'currentTheme');
|
||||
? writableSettingsValue(null, 'currentTheme')
|
||||
: writableWithStorage(null, 'currentTheme', x => x == null);
|
||||
export const currentEditorTheme = getElectron()
|
||||
? writableSettingsValue(null, 'currentEditorTheme')
|
||||
: writableWithStorage(null, 'currentEditorTheme');
|
||||
@@ -197,12 +211,24 @@ export const connectionAppObjectSearchSettings = writableWithStorage(
|
||||
'connectionAppObjectSearchSettings2'
|
||||
);
|
||||
|
||||
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
|
||||
$extensions?.themes?.find(x => x.themeClassName == $currentTheme)
|
||||
let currentThemeValue = null;
|
||||
currentTheme.subscribe(value => {
|
||||
currentThemeValue = value;
|
||||
});
|
||||
export const getCurrentTheme = () => currentThemeValue;
|
||||
|
||||
export const currentThemeDefinition = derived(
|
||||
[currentTheme, extensions, systemThemeStore],
|
||||
([$currentTheme, $extensions, $systemTheme]) => {
|
||||
const usedTheme = $currentTheme ?? $systemTheme;
|
||||
return $extensions?.themes?.find(x => x.themeClassName == usedTheme);
|
||||
}
|
||||
);
|
||||
currentThemeDefinition.subscribe(value => {
|
||||
if (value?.themeType) {
|
||||
if (value?.themeType && getCurrentTheme()) {
|
||||
localStorage.setItem('currentThemeType', value?.themeType);
|
||||
} else {
|
||||
localStorage.removeItem('currentThemeType');
|
||||
}
|
||||
});
|
||||
export const openedConnectionsWithTemporary = derived(
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
export let tabid;
|
||||
export let conid;
|
||||
export let connectionStore = undefined;
|
||||
export let inlineTabs = false;
|
||||
|
||||
export let onlyTestButton;
|
||||
|
||||
@@ -237,8 +238,10 @@
|
||||
<div class="wrapper">
|
||||
<TabControl
|
||||
isInline
|
||||
{inlineTabs}
|
||||
containerMaxWidth="800px"
|
||||
flex1={false}
|
||||
contentTestId="ConnectionTab_tabControlContent"
|
||||
tabs={[
|
||||
{
|
||||
label: 'General',
|
||||
|
||||
@@ -15,24 +15,29 @@
|
||||
|
||||
<script lang="ts">
|
||||
import useEditorData from '../query/useEditorData';
|
||||
import { extensions } from '../stores';
|
||||
import { useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { registerFileCommands } from '../commands/stdCommands';
|
||||
import createUndoReducer from '../utility/createUndoReducer';
|
||||
import _ from 'lodash';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import DiagramDesigner from '../designer/DiagramDesigner.svelte';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import invalidateCommands from '../commands/invalidateCommands';
|
||||
import ToolStripSaveButton from '../buttons/ToolStripSaveButton.svelte';
|
||||
import VerticalSplitter from "../elements/VerticalSplitter.svelte";
|
||||
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
|
||||
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
|
||||
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
|
||||
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
|
||||
import ToolStripButton from '../buttons/ToolStripButton.svelte';
|
||||
import DiagramSettings from '../designer/DiagramSettings.svelte';
|
||||
import { derived } from 'svelte/store';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
export let database;
|
||||
export let initialArgs;
|
||||
|
||||
let tableCounts = {};
|
||||
|
||||
export const activator = createActivator('DiagramTab', true);
|
||||
|
||||
@@ -87,6 +92,22 @@
|
||||
references: [],
|
||||
});
|
||||
|
||||
const setStyle = style =>
|
||||
// @ts-ignore
|
||||
dispatchModel({
|
||||
type: 'compute',
|
||||
compute: v => ({ ...v, style: _.isFunction(style) ? style(v.style) : style }),
|
||||
});
|
||||
|
||||
const styleDerivedStore = derived(modelState, ($modelState: any) =>
|
||||
$modelState.value ? $modelState.value.style || {} : {}
|
||||
);
|
||||
const styleStore = {
|
||||
...styleDerivedStore,
|
||||
update: setStyle,
|
||||
set: setStyle,
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ command: 'diagram.save' },
|
||||
@@ -98,20 +119,56 @@
|
||||
{ command: 'diagram.redo' },
|
||||
];
|
||||
}
|
||||
|
||||
function handleReportCounts(counts) {
|
||||
tableCounts = counts;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
<VerticalSplitter isSplitter={false}>
|
||||
<HorizontalSplitter isSplitter={isProApp() ? ($styleStore.settingsVisible ?? true) : false} initialSizeRight={300}>
|
||||
<svelte:fragment slot="1">
|
||||
<DiagramDesigner value={$modelState.value || {}} {conid} {database} onChange={handleChange} menu={createMenu} />
|
||||
<DiagramDesigner
|
||||
value={$modelState.value || {}}
|
||||
{conid}
|
||||
{database}
|
||||
onChange={handleChange}
|
||||
menu={createMenu}
|
||||
columnFilter={$styleStore.columnFilter}
|
||||
onReportCounts={handleReportCounts}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</VerticalSplitter>
|
||||
|
||||
<svelte:fragment slot="2">
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem
|
||||
title="Settings"
|
||||
name="diagramSettings"
|
||||
storageName="diagramSettingsWidget"
|
||||
onClose={() => {
|
||||
styleStore.update(x => ({ ...x, settingsVisible: false }));
|
||||
}}
|
||||
>
|
||||
<WidgetsInnerContainer skipDefineWidth>
|
||||
<DiagramSettings values={styleStore} {tableCounts} />
|
||||
</WidgetsInnerContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</svelte:fragment>
|
||||
</HorizontalSplitter>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="designer.arrange" />
|
||||
<ToolStripSaveButton idPrefix="diagram" />
|
||||
<ToolStripCommandButton command="diagram.export" />
|
||||
<ToolStripCommandButton command="diagram.undo" />
|
||||
<ToolStripCommandButton command="diagram.redo" />
|
||||
{#if isProApp()}
|
||||
<ToolStripButton
|
||||
icon="icon settings"
|
||||
on:click={() => {
|
||||
styleStore.update(x => ({ ...x, settingsVisible: !x.settingsVisible }));
|
||||
}}>Settings</ToolStripButton
|
||||
>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
@@ -5,16 +5,26 @@ import { runGroupCommand } from '../commands/runCommand';
|
||||
import { currentDropDownMenu, visibleCommandPalette } from '../stores';
|
||||
import getAsArray from './getAsArray';
|
||||
|
||||
let isContextMenuSupressed = false;
|
||||
|
||||
export function registerMenu(...items) {
|
||||
const parentMenu = getContext('componentContextMenu');
|
||||
setContext('componentContextMenu', [parentMenu, ...items]);
|
||||
}
|
||||
|
||||
export function supressContextMenu() {
|
||||
isContextMenuSupressed = true;
|
||||
}
|
||||
|
||||
export default function contextMenu(node, items: any = []) {
|
||||
const handleContextMenu = async e => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (isContextMenuSupressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invalidateCommands();
|
||||
|
||||
if (items) {
|
||||
@@ -24,13 +34,19 @@ export default function contextMenu(node, items: any = []) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseDown = () => {
|
||||
isContextMenuSupressed = false;
|
||||
};
|
||||
|
||||
if (items == '__no_menu') return;
|
||||
|
||||
node.addEventListener('contextmenu', handleContextMenu);
|
||||
node.addEventListener('mousedown', handleMouseDown);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
node.removeEventListener('contextmenu', handleContextMenu);
|
||||
node.removeEventListener('mousedown', handleMouseDown);
|
||||
},
|
||||
update(value) {
|
||||
items = value;
|
||||
|
||||
58
packages/web/src/utility/dragScroll.ts
Normal file
58
packages/web/src/utility/dragScroll.ts
Normal 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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -11,11 +11,16 @@ export default function moveDrag(node, dragEvents) {
|
||||
const handleMoveDown = e => {
|
||||
if (e.button != 0) return;
|
||||
|
||||
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
// const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
|
||||
const clientRect = node.getBoundingClientRect();
|
||||
clientX = clientRect.left * zoomKoef;
|
||||
clientY = clientRect.top * zoomKoef;
|
||||
clientX = clientRect.left;
|
||||
clientY = clientRect.top;
|
||||
|
||||
// console.log('ZOOM', zoomKoef);
|
||||
// console.log('CLIENT RECT', clientRect);
|
||||
// console.log('e.clientX', e.clientX);
|
||||
// console.log('e.clientY', e.clientY);
|
||||
|
||||
startX = e.clientX;
|
||||
startY = e.clientY;
|
||||
@@ -25,7 +30,7 @@ export default function moveDrag(node, dragEvents) {
|
||||
};
|
||||
|
||||
const handleMoveMove = e => {
|
||||
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
// const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
|
||||
e.preventDefault();
|
||||
const diffX = e.clientX - startX;
|
||||
@@ -36,7 +41,7 @@ export default function moveDrag(node, dragEvents) {
|
||||
onMove(diffX, diffY, e.clientX - clientX, e.clientY - clientY);
|
||||
};
|
||||
const handleMoveEnd = e => {
|
||||
const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
// const zoomKoef = window.getComputedStyle(node)['zoom'];
|
||||
|
||||
e.preventDefault();
|
||||
startX = null;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { createGridConfig } from 'dbgate-datalib';
|
||||
import { writable } from 'svelte/store';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { getOpenedTabs, openedTabs } from '../stores';
|
||||
import _ from 'lodash';
|
||||
|
||||
function doLoadGridConfigFunc(tabid) {
|
||||
try {
|
||||
@@ -17,9 +19,35 @@ function doLoadGridConfigFunc(tabid) {
|
||||
return createGridConfig();
|
||||
}
|
||||
|
||||
function containsNotEmptyObject(obj) {
|
||||
for (const key of Object.keys(obj)) {
|
||||
if (!_.isEmpty(obj[key])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export default function useGridConfig(tabid) {
|
||||
const config = writable(doLoadGridConfigFunc(tabid));
|
||||
const unsubscribe = config.subscribe(value => localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(value)));
|
||||
const unsubscribe = config.subscribe(value => {
|
||||
localStorage.setItem(`tabdata_grid_${tabid}`, JSON.stringify(value));
|
||||
|
||||
if (containsNotEmptyObject(value)) {
|
||||
if (getOpenedTabs().find(x => x.tabid == tabid)?.tabPreviewMode) {
|
||||
openedTabs.update(tabs =>
|
||||
tabs.map(x =>
|
||||
x.tabid == tabid
|
||||
? {
|
||||
...x,
|
||||
tabPreviewMode: false,
|
||||
}
|
||||
: x
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
onDestroy(unsubscribe);
|
||||
return config;
|
||||
}
|
||||
|
||||
80
packages/web/src/widgets/AdminPremiumPromoWidget.svelte
Normal file
80
packages/web/src/widgets/AdminPremiumPromoWidget.svelte
Normal 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 & 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>
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
<style>
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#starting-dbgate {
|
||||
.starting-dbgate {
|
||||
background-color: #111;
|
||||
color: #e3e3e3;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
visibleCommandPalette,
|
||||
} from '../stores';
|
||||
import { getConnectionLabel } from 'dbgate-tools';
|
||||
import { useConnectionList, useDatabaseServerVersion, useDatabaseStatus } from '../utility/metadataLoaders';
|
||||
import {
|
||||
useConfig,
|
||||
useConnectionList,
|
||||
useDatabaseServerVersion,
|
||||
useDatabaseStatus,
|
||||
} from '../utility/metadataLoaders';
|
||||
import { findCommand } from '../commands/runCommand';
|
||||
import { useConnectionColor } from '../utility/useConnectionColor';
|
||||
import { apiCall } from '../utility/api';
|
||||
@@ -27,6 +32,7 @@
|
||||
$: dbid = connection ? { conid: connection._id, database: databaseName } : null;
|
||||
$: status = useDatabaseStatus(dbid || {});
|
||||
$: serverVersion = useDatabaseServerVersion(dbid || {});
|
||||
$: config = useConfig();
|
||||
|
||||
$: contextItems = $statusBarTabInfo[$activeTabId] as any[];
|
||||
$: connectionLabel = getConnectionLabel(connection, { allowExplicitDatabase: false });
|
||||
@@ -171,6 +177,13 @@
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
{#if $config?.isUserLoggedIn && $config?.login}
|
||||
<div class="item clickable" on:click={() => visibleCommandPalette.set(findCommand('app.loggedUserCommands'))}>
|
||||
<FontIcon icon="icon users" padRight />
|
||||
{$config?.login}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $appUpdateStatus}
|
||||
<div class="item">
|
||||
<FontIcon icon={$appUpdateStatus.icon} padRight />
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import HistoryWidget from './HistoryWidget.svelte';
|
||||
import AppWidget from './AppWidget.svelte';
|
||||
import AdminMenuWidget from './AdminMenuWidget.svelte';
|
||||
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
|
||||
</script>
|
||||
|
||||
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
|
||||
@@ -33,3 +34,6 @@
|
||||
{#if $visibleSelectedWidget == 'admin'}
|
||||
<AdminMenuWidget />
|
||||
{/if}
|
||||
{#if $visibleSelectedWidget == 'premium'}
|
||||
<AdminPremiumPromoWidget />
|
||||
{/if}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
} from '../stores';
|
||||
import mainMenuDefinition from '../../../../app/src/mainMenuDefinition';
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
|
||||
let domSettings;
|
||||
let domMainMenu;
|
||||
@@ -61,6 +62,12 @@
|
||||
name: 'app',
|
||||
title: 'Application layers',
|
||||
},
|
||||
{
|
||||
icon: 'icon premium',
|
||||
name: 'premium',
|
||||
title: 'Premium promo',
|
||||
isPremiumPromo: true,
|
||||
},
|
||||
// {
|
||||
// icon: 'icon settings',
|
||||
// name: 'settings',
|
||||
@@ -104,7 +111,9 @@
|
||||
<FontIcon icon="icon menu" />
|
||||
</div>
|
||||
{/if}
|
||||
{#each widgets.filter(x => x && hasPermission(`widgets/${x.name}`)) as item}
|
||||
{#each widgets
|
||||
.filter(x => x && hasPermission(`widgets/${x.name}`))
|
||||
.filter(x => !x.isPremiumPromo || !isProApp()) as item}
|
||||
<div
|
||||
class="wrapper"
|
||||
class:selected={item.name == $visibleSelectedWidget}
|
||||
@@ -112,6 +121,9 @@
|
||||
on:click={() => handleChangeWidget(item.name)}
|
||||
>
|
||||
<FontIcon icon={item.icon} title={item.title} />
|
||||
{#if item.isPremiumPromo}
|
||||
<div class="premium-promo">Premium</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
@@ -141,6 +153,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--theme-font-inv-2);
|
||||
position: relative;
|
||||
}
|
||||
.wrapper:hover {
|
||||
color: var(--theme-font-inv-1);
|
||||
@@ -154,4 +167,15 @@
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.premium-promo {
|
||||
position: absolute;
|
||||
text-transform: uppercase;
|
||||
font-size: 6pt;
|
||||
background: var(--theme-bg-inv-3);
|
||||
color: var(--theme-font-inv-2);
|
||||
padding: 1px 3px;
|
||||
border-radius: 3px;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
export let hideContent = false;
|
||||
export let fixedWidth = 0;
|
||||
export let skipDefineWidth = false;
|
||||
|
||||
export function scrollTop() {
|
||||
domDiv.scrollTop = 0;
|
||||
@@ -13,7 +14,7 @@
|
||||
on:drop
|
||||
bind:this={domDiv}
|
||||
class:hideContent
|
||||
class:leftFixedWidth={!fixedWidth}
|
||||
class:leftFixedWidth={!fixedWidth && !skipDefineWidth}
|
||||
data-testid={$$props['data-testid']}
|
||||
style:width={fixedWidth ? `${fixedWidth}px` : undefined}
|
||||
>
|
||||
|
||||
@@ -34,7 +34,10 @@ class Analyser extends DatabaseAnalyser {
|
||||
this.feedback({ analysingMessage: 'Loading columns' });
|
||||
const columns = await this.analyserQuery('columns', ['tables', 'views']);
|
||||
this.feedback({ analysingMessage: 'Loading views' });
|
||||
const views = await this.analyserQuery('views', ['views']);
|
||||
let views = await this.analyserQuery('views', ['views']);
|
||||
if (views?.isError) {
|
||||
views = await this.analyserQuery('viewsNoDefinition', ['views']);
|
||||
}
|
||||
|
||||
const res = {
|
||||
tables: tables.rows.map((table) => ({
|
||||
@@ -64,7 +67,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
...col,
|
||||
...extractDataType(col.dataType),
|
||||
})),
|
||||
createSql: `CREATE VIEW "${view.pureName}"\nAS\n${view.viewDefinition}`,
|
||||
createSql: view.viewDefinition ? `CREATE VIEW "${view.pureName}"\nAS\n${view.viewDefinition}` : '',
|
||||
})),
|
||||
};
|
||||
this.feedback({ analysingMessage: null });
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
const columns = require('./columns');
|
||||
const tables = require('./tables');
|
||||
const views = require('./views');
|
||||
const viewsNoDefinition = require('./viewsNoDefinition');
|
||||
const tableModifications = require('./tableModifications');
|
||||
|
||||
module.exports = {
|
||||
columns,
|
||||
tables,
|
||||
views,
|
||||
viewsNoDefinition,
|
||||
tableModifications,
|
||||
};
|
||||
|
||||
@@ -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'
|
||||
`;
|
||||
@@ -164,7 +164,7 @@ class Analyser extends DatabaseAnalyser {
|
||||
this.feedback({ analysingMessage: 'Loading triggers' });
|
||||
const triggers = await this.analyserQuery('triggers');
|
||||
|
||||
this.feedback({ analysingMessage: 'Loading scehduler events' });
|
||||
this.feedback({ analysingMessage: 'Loading scheduler events' });
|
||||
const schedulerEvents = await this.analyserQuery('schedulerEvents');
|
||||
|
||||
const uniqueNames = await this.analyserQuery('uniqueNames', ['tables']);
|
||||
|
||||
@@ -81,12 +81,25 @@ function splitCommandLine(str) {
|
||||
const driver = {
|
||||
...driverBase,
|
||||
analyserClass: Analyser,
|
||||
async connect({ server, port, user, password, database, useDatabaseUrl, databaseUrl, treeKeySeparator, ssl }) {
|
||||
async connect({
|
||||
server,
|
||||
port,
|
||||
user,
|
||||
password,
|
||||
database,
|
||||
useDatabaseUrl,
|
||||
databaseUrl,
|
||||
treeKeySeparator,
|
||||
ssl,
|
||||
skipSetName,
|
||||
}) {
|
||||
let db = 0;
|
||||
let client;
|
||||
if (useDatabaseUrl) {
|
||||
client = new Redis(databaseUrl);
|
||||
await client.client('SETNAME', 'dbgate');
|
||||
if (!skipSetName) {
|
||||
await client.client('SETNAME', 'dbgate');
|
||||
}
|
||||
} else {
|
||||
if (_.isString(database) && database.startsWith('db')) db = parseInt(database.substring(2));
|
||||
if (_.isNumber(database)) db = database;
|
||||
@@ -96,15 +109,18 @@ const driver = {
|
||||
passphrase: ssl.password,
|
||||
};
|
||||
}
|
||||
client = new Redis({
|
||||
const connectionOptions = {
|
||||
host: server,
|
||||
port,
|
||||
username: user,
|
||||
password,
|
||||
db,
|
||||
connectionName: 'dbgate',
|
||||
tls: ssl,
|
||||
});
|
||||
};
|
||||
if (!skipSetName) {
|
||||
connectionOptions.connectionName = 'dbgate';
|
||||
}
|
||||
client = new Redis(connectionOptions);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -82,6 +82,16 @@ const driver = {
|
||||
}
|
||||
return ['server', 'port', 'user', 'password', 'isReadOnly', 'treeKeySeparator'].includes(field);
|
||||
},
|
||||
|
||||
getAdvancedConnectionFields() {
|
||||
return [
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'skipSetName',
|
||||
label: 'Skip SETNAME instruction',
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = driver;
|
||||
|
||||
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
|
||||
repository: dbgate/dbgate-pro
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
path: dbgate-pro
|
||||
ref: 54f1f9a82fbcca0307aa5c83f765b33af3325466
|
||||
ref: 00da2abe10e1ec8a3887b49dfabd42ccda365514
|
||||
- name: Merge dbgate/dbgate-pro
|
||||
run: |
|
||||
mkdir ../dbgate-pro
|
||||
|
||||
@@ -43,6 +43,12 @@ jobs:
|
||||
cd packages/datalib
|
||||
yarn test:ci
|
||||
|
||||
- name: Tools tests
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/tools
|
||||
yarn test:ci
|
||||
|
||||
- uses: tanmen/jest-reporter@v1
|
||||
if: always()
|
||||
with:
|
||||
|
||||
Reference in New Issue
Block a user