Compare commits

...

98 Commits

Author SHA1 Message Date
SPRINX0\prochazka c5ebc01978 v6.8.0 2025-12-17 15:38:39 +01:00
SPRINX0\prochazka f29b468fc1 changelog 2025-12-17 15:33:41 +01:00
SPRINX0\prochazka e796fbb990 save export jobs only in proapp 2025-12-17 14:58:12 +01:00
CI workflows 37a122c981 chore: auto-update github workflows 2025-12-17 13:42:41 +00:00
CI workflows 4f426a73f6 Update pro ref 2025-12-17 13:42:25 +00:00
Jan Prochazka 1bcb74cd85 SYNC: Merge pull request #20 from dbgate/feature/sfill2 2025-12-17 13:42:13 +00:00
SPRINX0\prochazka cd88c8de78 v6.7.4-premium-beta.2 2025-12-17 12:28:27 +01:00
CI workflows eee288b45b chore: auto-update github workflows 2025-12-17 11:27:35 +00:00
CI workflows 797cb7615d Update pro ref 2025-12-17 11:27:20 +00:00
Jan Prochazka ca4667ff1e SYNC: Merge pull request #19 from dbgate/feature/sfill 2025-12-17 11:27:08 +00:00
Jan Prochazka 66d1143ca0 replicator - support for skip update columns 2025-12-16 14:43:44 +01:00
SPRINX0\prochazka f310916c76 SYNC: fix 2025-12-16 09:30:38 +00:00
SPRINX0\prochazka 5d3d8ab932 v6.7.4-beta.1 2025-12-16 09:02:17 +01:00
SPRINX0\prochazka fd91c18460 SYNC: fixed e2e tests + new form cell view test 2025-12-15 15:03:43 +00:00
Jan Prochazka 2a12c04518 Merge pull request #1299 from dbgate/feature/wordwrap-editor
Feature/wordwrap editor
2025-12-15 13:26:54 +01:00
CI workflows d08cae6fa3 chore: auto-update github workflows 2025-12-15 12:23:33 +00:00
SPRINX0\prochazka d7f9de1881 concurrency settings for workflows 2025-12-15 13:23:10 +01:00
SPRINX0\prochazka 962190cc57 SYNC: code cleanup 2025-12-15 12:08:07 +00:00
SPRINX0\prochazka 4527866276 SYNC: form cell view - show JSON 2025-12-15 12:08:05 +00:00
SPRINX0\prochazka 088dfcd4dc SYNC: renamed table cell view => form cell view 2025-12-15 12:08:03 +00:00
SPRINX0\prochazka 6c317b6e64 SYNC: table cell view - single click 2025-12-15 12:08:01 +00:00
SPRINX0\prochazka 6b66c273b4 SYNC: fixed display numbers 2025-12-15 12:07:59 +00:00
SPRINX0\prochazka 60f31008c0 SYNC: table cell view UX 2025-12-15 12:07:58 +00:00
SPRINX0\prochazka 078f74db97 SYNC: cell data - allow to edit 2025-12-15 12:07:56 +00:00
SPRINX0\prochazka a0b025cf59 SYNC: Run macro context menu 2025-12-15 12:07:54 +00:00
SPRINX0\prochazka bc695f5af9 SYNC: run macro WIP 2025-12-15 12:07:52 +00:00
SPRINX0\prochazka 9685e63b09 SYNC: table cell view refactor 2025-12-15 12:07:50 +00:00
SPRINX0\prochazka 142791360c SYNC: working widget resizing 2025-12-15 12:07:48 +00:00
SPRINX0\prochazka e004ed2f4b SYNC: widget bar fix 2025-12-15 12:07:47 +00:00
SPRINX0\prochazka 23ed487252 SYNC: fix 2025-12-15 12:07:45 +00:00
SPRINX0\prochazka efefec3c20 SYNC: widgetbar refactor 2025-12-15 12:07:43 +00:00
SPRINX0\prochazka 3d2ad1cb9b SYNC: resize WIP 2025-12-15 12:07:41 +00:00
SPRINX0\prochazka 90d3016938 SYNC: resize heights 2025-12-15 12:07:39 +00:00
SPRINX0\prochazka 438f9fc94d SYNC: better widget panel height processing 2025-12-15 12:07:37 +00:00
SPRINX0\prochazka 82ec88cc2f SYNC: recompute WIP 2025-12-15 12:07:36 +00:00
SPRINX0\prochazka 149611041e SYNC: widget configuration saved to storage 2025-12-15 12:07:33 +00:00
SPRINX0\prochazka b12c79462e SYNC: widget column bar update 2025-12-15 12:07:31 +00:00
SPRINX0\prochazka fbf34fb730 SYNC: widgetcolumnbar refactor 2025-12-15 12:07:29 +00:00
SPRINX0\prochazka e1fe3eb710 SYNC: widgetcolumnbar props 2025-12-15 12:07:27 +00:00
SPRINX0\prochazka 76ae2e0e5a SYNC: improved data grid navigation 2025-12-15 12:07:25 +00:00
SPRINX0\prochazka a57063adf7 SYNC: refactor 2025-12-15 12:07:24 +00:00
SPRINX0\prochazka ff0157e624 SYNC: autodetect data grid cell 2025-12-15 12:07:22 +00:00
SPRINX0\prochazka af9701feb8 SYNC: cell data view 2025-12-15 12:07:20 +00:00
SPRINX0\prochazka 93c1f31588 SYNC: removed selectedCellsCallback 2025-12-15 12:07:17 +00:00
SPRINX0\prochazka 1964e54476 SYNC: cell data widget moved 2025-12-15 12:07:15 +00:00
Stela Augustinova 4682255d5f Refactor SQL editor settings layout and update word wrap field type 2025-12-15 13:02:11 +01:00
Stela Augustinova a503898b21 Refactor SQL editor to integrate word wrap settings and remove redundant options in QueryTab 2025-12-15 12:44:04 +01:00
Stela Augustinova 21352dae07 Revert "Implement word wrap feature in SQL editor and settings"
This reverts commit 28aa86f0aa.
2025-12-15 12:37:15 +01:00
Jan Prochazka 8470c7ac6b Merge pull request #1297 from dbgate/feature/redis-number-of-db
Retrieve the number of databases from Redis configuration
2025-12-15 10:50:53 +01:00
Stela Augustinova 28aa86f0aa Implement word wrap feature in SQL editor and settings 2025-12-14 17:26:25 +01:00
Stela Augustinova 3ed214269a Retrieve the number of databases from Redis configuration 2025-12-12 11:15:26 +01:00
SPRINX0\prochazka a71129df4b SYNC: AI assistant 2025-12-10 07:13:16 +00:00
SPRINX0\prochazka de6acfa1ce Revert "Revert "MPR archive""
This reverts commit ccf075dc65.
2025-12-10 07:48:30 +01:00
SPRINX0\prochazka ccf075dc65 Revert "MPR archive"
This reverts commit 391d04b45c.
2025-12-10 07:36:03 +01:00
SPRINX0\prochazka 1d8ac3cf86 Revert "MPR advanced exports"
This reverts commit 864797fc99.
2025-12-10 07:36:03 +01:00
Jan Prochazka 7a8ff89c5c Merge pull request #1293 from dbgate/feature/FK-test
Feature/fk test
2025-12-09 16:22:29 +01:00
CI workflows eda70def2a chore: auto-update github workflows 2025-12-09 15:02:50 +00:00
CI workflows 08fd75edc7 Update pro ref 2025-12-09 15:02:30 +00:00
Jan Prochazka 15ea53864f SYNC: Merge pull request #18 from dbgate/feature/translation5 2025-12-09 15:02:19 +00:00
Jan Prochazka 377cd64556 Revert "try to comment out earlier patch"
This reverts commit 955ca99cf3.
2025-12-09 12:56:47 +01:00
Stela Augustinova b37744d574 Merge pull request #1296 from dbgate/feature/map-autodetect-lat-lon
Feature/map autodetect lat lon
2025-12-09 11:01:49 +01:00
Stela Augustinova a7f21fe0c6 Merge pull request #1292 from dbgate/feature/table-cell-data-view
Feature/table cell data view
2025-12-09 11:00:19 +01:00
Jan Prochazka 955ca99cf3 try to comment out earlier patch 2025-12-09 10:46:25 +01:00
Jan Prochazka 98f5bb4124 sanitize constraints 2025-12-09 10:45:38 +01:00
Jan Prochazka b3943f005d alter table fixed 2025-12-08 17:35:29 +01:00
Jan Prochazka 8d4178b984 grouped table recreates 2025-12-08 16:57:09 +01:00
Stela Augustinova 2a88ed38c4 Added translation tags to TableCellView component, updated decimal handling 2025-12-08 16:45:18 +01:00
Jan Prochazka 52dce7dfd3 disabled grouping recreate table OPs 2025-12-08 16:41:55 +01:00
Stela Augustinova 6ebee92542 Merge branch 'master' into feature/table-cell-data-view 2025-12-08 16:07:55 +01:00
Jan Prochazka 1b5646f526 Revert "fix: correct reference from wholeNewDb to wholeOldDb in AlterPlan class"
This reverts commit 12e6afbaad.
2025-12-08 16:05:50 +01:00
Jan Prochazka 7024e4b40d Merge pull request #1289 from dbgate/feature/postgresql-decimal
Postgresql decimal
2025-12-08 15:42:19 +01:00
Jan Prochazka bc2e27d7da Merge pull request #1290 from david-pivonka/feature/table-cell-data-view
Add Table format to Cell data view sidebar
2025-12-08 15:35:35 +01:00
Jan Prochazka 189da2bfe2 Merge pull request #1291 from david-pivonka/fix/map-autodetect-lat-lon
Improve Map view lat/lon field autodetection
2025-12-08 15:33:58 +01:00
Stela Augustinova 12e6afbaad fix: correct reference from wholeNewDb to wholeOldDb in AlterPlan class 2025-12-08 15:27:45 +01:00
David Pivoňka 142ebe3d27 Fix scrolling in Table - Row view
Use absolute positioning pattern for proper scrolling behavior
when many columns are displayed.
2025-12-08 15:23:59 +01:00
Stela Augustinova 7579f6e42a fix: comment out incremental analysis in testTableDiff and correct clickhouse image reference 2025-12-08 15:19:27 +01:00
David Pivoňka 38c25cae74 Add multi-row selection support with bulk editing
- Show "(Multiple values)" when selected rows have different values
- Allow bulk editing: changes apply to all selected rows
- Rename format to "Table - Row" for clarity
2025-12-08 15:01:02 +01:00
David Pivoňka 408496eb7c Improve Map view lat/lon field autodetection
Prioritize field names that are more likely to be actual latitude/longitude
fields instead of randomly selecting the first numeric field containing
"lat" or "lon" in its name.
2025-12-08 14:42:56 +01:00
Jan Prochazka 4d61c74a8b fixed tes on CI 2025-12-08 14:34:45 +01:00
David Pivoňka 190c610466 Add column filter/search to Table cell data view
Adds a search input at the top of the Table view that filters columns
by name with regex support.
2025-12-08 14:31:38 +01:00
Jan Prochazka 85b7e3ebe3 fixed sqlite test folder 2025-12-08 14:18:28 +01:00
David Pivoňka d220525ac7 Use braces for isChangedRef.get() blocks to match codebase style 2025-12-08 13:47:35 +01:00
David Pivoňka 5e4a631ff2 Remove comments and apply early return pattern 2025-12-08 13:43:41 +01:00
David Pivoňka 9099ce42b9 Add Table format to Cell data view sidebar
Adds a new "Table" format option to the Cell data view widget that
displays the selected row as a vertical list with column names above
values, inspired by TablePlus.

Features:
- Shows all columns from the selected row in grid display order
- Inline editing support for regular values (double-click to edit)
- JSON values open Edit Cell modal on double-click
- Open-in-new button for JSON values to view in JSON tab
2025-12-08 13:37:55 +01:00
Jan Prochazka df226fea22 import test - greater timeout 2025-12-08 13:12:08 +01:00
Jan Prochazka 851d2e9151 fixed double drop constraint 2025-12-08 13:05:38 +01:00
SPRINX0\prochazka 89121a2608 handled UTF-8 BOM in CSV input 2025-12-04 16:44:08 +01:00
SPRINX0\prochazka 23cf264d4d fix 2025-12-04 16:29:06 +01:00
Stela Augustinova b3130225b5 Filter out primary key columns in nullability change tests 2025-12-04 14:53:08 +01:00
Stela Augustinova 65512defed Merge branch 'master' into feature/FK-test 2025-12-04 14:36:14 +01:00
Stela Augustinova 3b1c8748f1 Add createForeignKeyFore method to handle foreign key creation (Oracle) 2025-12-04 14:34:26 +01:00
Stela Augustinova aba660eddb Fix nullability filter in alter table tests 2025-12-04 14:08:22 +01:00
Stela Augustinova 137eac7dbf Add dropColumnDependencies property to dialect configuration 2025-12-04 14:07:55 +01:00
Stela Augustinova fdbd08f511 Added FK constraint type for sqlite 2025-12-04 13:00:06 +01:00
Stela Augustinova ace1cec1f6 Delete FK from drop column 2025-12-04 10:00:48 +01:00
Stela Augustinova fa5fda0c3b Incremental analysis 2025-12-03 15:31:05 +01:00
Stela Augustinova 251609e274 Update foreign key references when dropping or renaming columns in alter table tests 2025-12-03 15:21:12 +01:00
Stela Augustinova c0287e49d8 FK test 2025-12-03 14:28:58 +01:00
89 changed files with 2708 additions and 580 deletions
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -39,7 +39,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -44,7 +44,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+4 -1
View File
@@ -9,6 +9,9 @@ name: Cypress tests with screenshots PREMIUM
- develop
- feature/**
- hotfix/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-tests:
runs-on: ubuntu-latest
@@ -26,7 +29,7 @@ jobs:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+3
View File
@@ -9,6 +9,9 @@ name: Integration and unit tests
- develop
- feature/**
- hotfix/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
all-tests:
runs-on: ubuntu-latest
+19 -1
View File
@@ -8,6 +8,24 @@ Builds:
- linux - application for linux
- win - application for Windows
## 6.8.0
- ADDED: Form cell view for detailed data inspection and editing in data grids, with multi-row bulk editing support
- CHANGED: Cell data sidebar moved to right side, now is part of data grid
- FIXED: Improved widget resizing algorithm
- FIXED: Word wrap feature in SQL editor
- CHANGED: Data grid keyboard navigation improvements
- CHANGED: Improved PostgreSQL decimal type support in data grid #1214
- ADDED: Retrieve number of databases from Redis configuration #1278
- ADDED: Run macro context menu (Premium)
- ADDED: Support for skip update columns in replicator
- FIXED: UTF-8 BOM handling in CSV input
- CHANGED: Advanced export is now part of Community edition
- FIXED: SQLite foreign key constraint types
- FIXED: Double drop constraint issue
- CHANGED: Improved map view lat/lon field autodetection
- FIXED: Alter table operations and constraint sanitization
- ADDED: Import connections from environment variables (Team Premium)
## 6.7.3
- FIXED: Fixed problem in analyser core - in PostgreSQL, after dropping table, dropped table still appeared in structure
- FIXED: PostgreSQL numeric columns do not align right #1254
@@ -79,7 +97,7 @@ Builds:
- ADDED: SQL AI assistant - powered by database chat, could help you to write SQL queries (Premium)
- ADDED: Explain SQL error (powered by AI) (Premium)
- ADDED: Database chat (and SQL AI Assistant) now supports showing charts (Premium)
- FIXED: Fxied editing new files and roles (Team Premium)
- FIXED: Fixed editing new files and roles (Team Premium)
- FIXED: Connection to standalone database could be now pinned
- FIXED: Cannot open up large JSON file #1215
+18 -6
View File
@@ -202,7 +202,7 @@ describe('Data browser data', () => {
cy.themeshot('query-editor-join-wizard');
});
it('Mongo JSON data view', () => {
it('Mongo query JSON data view', () => {
cy.contains('Mongo-connection').click();
cy.contains('MgChinook').click();
cy.contains('Customer').click();
@@ -213,9 +213,10 @@ describe('Data browser data', () => {
cy.contains('Open query').click();
cy.wait(1000);
cy.contains('Execute').click();
cy.testid('WidgetIconPanel_cell-data').click();
cy.testid('TabContent_1').contains('Leonie').rightclick();
cy.contains('Show cell data').click();
// test JSON view
cy.contains('Country: "Brazil"');
cy.contains('Country: "Germany"');
cy.themeshot('mongo-query-json-view');
});
@@ -293,7 +294,8 @@ describe('Data browser data', () => {
// cy.contains('location').click();
cy.contains('14.2').click();
cy.contains('13.9').click({ shiftKey: true });
cy.testid('WidgetIconPanel_cell-data').click();
cy.testid('WidgetIconPanel_database').click();
cy.testid('TableDataTab_toggleCellDataView').click();
cy.wait(2000);
cy.themeshot('cell-map-view');
});
@@ -337,7 +339,7 @@ describe('Data browser data', () => {
cy.themeshot('save-changes-mongodb');
});
it('Edit mongo data JSON', () => {
it('Mongo JSON cell view', () => {
// TODO FIX: Auto expand cell view
cy.contains('Mongo-connection').click();
cy.contains('MgRivers').click();
@@ -347,7 +349,8 @@ describe('Data browser data', () => {
cy.testid('ColumnManagerRow_checkbox_countries.1').click();
cy.testid('ColumnManagerRow_checkbox__id').click();
cy.testid('DataFilterControl_input_countries.1').type('EXISTS{enter}');
cy.testid('WidgetIconPanel_cell-data').click();
cy.contains('Austria').click();
cy.testid('CollectionDataTab_toggleCellDataView').click();
cy.themeshot('mongodb-json-cell-view');
});
@@ -472,4 +475,13 @@ describe('Data browser data', () => {
cy.testid('DataDeployTab_importIntoDb').click();
cy.themeshot('data-replicator');
});
it('Form cell view', () => {
cy.contains('MySql-connection').click();
cy.contains('MyChinook').click();
cy.contains('Invoice').click();
cy.get('[data-row="0"][data-col="header"]').click();
cy.contains('Autodetect - Form');
cy.themeshot('form-cell-view');
});
});
+2 -2
View File
@@ -237,7 +237,7 @@ describe('Import CSV - source error', () => {
cy.testid('ImportExportTab_preview_content').contains('Invalid Closing Quote').should('be.visible');
cy.testid('ImportExportTab_executeButton').click();
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err').click();
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20-err', { timeout: 10000 }).click();
cy.testid('ErrorMessageModal_message').contains('Invalid Closing Quote').should('be.visible');
});
@@ -256,7 +256,7 @@ describe('Import CSV - target error', () => {
cy.contains('customers-20');
cy.testid('ImportExportConfigurator_targetName_customers-20').clear().type('system."]`');
cy.testid('ImportExportTab_executeButton').click();
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20').click();
cy.testid('ImportExportConfigurator_errorInfoIcon_customers-20', { timeout: 10000 }).click();
cy.testid('ErrorMessageModal_message').should('be.visible');
});
});
+32 -10
View File
@@ -26,13 +26,15 @@ function pickImportantTableInfo(engine, table) {
.map(props =>
_.omitBy(props, (v, k) => k == 'defaultValue' && v == 'NULL' && engine.setNullDefaultInsteadOfDrop)
),
// foreignKeys: table.foreignKeys
// .sort((a, b) => a.refTableName.localeCompare(b.refTableName))
// .map(fk => ({
// constraintType: fk.constraintType,
// refTableName: fk.refTableName,
// columns: fk.columns.map(col => ({ columnName: col.columnName, refColumnName: col.refColumnName })),
// })),
// TODO:
foreignKeys: table.foreignKeys
.sort((a, b) => a.refTableName.localeCompare(b.refTableName))
.map(fk => ({
constraintType: fk.constraintType,
refTableName: fk.refTableName,
columns: fk.columns.map(col => ({ columnName: col.columnName, refColumnName: col.refColumnName })),
})),
};
}
@@ -103,6 +105,7 @@ async function testTableDiff(engine, conn, driver, mangle, changedTable = 't1')
await driver.script(conn, sql);
// TODO:
// if (!engine.skipIncrementalAnalysis) {
// const structure2RealIncremental = await driver.analyseIncremental(conn, structure1Source);
// checkTableStructure(engine, tget(structure2RealIncremental), tget(structure2));
@@ -116,6 +119,7 @@ async function testTableDiff(engine, conn, driver, mangle, changedTable = 't1')
const TESTED_COLUMNS = ['col_pk', 'col_std', 'col_def', 'col_fk', 'col_ref', 'col_idx', 'col_uq'];
// const TESTED_COLUMNS = ['col_pk'];
// const TESTED_COLUMNS = ['col_fk'];
// const TESTED_COLUMNS = ['col_idx'];
// const TESTED_COLUMNS = ['col_def'];
// const TESTED_COLUMNS = ['col_std'];
@@ -179,11 +183,25 @@ describe('Alter table', () => {
)(
'Drop column - %s - %s',
testWrapper(async (conn, driver, column, engine) => {
await testTableDiff(engine, conn, driver, tbl => (tbl.columns = tbl.columns.filter(x => x.columnName != column)));
await testTableDiff(engine, conn, driver,
tbl => {
tbl.columns = tbl.columns.filter(x => x.columnName != column);
tbl.foreignKeys = tbl.foreignKeys
.map(fk => ({
...fk,
columns: fk.columns.filter(col => col.columnName != column)
}))
.filter(fk => fk.columns.length > 0);
}
);
})
);
test.each(createEnginesColumnsSource(engines.filter(x => !x.skipNullable && !x.skipChangeNullability)))(
test.each(
createEnginesColumnsSource(engines.filter(x => !x.skipNullability && !x.skipChangeNullability)).filter(
([_label, col]) => !col.endsWith('_pk')
)
)(
'Change nullability - %s - %s',
testWrapper(async (conn, driver, column, engine) => {
await testTableDiff(
@@ -202,7 +220,11 @@ describe('Alter table', () => {
engine,
conn,
driver,
tbl => (tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x)))
tbl => {
tbl.columns = tbl.columns.map(x => (x.columnName == column ? { ...x, columnName: 'col_renamed' } : x));
tbl.foreignKeys = tbl.foreignKeys.map(fk => ({...fk, columns: fk.columns.map(col => col.columnName == column ? { ...col, columnName: 'col_renamed' } : col)
}));
}
);
})
);
@@ -303,4 +303,52 @@ describe('Data replicator', () => {
}),
15 * 1000
);
test.each(engines.filter(x => !x.skipDataReplicator).map(engine => [engine.label, engine]))(
'Skip columns for update - %s',
testWrapper(async (conn, driver, engine) => {
runCommandOnDriver(conn, driver, dmp =>
dmp.createTable({
pureName: 't1',
columns: [
{ columnName: 'id', dataType: 'int', autoIncrement: true, notNull: true },
{ columnName: 'key', dataType: 'varchar(50)', notNull: true },
{ columnName: 'val', dataType: 'varchar(50)' },
],
primaryKey: {
columns: [{ columnName: 'id' }],
},
})
);
const getcfg = (v1 = 'v1') => ({
systemConnection: conn,
driver,
items: [
{
name: 't1',
matchColumns: ['key'],
skipUpdateColumns: ['val'],
findExisting: true,
updateExisting: true,
createNew: true,
jsonArray: [
{ key: '1', val: v1 },
{ key: '2', val: 'v2' },
{ key: '3', val: 'v3' },
],
},
],
});
await dataReplicator(getcfg('v1'));
const res1 = await runQueryOnDriver(conn, driver, dmp => dmp.put(`select ~val from ~t1 where ~key='1'`));
expect(res1.rows[0].val).toEqual('v1');
await dataReplicator(getcfg('v2'));
const res2 = await runQueryOnDriver(conn, driver, dmp => dmp.put(`select ~val from ~t1 where ~key='1'`));
expect(res2.rows[0].val).toEqual('v1');
})
);
});
+1 -1
View File
@@ -44,7 +44,7 @@ services:
# - 15942:9042
#
# clickhouse:
# image: bitnami/clickhouse:24.8.4
# image: bitnamilegacy/clickhouse:24.8.4
# restart: always
# ports:
# - 15005:8123
+3 -1
View File
@@ -22,7 +22,9 @@ async function connect(engine, database) {
if (engine.generateDbFile) {
const conn = await driver.connect({
...connection,
databaseFile: (engine.databaseFileLocationOnServer ?? 'dbtemp/') + database,
databaseFile:
(engine.databaseFileLocationOnServer ?? (process.env.CITEST ? 'dbtemp/' : 'integration-tests/dbtemp/')) +
database,
});
return conn;
} else {
+2 -1
View File
@@ -1,6 +1,6 @@
{
"private": true,
"version": "6.7.3",
"version": "6.8.0",
"name": "dbgate-all",
"workspaces": [
"packages/*",
@@ -22,6 +22,7 @@
"start:api:auth": "yarn workspace dbgate-api start:auth | pino-pretty",
"start:api:dblogin": "yarn workspace dbgate-api start:dblogin | pino-pretty",
"start:api:storage": "yarn workspace dbgate-api start:storage | pino-pretty",
"start:api:sfill": "yarn workspace dbgate-api start:sfill | pino-pretty",
"start:api:storage:built": "yarn workspace dbgate-api start:storage:built | pino-pretty",
"start:api:azure": "yarn workspace dbgate-api start:azure | pino-pretty",
"start:api:e2e:team": "yarn workspace dbgate-api start:e2e:team | pino-pretty",
+46
View File
@@ -0,0 +1,46 @@
DEVMODE=1
DEVWEB=1
STORAGE_SERVER=localhost
STORAGE_USER=root
STORAGE_PASSWORD=Pwd2020Db
STORAGE_PORT=3306
STORAGE_DATABASE=dbgate-filled
STORAGE_ENGINE=mysql@dbgate-plugin-mysql
CONNECTIONS=mysql,postgres,mongo,redis
LABEL_mysql=MySql
SERVER_mysql=dbgatedckstage1.sprinx.cz
USER_mysql=root
PASSWORD_mysql=Pwd2020Db
PORT_mysql=3306
ENGINE_mysql=mysql@dbgate-plugin-mysql
LABEL_postgres=Postgres
SERVER_postgres=dbgatedckstage1.sprinx.cz
USER_postgres=postgres
PASSWORD_postgres=Pwd2020Db
PORT_postgres=5432
ENGINE_postgres=postgres@dbgate-plugin-postgres
LABEL_mongo=Mongo
SERVER_mongo=dbgatedckstage1.sprinx.cz
USER_mongo=root
PASSWORD_mongo=Pwd2020Db
PORT_mongo=27017
ENGINE_mongo=mongo@dbgate-plugin-mongo
LABEL_redis=Redis
SERVER_redis=dbgatedckstage1.sprinx.cz
ENGINE_redis=redis@dbgate-plugin-redis
PORT_redis=6379
ROLE_test1_CONNECTIONS=mysql
ROLE_test1_PERMISSIONS=widgets/*
ROLE_test1_DATABASES_db1_CONNECTION=mysql
ROLE_test1_DATABASES_db1_PERMISSION=run_script
ROLE_test1_DATABASES_db1_DATABASES=db1
ROLE_test1_DATABASES_db2_CONNECTION=redis
ROLE_test1_DATABASES_db2_PERMISSION=run_script
ROLE_test1_DATABASES_db2_DATABASES=db2
+1
View File
@@ -75,6 +75,7 @@
"start:dblogin": "env-cmd -f env/dblogin/.env node src/index.js --listen-api",
"start:filedb": "env-cmd node src/index.js /home/jena/test/chinook/Chinook.db --listen-api",
"start:storage": "env-cmd -f env/storage/.env node src/index.js --listen-api",
"start:sfill": "env-cmd -f env/sfill/.env node src/index.js --listen-api",
"start:storage:built": "env-cmd -f env/storage/.env cross-env DEVMODE= BUILTWEBMODE=1 node dist/bundle.js --listen-api",
"start:singleconn": "env-cmd node src/index.js --server localhost --user root --port 3307 --engine mysql@dbgate-plugin-mysql --password test --listen-api",
"start:azure": "env-cmd -f env/azure/.env node src/index.js --listen-api",
+11 -49
View File
@@ -23,6 +23,7 @@ const pipeForkLogs = require('../utility/pipeForkLogs');
const requireEngineDriver = require('../utility/requireEngineDriver');
const { getAuthProviderById } = require('../auth/authProvider');
const { startTokenChecking } = require('../utility/authProxy');
const { extractConnectionsFromEnv } = require('../utility/envtools');
const logger = getLogger('connections');
@@ -61,55 +62,7 @@ function getDatabaseFileLabel(databaseFile) {
function getPortalCollections() {
if (process.env.CONNECTIONS) {
const connections = _.compact(process.env.CONNECTIONS.split(',')).map(id => ({
_id: id,
engine: process.env[`ENGINE_${id}`],
server: process.env[`SERVER_${id}`],
user: process.env[`USER_${id}`],
password: process.env[`PASSWORD_${id}`],
passwordMode: process.env[`PASSWORD_MODE_${id}`],
port: process.env[`PORT_${id}`],
databaseUrl: process.env[`URL_${id}`],
useDatabaseUrl: !!process.env[`URL_${id}`],
databaseFile: process.env[`FILE_${id}`]?.replace(
'%%E2E_TEST_DATA_DIRECTORY%%',
path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata')
),
socketPath: process.env[`SOCKET_PATH_${id}`],
serviceName: process.env[`SERVICE_NAME_${id}`],
authType: process.env[`AUTH_TYPE_${id}`] || (process.env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
defaultDatabase:
process.env[`DATABASE_${id}`] ||
(process.env[`FILE_${id}`] ? getDatabaseFileLabel(process.env[`FILE_${id}`]) : null),
singleDatabase: !!process.env[`DATABASE_${id}`] || !!process.env[`FILE_${id}`],
displayName: process.env[`LABEL_${id}`],
isReadOnly: process.env[`READONLY_${id}`],
databases: process.env[`DBCONFIG_${id}`] ? safeJsonParse(process.env[`DBCONFIG_${id}`]) : null,
allowedDatabases: process.env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'),
allowedDatabasesRegex: process.env[`ALLOWED_DATABASES_REGEX_${id}`],
parent: process.env[`PARENT_${id}`] || undefined,
useSeparateSchemas: !!process.env[`USE_SEPARATE_SCHEMAS_${id}`],
localDataCenter: process.env[`LOCAL_DATA_CENTER_${id}`],
// SSH tunnel
useSshTunnel: process.env[`USE_SSH_${id}`],
sshHost: process.env[`SSH_HOST_${id}`],
sshPort: process.env[`SSH_PORT_${id}`],
sshMode: process.env[`SSH_MODE_${id}`],
sshLogin: process.env[`SSH_LOGIN_${id}`],
sshPassword: process.env[`SSH_PASSWORD_${id}`],
sshKeyfile: process.env[`SSH_KEY_FILE_${id}`],
sshKeyfilePassword: process.env[`SSH_KEY_FILE_PASSWORD_${id}`],
// SSL
useSsl: process.env[`USE_SSL_${id}`],
sslCaFile: process.env[`SSL_CA_FILE_${id}`],
sslCertFile: process.env[`SSL_CERT_FILE_${id}`],
sslCertFilePassword: process.env[`SSL_CERT_FILE_PASSWORD_${id}`],
sslKeyFile: process.env[`SSL_KEY_FILE_${id}`],
sslRejectUnauthorized: process.env[`SSL_REJECT_UNAUTHORIZED_${id}`],
trustServerCertificate: process.env[`SSL_TRUST_CERTIFICATE_${id}`],
}));
const connections = extractConnectionsFromEnv(process.env);
for (const conn of connections) {
for (const prop in process.env) {
@@ -229,6 +182,15 @@ module.exports = {
);
}
await this.checkUnsavedConnectionsLimit();
if (process.env.STORAGE_DATABASE && process.env.CONNECTIONS) {
const storage = require('./storage');
try {
await storage.fillStorageConnectionsFromEnv();
} catch (err) {
logger.error(extractErrorLogData(err), 'DBGM-00268 Error filling storage connections from env');
}
}
},
list_meta: true,
+2 -2
View File
@@ -65,6 +65,8 @@ async function copyStream(input, output, options) {
});
}
} catch (err) {
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
process.send({
msgtype: 'copyStreamError',
copyStreamError: {
@@ -82,8 +84,6 @@ async function copyStream(input, output, options) {
errorMessage: extractErrorMessage(err),
});
}
logger.error(extractErrorLogData(err, { progressName }), 'DBGM-00157 Import/export job failed');
// throw err;
}
}
+1
View File
@@ -64,6 +64,7 @@ async function dataReplicator({
createNew: compileOperationFunction(item.createNew, item.createCondition),
updateExisting: compileOperationFunction(item.updateExisting, item.updateCondition),
deleteMissing: !!item.deleteMissing,
skipUpdateColumns: item.skipUpdateColumns,
deleteRestrictionColumns: item.deleteRestrictionColumns ?? [],
openStream: item.openStream
? item.openStream
+171 -2
View File
@@ -686,9 +686,34 @@ module.exports = {
"columnName": "connectionDefinition",
"dataType": "text",
"notNull": false
},
{
"pureName": "connections",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
},
{
"pureName": "connections",
"columnName": "id_original",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [
{
"constraintType": "foreignKey",
"constraintName": "FK_connections_import_source_id",
"pureName": "connections",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "connections",
"constraintType": "primaryKey",
@@ -790,6 +815,41 @@ module.exports = {
}
]
},
{
"pureName": "import_sources",
"columns": [
{
"pureName": "import_sources",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "import_sources",
"columnName": "name",
"dataType": "varchar(250)",
"notNull": true
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "import_sources",
"constraintType": "primaryKey",
"constraintName": "PK_import_sources",
"columns": [
{
"columnName": "id"
}
]
},
"preloadedRows": [
{
"id": -1,
"name": "env"
}
]
},
{
"pureName": "roles",
"columns": [
@@ -805,9 +865,34 @@ module.exports = {
"columnName": "name",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "roles",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
},
{
"pureName": "roles",
"columnName": "id_original",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [
{
"constraintType": "foreignKey",
"constraintName": "FK_roles_import_source_id",
"pureName": "roles",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "roles",
"constraintType": "primaryKey",
@@ -854,6 +939,12 @@ module.exports = {
"columnName": "connection_id",
"dataType": "int",
"notNull": true
},
{
"pureName": "role_connections",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
}
],
"foreignKeys": [
@@ -882,6 +973,18 @@ module.exports = {
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_role_connections_import_source_id",
"pureName": "role_connections",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
@@ -934,6 +1037,18 @@ module.exports = {
"columnName": "database_permission_role_id",
"dataType": "int",
"notNull": true
},
{
"pureName": "role_databases",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
},
{
"pureName": "role_databases",
"columnName": "id_original",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [
@@ -974,6 +1089,18 @@ module.exports = {
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_role_databases_import_source_id",
"pureName": "role_databases",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
@@ -1087,6 +1214,12 @@ module.exports = {
"columnName": "permission",
"dataType": "varchar(250)",
"notNull": true
},
{
"pureName": "role_permissions",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
}
],
"foreignKeys": [
@@ -1102,6 +1235,18 @@ module.exports = {
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_role_permissions_import_source_id",
"pureName": "role_permissions",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
@@ -1184,6 +1329,18 @@ module.exports = {
"columnName": "table_permission_scope_id",
"dataType": "int",
"notNull": true
},
{
"pureName": "role_tables",
"columnName": "import_source_id",
"dataType": "int",
"notNull": false
},
{
"pureName": "role_tables",
"columnName": "id_original",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [
@@ -1236,6 +1393,18 @@ module.exports = {
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_role_tables_import_source_id",
"pureName": "role_tables",
"refTableName": "import_sources",
"columns": [
{
"columnName": "import_source_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
+445
View File
@@ -0,0 +1,445 @@
const path = require('path');
const _ = require('lodash');
const { safeJsonParse, getDatabaseFileLabel } = require('dbgate-tools');
const crypto = require('crypto');
function extractConnectionsFromEnv(env) {
if (!env?.CONNECTIONS) {
return null;
}
const connections = _.compact(env.CONNECTIONS.split(',')).map(id => ({
_id: id,
engine: env[`ENGINE_${id}`],
server: env[`SERVER_${id}`],
user: env[`USER_${id}`],
password: env[`PASSWORD_${id}`],
passwordMode: env[`PASSWORD_MODE_${id}`],
port: env[`PORT_${id}`],
databaseUrl: env[`URL_${id}`],
useDatabaseUrl: !!env[`URL_${id}`],
databaseFile: env[`FILE_${id}`]?.replace(
'%%E2E_TEST_DATA_DIRECTORY%%',
path.join(path.dirname(path.dirname(__dirname)), 'e2e-tests', 'tmpdata')
),
socketPath: env[`SOCKET_PATH_${id}`],
serviceName: env[`SERVICE_NAME_${id}`],
authType: env[`AUTH_TYPE_${id}`] || (env[`SOCKET_PATH_${id}`] ? 'socket' : undefined),
defaultDatabase: env[`DATABASE_${id}`] || (env[`FILE_${id}`] ? getDatabaseFileLabel(env[`FILE_${id}`]) : null),
singleDatabase: !!env[`DATABASE_${id}`] || !!env[`FILE_${id}`],
displayName: env[`LABEL_${id}`],
isReadOnly: env[`READONLY_${id}`],
databases: env[`DBCONFIG_${id}`] ? safeJsonParse(env[`DBCONFIG_${id}`]) : null,
allowedDatabases: env[`ALLOWED_DATABASES_${id}`]?.replace(/\|/g, '\n'),
allowedDatabasesRegex: env[`ALLOWED_DATABASES_REGEX_${id}`],
parent: env[`PARENT_${id}`] || undefined,
useSeparateSchemas: !!env[`USE_SEPARATE_SCHEMAS_${id}`],
localDataCenter: env[`LOCAL_DATA_CENTER_${id}`],
// SSH tunnel
useSshTunnel: env[`USE_SSH_${id}`],
sshHost: env[`SSH_HOST_${id}`],
sshPort: env[`SSH_PORT_${id}`],
sshMode: env[`SSH_MODE_${id}`],
sshLogin: env[`SSH_LOGIN_${id}`],
sshPassword: env[`SSH_PASSWORD_${id}`],
sshKeyfile: env[`SSH_KEY_FILE_${id}`],
sshKeyfilePassword: env[`SSH_KEY_FILE_PASSWORD_${id}`],
// SSL
useSsl: env[`USE_SSL_${id}`],
sslCaFile: env[`SSL_CA_FILE_${id}`],
sslCertFile: env[`SSL_CERT_FILE_${id}`],
sslCertFilePassword: env[`SSL_CERT_FILE_PASSWORD_${id}`],
sslKeyFile: env[`SSL_KEY_FILE_${id}`],
sslRejectUnauthorized: env[`SSL_REJECT_UNAUTHORIZED_${id}`],
trustServerCertificate: env[`SSL_TRUST_CERTIFICATE_${id}`],
}));
return connections;
}
function extractImportEntitiesFromEnv(env) {
const portalConnections = extractConnectionsFromEnv(env) || [];
const connections = portalConnections.map((conn, index) => ({
...conn,
id_original: conn._id,
import_source_id: -1,
conid: crypto.randomUUID(),
_id: undefined,
id: index + 1, // autoincrement id
}));
const connectionEnvIdToDbId = {};
for (const conn of connections) {
connectionEnvIdToDbId[conn.id_original] = conn.id;
}
const connectionsRegex = /^ROLE_(.+)_CONNECTIONS$/;
const permissionsRegex = /^ROLE_(.+)_PERMISSIONS$/;
const dbConnectionRegex = /^ROLE_(.+)_DATABASES_(.+)_CONNECTION$/;
const dbDatabasesRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES$/;
const dbDatabasesRegexRegex = /^ROLE_(.+)_DATABASES_(.+)_DATABASES_REGEX$/;
const dbPermissionRegex = /^ROLE_(.+)_DATABASES_(.+)_PERMISSION$/;
const tableConnectionRegex = /^ROLE_(.+)_TABLES_(.+)_CONNECTION$/;
const tableDatabasesRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES$/;
const tableDatabasesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_DATABASES_REGEX$/;
const tableSchemasRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS$/;
const tableSchemasRegexRegex = /^ROLE_(.+)_TABLES_(.+)_SCHEMAS_REGEX$/;
const tableTablesRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES$/;
const tableTablesRegexRegex = /^ROLE_(.+)_TABLES_(.+)_TABLES_REGEX$/;
const tablePermissionRegex = /^ROLE_(.+)_TABLES_(.+)_PERMISSION$/;
const tableScopeRegex = /^ROLE_(.+)_TABLES_(.+)_SCOPE$/;
const roles = [];
const role_connections = [];
const role_permissions = [];
const role_databases = [];
const role_tables = [];
// Permission name to ID mappings
const databasePermissionMap = {
view: -1,
read_content: -2,
write_data: -3,
run_script: -4,
deny: -5,
};
const tablePermissionMap = {
read: -1,
update_only: -2,
create_update_delete: -3,
run_script: -4,
deny: -5,
};
const tableScopeMap = {
all_objects: -1,
tables: -2,
views: -3,
tables_views_collections: -4,
procedures: -5,
functions: -6,
triggers: -7,
sql_objects: -8,
collections: -9,
};
// Collect database and table permissions data
const databasePermissions = {};
const tablePermissions = {};
// First pass: collect all database and table permission data
for (const key in env) {
const dbConnMatch = key.match(dbConnectionRegex);
const dbDatabasesMatch = key.match(dbDatabasesRegex);
const dbDatabasesRegexMatch = key.match(dbDatabasesRegexRegex);
const dbPermMatch = key.match(dbPermissionRegex);
const tableConnMatch = key.match(tableConnectionRegex);
const tableDatabasesMatch = key.match(tableDatabasesRegex);
const tableDatabasesRegexMatch = key.match(tableDatabasesRegexRegex);
const tableSchemasMatch = key.match(tableSchemasRegex);
const tableSchemasRegexMatch = key.match(tableSchemasRegexRegex);
const tableTablesMatch = key.match(tableTablesRegex);
const tableTablesRegexMatch = key.match(tableTablesRegexRegex);
const tablePermMatch = key.match(tablePermissionRegex);
const tableScopeMatch = key.match(tableScopeRegex);
// Database permissions
if (dbConnMatch) {
const [, roleName, permId] = dbConnMatch;
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
databasePermissions[roleName][permId].connection = env[key];
}
if (dbDatabasesMatch) {
const [, roleName, permId] = dbDatabasesMatch;
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
databasePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n');
}
if (dbDatabasesRegexMatch) {
const [, roleName, permId] = dbDatabasesRegexMatch;
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
databasePermissions[roleName][permId].databasesRegex = env[key];
}
if (dbPermMatch) {
const [, roleName, permId] = dbPermMatch;
if (!databasePermissions[roleName]) databasePermissions[roleName] = {};
if (!databasePermissions[roleName][permId]) databasePermissions[roleName][permId] = {};
databasePermissions[roleName][permId].permission = env[key];
}
// Table permissions
if (tableConnMatch) {
const [, roleName, permId] = tableConnMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].connection = env[key];
}
if (tableDatabasesMatch) {
const [, roleName, permId] = tableDatabasesMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].databases = env[key]?.replace(/\|/g, '\n');
}
if (tableDatabasesRegexMatch) {
const [, roleName, permId] = tableDatabasesRegexMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].databasesRegex = env[key];
}
if (tableSchemasMatch) {
const [, roleName, permId] = tableSchemasMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].schemas = env[key];
}
if (tableSchemasRegexMatch) {
const [, roleName, permId] = tableSchemasRegexMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].schemasRegex = env[key];
}
if (tableTablesMatch) {
const [, roleName, permId] = tableTablesMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].tables = env[key]?.replace(/\|/g, '\n');
}
if (tableTablesRegexMatch) {
const [, roleName, permId] = tableTablesRegexMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].tablesRegex = env[key];
}
if (tablePermMatch) {
const [, roleName, permId] = tablePermMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].permission = env[key];
}
if (tableScopeMatch) {
const [, roleName, permId] = tableScopeMatch;
if (!tablePermissions[roleName]) tablePermissions[roleName] = {};
if (!tablePermissions[roleName][permId]) tablePermissions[roleName][permId] = {};
tablePermissions[roleName][permId].scope = env[key];
}
}
// Second pass: process roles, connections, and permissions
for (const key in env) {
const connMatch = key.match(connectionsRegex);
const permMatch = key.match(permissionsRegex);
if (connMatch) {
const roleName = connMatch[1];
let role = roles.find(r => r.name === roleName);
if (!role) {
role = {
id: roles.length + 1,
name: roleName,
import_source_id: -1,
};
roles.push(role);
}
const connIds = env[key]
.split(',')
.map(id => id.trim())
.filter(id => id.length > 0);
for (const connId of connIds) {
const dbId = connectionEnvIdToDbId[connId];
if (dbId) {
role_connections.push({
role_id: role.id,
connection_id: dbId,
import_source_id: -1,
});
}
}
}
if (permMatch) {
const roleName = permMatch[1];
let role = roles.find(r => r.name === roleName);
if (!role) {
role = {
id: roles.length + 1,
name: roleName,
import_source_id: -1,
};
roles.push(role);
}
const permissions = env[key]
.split(',')
.map(p => p.trim())
.filter(p => p.length > 0);
for (const permission of permissions) {
role_permissions.push({
role_id: role.id,
permission,
import_source_id: -1,
});
}
}
}
// Process database permissions
for (const roleName in databasePermissions) {
let role = roles.find(r => r.name === roleName);
if (!role) {
role = {
id: roles.length + 1,
name: roleName,
import_source_id: -1,
};
roles.push(role);
}
for (const permId in databasePermissions[roleName]) {
const perm = databasePermissions[roleName][permId];
if (perm.connection && perm.permission) {
const dbId = connectionEnvIdToDbId[perm.connection];
const permissionId = databasePermissionMap[perm.permission];
if (dbId && permissionId) {
role_databases.push({
role_id: role.id,
connection_id: dbId,
database_names_list: perm.databases || null,
database_names_regex: perm.databasesRegex || null,
database_permission_role_id: permissionId,
id_original: permId,
import_source_id: -1,
});
}
}
}
}
// Process table permissions
for (const roleName in tablePermissions) {
let role = roles.find(r => r.name === roleName);
if (!role) {
role = {
id: roles.length + 1,
name: roleName,
import_source_id: -1,
};
roles.push(role);
}
for (const permId in tablePermissions[roleName]) {
const perm = tablePermissions[roleName][permId];
if (perm.connection && perm.permission) {
const dbId = connectionEnvIdToDbId[perm.connection];
const permissionId = tablePermissionMap[perm.permission];
const scopeId = tableScopeMap[perm.scope || 'all_objects'];
if (dbId && permissionId && scopeId) {
role_tables.push({
role_id: role.id,
connection_id: dbId,
database_names_list: perm.databases || null,
database_names_regex: perm.databasesRegex || null,
schema_names_list: perm.schemas || null,
schema_names_regex: perm.schemasRegex || null,
table_names_list: perm.tables || null,
table_names_regex: perm.tablesRegex || null,
table_permission_role_id: permissionId,
table_permission_scope_id: scopeId,
id_original: permId,
import_source_id: -1,
});
}
}
}
}
if (connections.length == 0 && roles.length == 0) {
return null;
}
return {
connections,
roles,
role_connections,
role_permissions,
role_databases,
role_tables,
};
}
function createStorageFromEnvReplicatorItems(importEntities) {
return [
{
name: 'connections',
findExisting: true,
createNew: true,
updateExisting: true,
matchColumns: ['id_original', 'import_source_id'],
deleteMissing: true,
deleteRestrictionColumns: ['import_source_id'],
skipUpdateColumns: ['conid'],
jsonArray: importEntities.connections,
},
{
name: 'roles',
findExisting: true,
createNew: true,
updateExisting: true,
matchColumns: ['name', 'import_source_id'],
deleteMissing: true,
deleteRestrictionColumns: ['import_source_id'],
jsonArray: importEntities.roles,
},
{
name: 'role_connections',
findExisting: true,
createNew: true,
updateExisting: false,
deleteMissing: true,
matchColumns: ['role_id', 'connection_id', 'import_source_id'],
jsonArray: importEntities.role_connections,
deleteRestrictionColumns: ['import_source_id'],
},
{
name: 'role_permissions',
findExisting: true,
createNew: true,
updateExisting: false,
deleteMissing: true,
matchColumns: ['role_id', 'permission', 'import_source_id'],
jsonArray: importEntities.role_permissions,
deleteRestrictionColumns: ['import_source_id'],
},
{
name: 'role_databases',
findExisting: true,
createNew: true,
updateExisting: true,
deleteMissing: true,
matchColumns: ['role_id', 'id_original', 'import_source_id'],
jsonArray: importEntities.role_databases,
deleteRestrictionColumns: ['import_source_id'],
},
{
name: 'role_tables',
findExisting: true,
createNew: true,
updateExisting: true,
deleteMissing: true,
matchColumns: ['role_id', 'id_original', 'import_source_id'],
jsonArray: importEntities.role_tables,
deleteRestrictionColumns: ['import_source_id'],
},
];
}
module.exports = {
extractConnectionsFromEnv,
extractImportEntitiesFromEnv,
createStorageFromEnvReplicatorItems,
};
+7 -1
View File
@@ -23,6 +23,7 @@ export interface DataReplicatorItem {
deleteMissing: boolean;
deleteRestrictionColumns: string[];
matchColumns: string[];
skipUpdateColumns?: string[];
}
export interface DataReplicatorOptions {
@@ -151,7 +152,12 @@ class ReplicatorItemHolder {
chunk,
this.table.columns.map(x => x.columnName)
),
[this.autoColumn, ...this.backReferences.map(x => x.columnName), ...this.references.map(x => x.columnName)]
[
this.autoColumn,
...this.backReferences.map(x => x.columnName),
...this.references.map(x => x.columnName),
...(this.item.skipUpdateColumns || []),
]
);
return res;
+75 -9
View File
@@ -26,6 +26,7 @@ import _isDate from 'lodash/isDate';
import _isArray from 'lodash/isArray';
import _isPlainObject from 'lodash/isPlainObject';
import _keys from 'lodash/keys';
import _cloneDeep from 'lodash/cloneDeep';
import uuidv1 from 'uuid/v1';
export class SqlDumper implements AlterProcessor {
@@ -667,6 +668,68 @@ export class SqlDumper implements AlterProcessor {
}
}
sanitizeTableConstraints(table: TableInfo): TableInfo {
// Create a deep copy of the table
const sanitized = _cloneDeep(table);
// Get the set of existing column names
const existingColumns = new Set(sanitized.columns.map(col => col.columnName));
// Filter primary key columns to only include existing columns
if (sanitized.primaryKey) {
const validPkColumns = sanitized.primaryKey.columns.filter(col => existingColumns.has(col.columnName));
if (validPkColumns.length === 0) {
// If no valid columns remain, remove the primary key entirely
sanitized.primaryKey = null;
} else if (validPkColumns.length < sanitized.primaryKey.columns.length) {
// Update primary key with only valid columns
sanitized.primaryKey = {
...sanitized.primaryKey,
columns: validPkColumns
};
}
}
// Filter sorting key columns to only include existing columns
if (sanitized.sortingKey) {
const validSkColumns = sanitized.sortingKey.columns.filter(col => existingColumns.has(col.columnName));
if (validSkColumns.length === 0) {
sanitized.sortingKey = null;
} else if (validSkColumns.length < sanitized.sortingKey.columns.length) {
sanitized.sortingKey = {
...sanitized.sortingKey,
columns: validSkColumns
};
}
}
// Filter foreign keys to only include those with all columns present
if (sanitized.foreignKeys) {
sanitized.foreignKeys = sanitized.foreignKeys.filter(fk =>
fk.columns.every(col => existingColumns.has(col.columnName))
);
}
// Filter indexes to only include those with all columns present
if (sanitized.indexes) {
sanitized.indexes = sanitized.indexes.filter(idx =>
idx.columns.every(col => existingColumns.has(col.columnName))
);
}
// Filter unique constraints to only include those with all columns present
if (sanitized.uniques) {
sanitized.uniques = sanitized.uniques.filter(uq =>
uq.columns.every(col => existingColumns.has(col.columnName))
);
}
// Filter dependencies (references from other tables) - these should remain as-is
// since they don't affect the CREATE TABLE statement for this table
return sanitized;
}
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
if (!oldTable.pairingId || !newTable.pairingId || oldTable.pairingId != newTable.pairingId) {
throw new Error('Recreate is not possible: oldTable.paringId != newTable.paringId');
@@ -681,48 +744,51 @@ export class SqlDumper implements AlterProcessor {
}))
.filter(x => x.newcol);
// Create a sanitized version of newTable with constraints that only reference existing columns
const sanitizedNewTable = this.sanitizeTableConstraints(newTable);
if (this.driver.supportsTransactions) {
this.dropConstraints(oldTable, true);
this.renameTable(oldTable, tmpTable);
this.createTable(newTable);
this.createTable(sanitizedNewTable);
const autoinc = newTable.columns.find(x => x.autoIncrement);
const autoinc = sanitizedNewTable.columns.find(x => x.autoIncrement);
if (autoinc) {
this.allowIdentityInsert(newTable, true);
this.allowIdentityInsert(sanitizedNewTable, true);
}
this.putCmd(
'^insert ^into %f (%,i) select %,i ^from %f',
newTable,
sanitizedNewTable,
columnPairs.map(x => x.newcol.columnName),
columnPairs.map(x => x.oldcol.columnName),
{ ...oldTable, pureName: tmpTable }
);
if (autoinc) {
this.allowIdentityInsert(newTable, false);
this.allowIdentityInsert(sanitizedNewTable, false);
}
if (this.dialect.dropForeignKey) {
newTable.dependencies.forEach(cnt => this.createConstraint(cnt));
sanitizedNewTable.dependencies.forEach(cnt => this.createConstraint(cnt));
}
this.dropTable({ ...oldTable, pureName: tmpTable });
} else {
// we have to preserve old table as long as possible
this.createTable({ ...newTable, pureName: tmpTable });
this.createTable({ ...sanitizedNewTable, pureName: tmpTable });
this.putCmd(
'^insert ^into %f (%,i) select %,s ^from %f',
{ ...newTable, pureName: tmpTable },
{ ...sanitizedNewTable, pureName: tmpTable },
columnPairs.map(x => x.newcol.columnName),
columnPairs.map(x => x.oldcol.columnName),
oldTable
);
this.dropTable(oldTable);
this.renameTable({ ...newTable, pureName: tmpTable }, newTable.pureName);
this.renameTable({ ...sanitizedNewTable, pureName: tmpTable }, newTable.pureName);
}
}
+121 -45
View File
@@ -91,8 +91,8 @@ interface AlterOperation_RenameConstraint {
}
interface AlterOperation_RecreateTable {
operationType: 'recreateTable';
table: TableInfo;
operations: AlterOperation[];
oldTable: TableInfo;
newTable: TableInfo;
}
interface AlterOperation_FillPreloadedRows {
operationType: 'fillPreloadedRows';
@@ -249,11 +249,11 @@ export class AlterPlan {
});
}
recreateTable(table: TableInfo, operations: AlterOperation[]) {
recreateTable(oldTable: TableInfo, newTable: TableInfo) {
this.operations.push({
operationType: 'recreateTable',
table,
operations,
oldTable,
newTable,
});
this.recreates.tables += 1;
}
@@ -337,7 +337,13 @@ export class AlterPlan {
return opRes;
}),
op,
];
].filter(op => {
// filter duplicated drops
const existingDrop = this.operations.find(
o => o.operationType == 'dropConstraint' && o.oldObject === op['oldObject']
);
return existingDrop == null;
});
return res;
}
@@ -498,53 +504,121 @@ export class AlterPlan {
return [];
}
const table = this.wholeNewDb.tables.find(
const oldTable = this.wholeOldDb.tables.find(
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
);
const newTable = this.wholeNewDb.tables.find(
x => x.pureName == op[objectField].pureName && x.schemaName == op[objectField].schemaName
);
this.recreates.tables += 1;
return [
{
operationType: 'recreateTable',
table,
operations: [op],
oldTable,
newTable,
// operations: [op],
},
];
}
return null;
}
_groupTableRecreations(): AlterOperation[] {
const res = [];
const recreates = {};
_removeRecreatedTableAlters(): AlterOperation[] {
const res: AlterOperation[] = [];
const recreates = new Set<string>();
for (const op of this.operations) {
if (op.operationType == 'recreateTable' && op.table) {
const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
if (existingRecreate) {
existingRecreate.operations.push(...op.operations);
} else {
const recreate = {
...op,
operations: [...op.operations],
};
res.push(recreate);
recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
}
} else {
// @ts-ignore
const oldObject: TableInfo = op.oldObject || op.object;
if (oldObject) {
const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
if (recreated) {
recreated.operations.push(op);
continue;
}
}
res.push(op);
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
recreates.add(key);
}
}
for (const op of this.operations) {
switch (op.operationType) {
case 'createColumn':
case 'createConstraint':
{
const key = `${op.newObject.schemaName}||${op.newObject.pureName}`;
if (recreates.has(key)) {
// skip create inside recreated table
continue;
}
}
break;
case 'dropColumn':
case 'dropConstraint':
case 'changeColumn':
{
const key = `${op.oldObject.schemaName}||${op.oldObject.pureName}`;
if (recreates.has(key)) {
// skip drop/change inside recreated table
continue;
}
}
break;
case 'renameColumn':
{
const key = `${op.object.schemaName}||${op.object.pureName}`;
if (recreates.has(key)) {
// skip rename inside recreated table
continue;
}
}
break;
}
res.push(op);
}
return res;
}
_groupTableRecreations(): AlterOperation[] {
const res = [];
const recreates = new Set<string>();
for (const op of this.operations) {
if (op.operationType == 'recreateTable' && op.oldTable && op.newTable) {
const key = `${op.oldTable.schemaName}||${op.oldTable.pureName}`;
if (recreates.has(key)) {
// prevent duplicate recreates
continue;
}
recreates.add(key);
}
res.push(op);
}
return res;
// const res = [];
// const recreates = {};
// for (const op of this.operations) {
// if (op.operationType == 'recreateTable' && op.table) {
// const existingRecreate = recreates[`${op.table.schemaName}||${op.table.pureName}`];
// if (existingRecreate) {
// existingRecreate.operations.push(...op.operations);
// } else {
// const recreate = {
// ...op,
// operations: [...op.operations],
// };
// res.push(recreate);
// recreates[`${op.table.schemaName}||${op.table.pureName}`] = recreate;
// }
// } else {
// // @ts-ignore
// const oldObject: TableInfo = op.oldObject || op.object;
// if (oldObject) {
// const recreated = recreates[`${oldObject.schemaName}||${oldObject.pureName}`];
// if (recreated) {
// recreated.operations.push(op);
// continue;
// }
// }
// res.push(op);
// }
// }
// return res;
}
_moveForeignKeysToLast(): AlterOperation[] {
if (!this.dialect.createForeignKey) {
return this.operations;
@@ -611,6 +685,8 @@ export class AlterPlan {
// console.log('*****************OPERATIONS3', this.operations);
this.operations = this._removeRecreatedTableAlters();
this.operations = this._moveForeignKeysToLast();
// console.log('*****************OPERATIONS4', this.operations);
@@ -673,16 +749,16 @@ export function runAlterOperation(op: AlterOperation, processor: AlterProcessor)
break;
case 'recreateTable':
{
const oldTable = generateTablePairingId(op.table);
const newTable = _.cloneDeep(oldTable);
const newDb = DatabaseAnalyser.createEmptyStructure();
newDb.tables.push(newTable);
// console.log('////////////////////////////newTable1', newTable);
op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
// console.log('////////////////////////////op.operations', op.operations);
// console.log('////////////////////////////op.table', op.table);
// console.log('////////////////////////////newTable2', newTable);
processor.recreateTable(oldTable, newTable);
// const oldTable = generateTablePairingId(op.table);
// const newTable = _.cloneDeep(oldTable);
// const newDb = DatabaseAnalyser.createEmptyStructure();
// newDb.tables.push(newTable);
// // console.log('////////////////////////////newTable1', newTable);
// op.operations.forEach(child => runAlterOperation(child, new DatabaseInfoAlterProcessor(newDb)));
// // console.log('////////////////////////////op.operations', op.operations);
// // console.log('////////////////////////////op.table', op.table);
// // console.log('////////////////////////////newTable2', newTable);
processor.recreateTable(op.oldTable, op.newTable);
}
break;
}
@@ -7,6 +7,7 @@
export let onSetPermission;
export let label;
export let folder;
export let disabled = false;
</script>
<PermissionCheckBox
@@ -15,6 +16,7 @@
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
{disabled}
/>
<div class="ml-4">
@@ -24,6 +26,7 @@
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
{disabled}
/>
<PermissionCheckBox
label="Write"
@@ -31,5 +34,6 @@
permissions={$values.permissions}
basePermissions={$values.basePermissions}
{onSetPermission}
{disabled}
/>
</div>
@@ -467,14 +467,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
{ divider: true },
isSqlOrDoc &&
isProApp() &&
!connection.isReadOnly &&
hasPermission(`dbops/import`) && {
onClick: handleImport,
text: _t('database.import', { defaultMessage: 'Import' }),
},
isSqlOrDoc &&
isProApp() &&
hasPermission(`dbops/export`) && {
onClick: handleExport,
text: _t('database.export', { defaultMessage: 'Export' }),
@@ -0,0 +1,398 @@
<script lang="ts">
import _ from 'lodash';
import { tick } from 'svelte';
import CellValue from '../datagrid/CellValue.svelte';
import { isJsonLikeLongString, safeJsonParse, parseCellValue, stringifyCellValue, filterName } from 'dbgate-tools';
import keycodes from '../utility/keycodes';
import createRef from '../utility/createRef';
import { showModal } from '../modals/modalTools';
import EditCellDataModal from '../modals/EditCellDataModal.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import CloseSearchButton from '../buttons/CloseSearchButton.svelte';
import { _t } from '../translations';
import ColumnLabel from '../elements/ColumnLabel.svelte';
import CheckboxField from '../forms/CheckboxField.svelte';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import JSONTree from '../jsontree/JSONTree.svelte';
import Link from '../elements/Link.svelte';
export let selection;
$: firstSelection = selection?.[0];
$: rowData = firstSelection?.rowData;
$: editable = firstSelection?.editable;
$: editorTypes = firstSelection?.editorTypes;
$: displayColumns = firstSelection?.displayColumns || [];
$: realColumnUniqueNames = firstSelection?.realColumnUniqueNames || [];
$: grider = firstSelection?.grider;
$: uniqueRows = _.uniqBy(selection || [], 'row');
$: isMultipleRows = uniqueRows.length > 1;
function areValuesEqual(val1, val2) {
if (val1 === val2) return true;
if (val1 == null && val2 == null) return true;
if (val1 == null || val2 == null) return false;
return _.isEqual(val1, val2);
}
function getFieldValue(colName) {
if (!isMultipleRows) return { value: rowData?.[colName], hasMultipleValues: false };
const values = uniqueRows.map(sel => sel.rowData?.[colName]);
const firstValue = values[0];
const allSame = values.every(v => areValuesEqual(v, firstValue));
return allSame ? { value: firstValue, hasMultipleValues: false } : { value: null, hasMultipleValues: true };
}
let filter = '';
let notNull = getLocalStorage('dataGridCellDataFormNotNull') === 'true';
$: orderedFields = realColumnUniqueNames
.map(colName => {
const col = displayColumns.find(c => c.uniqueName === colName);
if (!col) return null;
const { value, hasMultipleValues } = getFieldValue(colName);
return {
...col,
value,
hasMultipleValues,
// columnName: col.columnName || colName,
// uniqueName: colName,
// value,
// hasMultipleValues,
// col,
};
})
.filter(Boolean);
$: filteredFields = orderedFields
.filter(field => filterName(filter, field.columnName))
.filter(field => {
if (notNull) {
return field.value != null || field.hasMultipleValues;
}
return true;
});
let editingColumn = null;
let editValue = '';
let domEditor = null;
const isChangedRef = createRef(false);
function isJsonValue(value) {
if (
_.isPlainObject(value) &&
!(value?.type == 'Buffer' && _.isArray(value.data)) &&
!value.$oid &&
!value.$bigint &&
!value.$decimal
) {
return true;
}
if (_.isArray(value)) return true;
if (typeof value !== 'string') return false;
if (!isJsonLikeLongString(value)) return false;
const parsed = safeJsonParse(value);
return parsed !== null && (_.isPlainObject(parsed) || _.isArray(parsed));
}
function getJsonObject(value) {
if (_.isPlainObject(value) || _.isArray(value)) return value;
if (typeof value === 'string') return safeJsonParse(value);
return null;
}
function handleClick(field) {
if (!editable || !grider) return;
if (isJsonValue(field.value)) return;
// if (isJsonValue(field.value) && !field.hasMultipleValues) {
// openEditModal(field);
// return;
// }
startEditing(field);
}
function handleDoubleClick(field) {
if (!editable || !grider) return;
if (isJsonValue(field.value) && !field.hasMultipleValues) {
openEditModal(field);
return;
}
startEditing(field);
}
function startEditing(field) {
if (!editable || !grider) return;
editingColumn = field.uniqueName;
editValue = field.hasMultipleValues ? '' : stringifyCellValue(field.value, 'inlineEditorIntent', editorTypes).value;
isChangedRef.set(false);
tick().then(() => {
if (!domEditor) return;
domEditor.focus();
if (!field.hasMultipleValues) domEditor.select();
});
}
function handleKeyDown(event, field) {
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.set(false);
editingColumn = null;
break;
case keycodes.enter:
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
event.preventDefault();
break;
case keycodes.tab:
case keycodes.upArrow:
case keycodes.downArrow:
const reverse = event.keyCode === keycodes.upArrow || (event.keyCode === keycodes.tab && event.shiftKey);
event.preventDefault();
moveToNextField(field, reverse);
break;
}
}
function moveToNextField(field, reverse) {
const currentIndex = filteredFields.findIndex(f => f.uniqueName === field.uniqueName);
const nextIndex = reverse ? currentIndex - 1 : currentIndex + 1;
const nextField = filteredFields[nextIndex];
if (!nextField) return;
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
if (nextIndex < 0 || nextIndex >= filteredFields.length) return;
tick().then(() => {
startEditing(nextField);
// if (isJsonValue(nextField.value)) {
// openEditModal(nextField);
// } else {
// startEditing(nextField);
// }
});
}
function handleSearchKeyDown(e) {
if (e.keyCode === keycodes.backspace && (e.metaKey || e.ctrlKey)) {
filter = '';
e.stopPropagation();
e.preventDefault();
}
}
function handleBlur(field) {
if (isChangedRef.get()) {
saveValue(field);
}
editingColumn = null;
}
function setCellValue(fieldName, value) {
if (!grider) return;
if (selection.length > 0) {
const uniqueRowIndices = _.uniq(selection.map(x => x.row));
grider.beginUpdate();
for (const row of uniqueRowIndices) {
grider.setCellValue(row, fieldName, value);
}
grider.endUpdate();
}
}
function saveValue(field) {
if (!grider) return;
const parsedValue = parseCellValue(editValue, editorTypes);
setCellValue(field.uniqueName, parsedValue);
isChangedRef.set(false);
}
function openEditModal(field) {
if (!grider) return;
showModal(EditCellDataModal, {
value: field.value,
dataEditorTypesBehaviour: editorTypes,
onSave: value => setCellValue(field.uniqueName, value),
});
}
function getJsonParsedValue(value) {
if (editorTypes?.explicitDataType) return null;
if (!isJsonLikeLongString(value)) return null;
return safeJsonParse(value);
}
function handleEdit(field) {
editingColumn = null;
openEditModal(field);
}
</script>
<div class="outer">
<div class="content">
{#if rowData}
<div class="search-wrapper" on:keydown={handleSearchKeyDown}>
<SearchBoxWrapper noMargin>
<SearchInput
placeholder={_t('tableCell.filterColumns', { defaultMessage: 'Filter columns' })}
bind:value={filter}
/>
<CloseSearchButton bind:filter />
</SearchBoxWrapper>
<CheckboxField
defaultChecked={notNull}
on:change={e => {
// @ts-ignore
notNull = e.target.checked;
setLocalStorage('dataGridCellDataFormNotNull', notNull ? 'true' : 'false');
}}
/>
{_t('tableCell.hideNullValues', { defaultMessage: 'Hide NULL values' })}
</div>
{/if}
<div class="inner">
{#if !rowData}
<div class="no-data">{_t('tableCell.noDataSelected', { defaultMessage: 'No data selected' })}</div>
{:else}
{#each filteredFields as field (field.uniqueName)}
<div class="field">
<div class="field-name">
<ColumnLabel {...field} showDataType /><Link onClick={() => handleEdit(field)}
>{_t('tableCell.edit', { defaultMessage: 'Edit' })}
</Link>
</div>
<div class="field-value" class:editable on:click={() => handleClick(field)}>
{#if editingColumn === field.uniqueName}
<div class="editor-wrapper">
<input
type="text"
bind:this={domEditor}
bind:value={editValue}
on:input={() => isChangedRef.set(true)}
on:keydown={e => handleKeyDown(e, field)}
on:blur={() => handleBlur(field)}
class="inline-editor"
/>
</div>
{:else if field.hasMultipleValues}
<span class="multiple-values"
>({_t('tableCell.multipleValues', { defaultMessage: 'Multiple values' })})</span
>
{:else if isJsonValue(field.value)}
<JSONTree value={getJsonParsedValue(field.value)} />
{:else}
<CellValue
{rowData}
value={field.value}
jsonParsedValue={getJsonParsedValue(field.value)}
{editorTypes}
/>
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
</div>
<style>
.outer {
flex: 1;
position: relative;
}
.content {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: column;
}
.search-wrapper {
padding: 4px 4px 0 4px;
flex-shrink: 0;
border: 1px solid var(--theme-border);
border-bottom: none;
}
.inner {
overflow: auto;
flex: 1;
padding: 4px;
}
.no-data {
color: var(--theme-font-3);
font-style: italic;
padding: 8px;
}
.field {
margin-bottom: 8px;
border: 1px solid var(--theme-border);
border-radius: 3px;
overflow: hidden;
}
.field-name {
background: var(--theme-bg-1);
padding: 4px 8px;
font-weight: 500;
font-size: 11px;
color: var(--theme-font-2);
border-bottom: 1px solid var(--theme-border);
display: flex;
justify-content: space-between;
}
.field-value {
padding: 6px 8px;
background: var(--theme-bg-0);
min-height: 20px;
word-break: break-all;
position: relative;
}
.field-value.editable {
cursor: text;
}
.editor-wrapper {
display: flex;
align-items: center;
}
.inline-editor {
flex: 1;
border: none;
outline: none;
background: var(--theme-bg-0);
color: var(--theme-font-1);
padding: 0;
margin: 0;
font-family: inherit;
font-size: inherit;
}
.inline-editor:focus {
outline: none;
}
.multiple-values {
color: var(--theme-font-3);
font-style: italic;
}
</style>
+11 -1
View File
@@ -3,12 +3,21 @@
export let selection;
export let wrap;
$: singleSelection = selection?.length == 1 && selection?.[0];
$: grider = singleSelection?.grider;
$: editable = grider?.editable ?? false;
function setCellValue(value) {
if (!editable) return;
grider.setCellValue(singleSelection.row, singleSelection.column, value);
}
</script>
<textarea
class="flex1"
{wrap}
readonly
readonly={!editable}
value={selection
.map(cell => {
const { value } = cell;
@@ -16,4 +25,5 @@
return cell.value;
})
.join('\n')}
on:input={e => setCellValue(e.target['value'])}
/>
+1 -1
View File
@@ -703,7 +703,7 @@ registerCommand({
name: __t('command.database.export', { defaultMessage: 'Export database' }),
toolbar: true,
icon: 'icon export',
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`) && isProApp(),
testEnabled: () => getCurrentDatabase() != null && hasPermission(`dbops/export`),
onClick: () => {
openImportExportTab({
targetStorageType: getDefaultFileFormat(getExtensions()).storageType,
@@ -25,6 +25,7 @@
export let setFilter;
export let showResizeSplitter = false;
export let onFocusGrid = null;
export let onFocusGridHeader = null;
export let onGetReference = null;
export let foreignKey = null;
export let conid = null;
@@ -204,6 +205,11 @@
// ev.stopPropagation();
ev.preventDefault();
}
if (ev.keyCode == keycodes.upArrow) {
if (onFocusGridHeader) onFocusGridHeader();
// ev.stopPropagation();
ev.preventDefault();
}
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
+72 -25
View File
@@ -24,7 +24,7 @@
registerCommand({
id: 'dataGrid.switchToTable',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.witchToTable', { defaultMessage: 'Switch to table'}),
name: __t('command.datagrid.witchToTable', { defaultMessage: 'Switch to table' }),
icon: 'icon table',
keyText: 'F4',
testEnabled: () => getCurrentEditor()?.switchViewEnabled('table'),
@@ -40,6 +40,17 @@
onClick: () => getCurrentEditor().toggleLeftPanel(),
});
registerCommand({
id: 'dataGrid.toggleCellDataView',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.toggleCellDataView', { defaultMessage: 'Toggle cell data view' }),
toolbarName: __t('command.datagrid.toggleCellDataView.toolbar', { defaultMessage: 'Cell Data' }),
menuName: __t('command.datagrid.toggleCellDataView.menu', { defaultMessage: 'Show cell data' }),
icon: 'icon cell-data',
testEnabled: () => !!getCurrentEditor(),
onClick: () => getCurrentEditor().toggleCellDataView(),
});
function extractMacroValuesForMacro(macroValues, macro) {
// return {};
if (!macro) return {};
@@ -70,6 +81,7 @@
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import { __t, _t } from '../translations';
import { isProApp } from '../utility/proTools';
import CellDataWidget from '../widgets/CellDataWidget.svelte';
export let config;
export let setConfig;
@@ -91,6 +103,7 @@
export let hasMultiColumnFilter = false;
export let setLoadedRows = null;
export let hideGridLeftColumn = false;
export let cellDataViewVisible = false;
export let onPublishedCellsChanged;
@@ -107,6 +120,7 @@
setContext('macroValues', macroValues);
let managerSize;
let cellViewWidth;
const collapsedLeftColumnStore =
getContext('collapsedLeftColumnStore') || writable(getLocalStorage('dataGrid_collapsedLeftColumn', false));
@@ -149,6 +163,10 @@
collapsedLeftColumnStore.update(x => !x);
}
export function toggleCellDataView() {
cellDataViewVisible = !cellDataViewVisible;
}
registerMenu(
{ command: 'dataGrid.switchToForm', tag: 'switch', hideDisabled: true },
{ command: 'dataGrid.switchToTable', tag: 'switch', hideDisabled: true },
@@ -157,6 +175,7 @@
);
$: if (managerSize) setLocalStorage('dataGridManagerWidth', managerSize);
$: if (cellViewWidth) setLocalStorage('dataGridCellViewWidth', cellViewWidth);
function getInitialManagerSize() {
const width = getLocalStorage('dataGridManagerWidth');
@@ -165,6 +184,14 @@
}
return '300px';
}
function getInitialCellViewWidth() {
const width = getLocalStorage('dataGridCellViewWidth');
if (_.isNumber(width) && width > 30 && width < 500) {
return width;
}
return 300;
}
</script>
<HorizontalSplitter
@@ -219,6 +246,7 @@
skip={!(showMacros && isProApp())}
collapsed={!expandMacros}
data-testid="DataGrid_itemMacros"
height="20%"
>
<MacroManager {...$$props} {managerSize} />
</WidgetColumnBarItem>
@@ -227,30 +255,49 @@
<svelte:fragment slot="2">
<VerticalSplitter initialValue="70%" isSplitter={!!$selectedMacro && !isFormView && showMacros}>
<svelte:fragment slot="1">
{#if isFormView}
<svelte:component this={formViewComponent} {...$$props} />
{:else if isJsonView}
<svelte:component this={jsonViewComponent} {...$$props} {setLoadedRows} />
{:else}
<svelte:component
this={gridCoreComponent}
{...$$props}
{collapsedLeftColumnStore}
formViewAvailable={!!formViewComponent}
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
macroPreview={$selectedMacro}
{setLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
}}
onChangeSelectedColumns={cols => {
if (domColumnManager) domColumnManager.setSelectedColumns(cols);
}}
/>
{/if}
<HorizontalSplitter
initialSizeRight={getInitialCellViewWidth()}
onChangeSize={value => (cellViewWidth = value)}
isSplitter={cellDataViewVisible && !isFormView}
>
<svelte:fragment slot="1">
{#if isFormView}
<svelte:component this={formViewComponent} {...$$props} />
{:else if isJsonView}
<svelte:component this={jsonViewComponent} {...$$props} {setLoadedRows} />
{:else}
<svelte:component
this={gridCoreComponent}
{...$$props}
{collapsedLeftColumnStore}
formViewAvailable={!!formViewComponent}
macroValues={extractMacroValuesForMacro($macroValues, $selectedMacro)}
macroPreview={$selectedMacro}
{setLoadedRows}
onPublishedCellsChanged={value => {
publishedCells = value;
if (onPublishedCellsChanged) {
onPublishedCellsChanged(value);
}
if (value[0]?.isSelectedFullRow && !isFormView) {
cellDataViewVisible = true;
}
}}
onChangeSelectedColumns={cols => {
if (domColumnManager) domColumnManager.setSelectedColumns(cols);
}}
/>
{/if}
</svelte:fragment>
<svelte:fragment slot="2">
<CellDataWidget
onClose={() => {
cellDataViewVisible = false;
}}
selection={publishedCells}
/>
</svelte:fragment>
</HorizontalSplitter>
</svelte:fragment>
<svelte:fragment slot="2">
@@ -57,7 +57,11 @@
$: style = computeStyle(maxWidth, col);
$: isJson =
_.isPlainObject(value) && !(value?.type == 'Buffer' && _.isArray(value.data)) && !value.$oid && !value.$bigint && !value.$decimal;
_.isPlainObject(value) &&
!(value?.type == 'Buffer' && _.isArray(value.data)) &&
!value.$oid &&
!value.$bigint &&
!value.$decimal;
// don't parse JSON for explicit data types
$: jsonParsedValue = !editorTypes?.explicitDataType && isJsonLikeLongString(value) ? safeJsonParse(value) : null;
@@ -80,7 +84,7 @@
class:isFocusedColumn
class:hasOverlayValue
class:isMissingOverlayField
class:alignRight={ (_.isNumber(value) || isTypeNumber(col.dataType)) && !showHint}
class:alignRight={(_.isNumber(value) || isTypeNumber(col.dataType)) && !showHint && !isModifiedCell}
{style}
>
{#if hasOverlayValue}
+133 -35
View File
@@ -217,7 +217,7 @@
registerCommand({
id: 'dataGrid.filterSelected',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.filterSelected', { defaultMessage : 'Filter selected value'}),
name: __t('command.datagrid.filterSelected', { defaultMessage: 'Filter selected value' }),
keyText: 'CtrlOrCommand+Shift+F',
testEnabled: () => getCurrentDataGrid()?.getDisplay().filterable,
onClick: () => getCurrentDataGrid().filterSelectedValue(),
@@ -225,7 +225,7 @@
registerCommand({
id: 'dataGrid.findColumn',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.findColumn', { defaultMessage: 'Find column'}),
name: __t('command.datagrid.findColumn', { defaultMessage: 'Find column' }),
keyText: 'CtrlOrCommand+F',
testEnabled: () => getCurrentDataGrid() != null,
getSubCommands: () => getCurrentDataGrid().buildFindMenu(),
@@ -241,7 +241,7 @@
registerCommand({
id: 'dataGrid.clearFilter',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.clearFilter', { defaultMessage : 'Clear filter'}),
name: __t('command.datagrid.clearFilter', { defaultMessage: 'Clear filter' }),
keyText: 'CtrlOrCommand+Shift+E',
testEnabled: () => getCurrentDataGrid()?.clearFilterEnabled(),
onClick: () => getCurrentDataGrid().clearFilter(),
@@ -249,7 +249,7 @@
registerCommand({
id: 'dataGrid.generateSqlFromData',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.generateSql', { defaultMessage: 'Generate SQL'}),
name: __t('command.datagrid.generateSql', { defaultMessage: 'Generate SQL' }),
keyText: 'CtrlOrCommand+G',
testEnabled: () => getCurrentDataGrid()?.generateSqlFromDataEnabled(),
onClick: () => getCurrentDataGrid().generateSqlFromData(),
@@ -257,14 +257,14 @@
registerCommand({
id: 'dataGrid.openFreeTable',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.editSelection', { defaultMessage: 'Edit selection as table'}),
name: __t('command.datagrid.editSelection', { defaultMessage: 'Edit selection as table' }),
testEnabled: () => getCurrentDataGrid() != null,
onClick: () => getCurrentDataGrid().openFreeTable(),
});
registerCommand({
id: 'dataGrid.newJson',
category: __t('command.datagrid', { defaultMessage: 'Data grid' }),
name: __t('command.datagrid.addJsonDocument', { defaultMessage: 'Add JSON document'}),
name: __t('command.datagrid.addJsonDocument', { defaultMessage: 'Add JSON document' }),
testEnabled: () => getCurrentDataGrid()?.addJsonDocumentEnabled(),
onClick: () => getCurrentDataGrid().addJsonDocument(),
});
@@ -354,7 +354,7 @@
</script>
<script lang="ts">
import { GridDisplay } from 'dbgate-datalib';
import { GridDisplay, MacroDefinition } from 'dbgate-datalib';
import {
driverBase,
parseCellValue,
@@ -364,6 +364,7 @@
base64ToHex,
} from 'dbgate-tools';
import { getContext, onDestroy } from 'svelte';
import { type Writable } from 'svelte/store';
import _, { map } from 'lodash';
import registerCommand from '../commands/registerCommand';
import ColumnHeaderControl from './ColumnHeaderControl.svelte';
@@ -380,7 +381,17 @@
filterCellsForRow,
} from './gridutil';
import HorizontalScrollBar from './HorizontalScrollBar.svelte';
import { cellFromEvent, emptyCellArray, getCellRange, isRegularCell, nullCell, topLeftCell } from './selection';
import {
cellFromEvent,
emptyCellArray,
getCellRange,
isColumnHeaderCell,
isRegularCell,
isRowHeaderCell,
isTableHeaderCell,
nullCell,
topLeftCell,
} from './selection';
import VerticalScrollBar from './VerticalScrollBar.svelte';
import LoadingInfo from '../elements/LoadingInfo.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
@@ -388,7 +399,7 @@
import DataFilterControl from './DataFilterControl.svelte';
import createReducer from '../utility/createReducer';
import keycodes from '../utility/keycodes';
import { copyRowsFormat, currentArchive, selectedCellsCallback } from '../stores';
import { copyRowsFormat, currentArchive } from '../stores';
import {
copyRowsFormatDefs,
copyRowsToClipboard,
@@ -426,6 +437,7 @@
import { isProApp } from '../utility/proTools';
import SaveArchiveModal from '../modals/SaveArchiveModal.svelte';
import hasPermission from '../utility/hasPermission';
import macros from '../macro/macros';
export let onLoadNextData = undefined;
export let grider = undefined;
@@ -465,6 +477,7 @@
export let overlayDefinition = null;
export let onGetSelectionMenu = null;
export let onOpenChart = null;
export let macroCondition = null;
export const activator = createActivator('DataGridCore', false);
@@ -496,6 +509,7 @@
let selectionMenu = null;
const tabid = getContext('tabid');
const selectedMacro = getContext('selectedMacro') as Writable<MacroDefinition>;
let unsubscribeDbRefresh;
@@ -759,7 +773,7 @@
export function saveCellToFileEnabled() {
const value = getSelectedExportableCell();
return _.isString(value) || (value?.type == 'Buffer' && _.isArray(value?.data)) || (value?.$binary?.base64);
return _.isString(value) || (value?.type == 'Buffer' && _.isArray(value?.data)) || value?.$binary?.base64;
}
export async function saveCellToFile() {
@@ -1203,7 +1217,6 @@
if (rowIndexes.every(x => grider.getRowData(x))) {
lastPublishledSelectedCellsRef.set(stringified);
changeSetValueRef.set($changeSetStore?.value);
$selectedCellsCallback = () => getCellsPublished(selectedCells);
if (onChangeSelectedColumns) {
onChangeSelectedColumns(getSelectedColumns().map(x => x.columnName));
@@ -1244,30 +1257,59 @@
function getCellsPublished(cells) {
const regular = cellsToRegularCells(cells);
const commonInfo = {
engine: display?.driver,
editable: grider.editable,
editorTypes: display?.driver?.dataEditorTypesBehaviour,
displayColumns: columns,
realColumnUniqueNames,
grider,
};
const rowIndexes = _.sortBy(_.uniq(regular.map(x => x[0])));
const fullRowIndexes = new Set(cells.filter(x => x[1] == 'header').map(x => x[0]));
const rowInfos = rowIndexes.map(row => {
const rowData = grider.getRowData(row);
return {
row,
rowData,
condition: display?.getChangeSetCondition(rowData),
insertedRowIndex: grider?.getInsertedRowIndex(row),
rowStatus: grider.getRowStatus(row),
isSelectedFullRow: fullRowIndexes.has(row),
};
});
const rowInfoByIndex = _.zipObject(
rowIndexes.map(x => x.toString()),
rowInfos
);
const res = regular
.map(cell => {
const row = cell[0];
const rowData = grider.getRowData(row);
const column = realColumnUniqueNames[cell[1]];
const rowData = rowInfoByIndex[row].rowData;
return {
row,
rowData,
...commonInfo,
...rowInfoByIndex[row],
column,
value: rowData && rowData[column],
engine: display?.driver,
condition: display?.getChangeSetCondition(rowData),
insertedRowIndex: grider?.getInsertedRowIndex(row),
rowStatus: grider.getRowStatus(row),
onSetValue: value => grider.setCellValue(row, column, value),
};
})
.filter(x => x.column);
return res;
}
function scrollIntoView(cell) {
const [row, col] = cell;
if (row != null) {
if (_.isNumber(row)) {
let newRow = null;
const rowCount = grider.rowCount;
if (rowCount == 0) return;
@@ -1285,7 +1327,7 @@
}
}
if (col != null) {
if (_.isNumber(col)) {
if (col >= columnSizes.frozenCount) {
let newColumn = columnSizes.scrollInView(
firstVisibleColumnScrollIndex,
@@ -1515,7 +1557,11 @@
}
if (event.shiftKey) {
if (!isRegularCell(shiftDragStartCell)) {
if (
!isRegularCell(shiftDragStartCell) &&
!isColumnHeaderCell(shiftDragStartCell) &&
!isRowHeaderCell(shiftDragStartCell)
) {
shiftDragStartCell = currentCell;
}
} else {
@@ -1543,7 +1589,13 @@
}
function handleCursorMove(event) {
if (!isRegularCell(currentCell)) return null;
if (
!isRegularCell(currentCell) &&
!isColumnHeaderCell(currentCell) &&
!isRowHeaderCell(currentCell) &&
!isTableHeaderCell(currentCell)
)
return null;
let rowCount = grider.rowCount;
if (isCtrlOrCommandKey(event)) {
switch (event.keyCode) {
@@ -1570,24 +1622,36 @@
switch (event.keyCode) {
case keycodes.upArrow:
if (currentCell[0] == 0) return focusFilterEditor(currentCell[1]);
return moveCurrentCell(currentCell[0] - 1, currentCell[1], event);
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] - 1, currentCell[1], event) : null;
case keycodes.downArrow:
return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
if (currentCell[0] == 'header') return focusFilterEditor(currentCell[1]);
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] + 1, currentCell[1], event) : null;
case keycodes.enter:
if (!grider.editable) return moveCurrentCell(currentCell[0] + 1, currentCell[1], event);
if (!grider.editable)
return _.isNumber(currentCell[0]) ? moveCurrentCell(currentCell[0] + 1, currentCell[1], event) : null;
break;
case keycodes.leftArrow:
return moveCurrentCell(currentCell[0], currentCell[1] - 1, event);
return _.isNumber(currentCell[1])
? moveCurrentCell(currentCell[0], currentCell[1] == 0 ? 'header' : currentCell[1] - 1, event)
: null;
case keycodes.rightArrow:
return moveCurrentCell(currentCell[0], currentCell[1] + 1, event);
return currentCell[1] == 'header'
? moveCurrentCell(currentCell[0], 0, event)
: _.isNumber(currentCell[1])
? moveCurrentCell(currentCell[0], currentCell[1] + 1, event)
: null;
case keycodes.home:
return moveCurrentCell(currentCell[0], 0, event);
case keycodes.end:
return moveCurrentCell(currentCell[0], columnSizes.realCount - 1, event);
case keycodes.pageUp:
return moveCurrentCell(currentCell[0] - visibleRowCountLowerBound, currentCell[1], event);
return _.isNumber(currentCell[0])
? moveCurrentCell(currentCell[0] - visibleRowCountLowerBound, currentCell[1], event)
: null;
case keycodes.pageDown:
return moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event);
return _.isNumber(currentCell[0])
? moveCurrentCell(currentCell[0] + visibleRowCountLowerBound, currentCell[1], event)
: null;
case keycodes.tab: {
return moveCurrentCellWithTabKey(event.shiftKey);
}
@@ -1621,10 +1685,14 @@
function moveCurrentCell(row, col, event = null) {
const rowCount = grider.rowCount;
if (row < 0) row = 0;
if (row >= rowCount) row = rowCount - 1;
if (col < 0) col = 0;
if (col >= columnSizes.realCount) col = columnSizes.realCount - 1;
if (_.isNumber(row)) {
if (row < 0) row = 0;
if (row >= rowCount) row = rowCount - 1;
}
if (_.isNumber(col)) {
if (col < 0) col = 0;
if (col >= columnSizes.realCount) col = columnSizes.realCount - 1;
}
currentCell = [row, col];
// setSelectedCells([...(event.ctrlKey ? selectedCells : []), [row, col]]);
selectedCells = [[row, col]];
@@ -1744,6 +1812,17 @@
if (domFocusField) domFocusField.focus();
};
const selectColumnHeaderCell = uniquePath => {
const modelIndex = columns.findIndex(x => x.uniquePath == uniquePath);
const realIndex = columnSizes.modelToReal(modelIndex);
let cell = ['header', realIndex];
// @ts-ignore
currentCell = cell;
// @ts-ignore
selectedCells = [cell];
if (domFocusField) domFocusField.focus();
};
const [inplaceEditorState, dispatchInsplaceEditor] = createReducer((state, action) => {
switch (action.type) {
case 'show':
@@ -1796,7 +1875,7 @@
{ command: 'dataGrid.refresh' },
{ placeTag: 'copy' },
{
text: _t('datagrid.copyAdvanced', { defaultMessage: 'Copy advanced'}),
text: _t('datagrid.copyAdvanced', { defaultMessage: 'Copy advanced' }),
submenu: [
_.keys(copyRowsFormatDefs).map(format => ({
text: _tval(copyRowsFormatDefs[format].label),
@@ -1804,7 +1883,7 @@
})),
{ divider: true },
_.keys(copyRowsFormatDefs).map(format => ({
text: _t('datagrid.setFormat', { defaultMessage: 'Set format: ' }) + (_tval(copyRowsFormatDefs[format].name)),
text: _t('datagrid.setFormat', { defaultMessage: 'Set format: ' }) + _tval(copyRowsFormatDefs[format].name),
onClick: () => ($copyRowsFormat = format),
})),
@@ -1841,6 +1920,18 @@
{ command: 'dataGrid.openJsonArrayInSheet', hideDisabled: true },
{ command: 'dataGrid.saveCellToFile', hideDisabled: true },
{ command: 'dataGrid.loadCellFromFile', hideDisabled: true },
{ command: 'dataGrid.toggleCellDataView', hideDisabled: true },
isProApp() && {
text: _t('datagrid.useMacro', { defaultMessage: 'Use macro' }),
submenu: macros
.filter(macro => !macroCondition || macroCondition(macro))
.map(macro => ({
text: _tval(macro.title),
onClick: () => {
selectedMacro.set(macro);
},
})),
},
// { command: 'dataGrid.copyJsonDocument', hideDisabled: true },
{ divider: true },
{ placeTag: 'export' },
@@ -1992,6 +2083,7 @@
data-row="header"
data-col={col.colIndex}
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
class:active-header-cell={currentCell && currentCell[0] == 'header' && currentCell[1] == col.colIndex}
>
<ColumnHeaderControl
column={col}
@@ -2066,6 +2158,9 @@
onFocusGrid={() => {
selectTopmostCell(col.uniqueName);
}}
onFocusGridHeader={() => {
selectColumnHeaderCell(col.uniqueName);
}}
dataType={col.dataType}
filterDisabled={display.isFilterDisabled(col.uniqueName)}
/>
@@ -2192,6 +2287,9 @@
background-color: var(--theme-bg-1);
overflow: hidden;
}
:global(.data-grid-focused) .active-header-cell {
background-color: var(--theme-bg-selected);
}
.filter-cell {
text-align: left;
overflow: hidden;
@@ -76,6 +76,7 @@
onShowForm={onSetFormView && !overlayDefinition ? () => onSetFormView(rowData, null) : null}
extraIcon={overlayDefinition ? OVERLAY_STATUS_ICONS[rowStatus.status] : null}
extraIconTooltip={overlayDefinition ? OVERLAY_STATUS_TOOLTIPS[rowStatus.status] : null}
isSelected={frameSelection ? false : !!selectedCells?.find(cell => cell[0] == rowIndex && cell[1] == 'header')}
/>
{#each visibleRealColumns as col (col.uniqueName)}
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
@@ -7,6 +7,7 @@
export let extraIcon = null;
export let extraIconTooltip = null;
export let isSelected = false;
let mouseIn = false;
</script>
@@ -14,6 +15,7 @@
<td
data-row={rowIndex}
data-col="header"
class:selected={isSelected}
on:mouseenter={() => (mouseIn = true)}
on:mouseleave={() => (mouseIn = false)}
>
@@ -43,4 +45,7 @@
right: 0px;
top: 1px;
}
:global(.data-grid-focused) td.selected {
background-color: var(--theme-bg-selected);
}
</style>
+18
View File
@@ -13,6 +13,24 @@ export function isRegularCell(cell: CellAddress): cell is RegularCellAddress {
return _.isNumber(row) && _.isNumber(col);
}
export function isRowHeaderCell(cell: CellAddress): boolean {
if (!cell) return false;
const [row, col] = cell;
return col === 'header' && _.isNumber(row);
}
export function isColumnHeaderCell(cell: CellAddress): boolean {
if (!cell) return false;
const [row, col] = cell;
return row === 'header' && _.isNumber(col);
}
export function isTableHeaderCell(cell: CellAddress): boolean {
if (!cell) return false;
const [row, col] = cell;
return row === 'header' && col === 'header';
}
function normalizeHeaderForSelection(addr: CellAddress): CellAddress {
if (addr[0] == 'filter') return ['header', addr[1]];
return addr;
@@ -1,4 +1,44 @@
<script lang="ts" context="module">
const LAT_PRIORITY_PATTERNS = [
/^lat$/i,
/^latitude$/i,
/latitude$/i,
/lat$/i,
/latitude/i,
/lat/i,
];
const LON_PRIORITY_PATTERNS = [
/^lon$/i,
/^lng$/i,
/^longitude$/i,
/longitude$/i,
/lon$/i,
/lng$/i,
/longitude/i,
/lon|lng/i,
];
function getFieldName(fieldPath) {
return fieldPath.split('.').pop() || fieldPath;
}
function getFieldPriority(fieldPath, patterns) {
const name = getFieldName(fieldPath);
for (let i = 0; i < patterns.length; i++) {
if (patterns[i].test(name)) return i;
}
return patterns.length;
}
function sortByPriorityThenLength(paths, patterns) {
return paths.sort((a, b) => {
const priorityDiff = getFieldPriority(a, patterns) - getFieldPriority(b, patterns);
if (priorityDiff !== 0) return priorityDiff;
return getFieldName(a).length - getFieldName(b).length;
});
}
function findLatLonPaths(obj, attrTest, res = [], prefix = '') {
for (const key of Object.keys(obj)) {
if (attrTest(key, obj[key])) {
@@ -10,11 +50,15 @@
}
return res;
}
export function findLatPaths(obj) {
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lat'));
return sortByPriorityThenLength(paths, LAT_PRIORITY_PATTERNS);
}
export function findLonPaths(obj) {
return findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
const paths = findLatLonPaths(obj, x => x.toLowerCase()?.includes('lon') || x.toLowerCase()?.includes('lng'));
return sortByPriorityThenLength(paths, LON_PRIORITY_PATTERNS);
}
export function findAllObjectPaths(obj) {
return findLatLonPaths(obj, (_k, v) => v != null && !_.isNaN(Number(v)));
+19 -11
View File
@@ -7,9 +7,11 @@
import FormStringList from './FormStringList.svelte';
import FormDropDownTextField from './FormDropDownTextField.svelte';
import { getFormContext } from './FormProviderCore.svelte';
import { _tval } from '../translations';
export let arg;
export let namePrefix;
export let isReadOnly = false;
$: name = `${namePrefix}${arg.name}`;
@@ -18,46 +20,52 @@
{#if arg.type == 'text'}
<FormTextField
label={arg.label}
label={_tval(arg.label)}
{name}
defaultValue={arg.default}
focused={arg.focused}
placeholder={arg.placeholder}
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{:else if arg.type == 'stringlist'}
<FormStringList label={arg.label} addButtonLabel={arg.addButtonLabel} {name} placeholder={arg.placeholder} />
<FormStringList
label={_tval(arg.label)}
addButtonLabel={_tval(arg.addButtonLabel)}
{name}
placeholder={arg.placeholder}
isReadOnly={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{:else if arg.type == 'number'}
<FormTextField
label={arg.label}
label={_tval(arg.label)}
type="number"
{name}
defaultValue={arg.default}
focused={arg.focused}
placeholder={arg.placeholder}
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{:else if arg.type == 'checkbox'}
<FormCheckboxField
label={arg.label}
label={_tval(arg.label)}
{name}
defaultValue={arg.default}
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{:else if arg.type == 'select'}
<FormSelectField
label={arg.label}
label={_tval(arg.label)}
isNative
{name}
defaultValue={arg.default}
options={arg.options.map(opt =>
_.isString(opt) ? { label: opt, value: opt } : { label: opt.name, value: opt.value }
)}
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{:else if arg.type == 'dropdowntext'}
<FormDropDownTextField
label={arg.label}
label={_tval(arg.label)}
{name}
defaultValue={arg.default}
menu={() => {
@@ -66,6 +74,6 @@
onClick: () => setFieldValue(name, _.isString(opt) ? opt : opt.value),
}));
}}
disabled={arg.disabledFn ? arg.disabledFn($values) : arg.disabled}
disabled={isReadOnly || (arg.disabledFn ? arg.disabledFn($values) : arg.disabled)}
/>
{/if}
@@ -3,10 +3,11 @@
export let namePrefix = '';
export let args: any[];
export let isReadOnly = false;
</script>
<div>
{#each args as arg (arg.name)}
<FormArgument {arg} {namePrefix} />
<FormArgument {arg} {namePrefix} {isReadOnly} />
{/each}
</div>
+9 -5
View File
@@ -12,6 +12,7 @@
export let addButtonLabel;
export let placeholder;
export let templateProps;
export let isReadOnly = false;
const { template, values, setFieldValue } = getFormContext();
@@ -20,7 +21,7 @@
<svelte:component this={template} type="text" {label} {...templateProps}>
{#each stringList as value, index}
<div class='input-line-flex'>
<div class="input-line-flex">
<TextField
{value}
{placeholder}
@@ -28,12 +29,14 @@
const newValues = stringList.map((v, i) => (i === index ? e.target['value'] : v));
setFieldValue(name, newValues);
}}
disabled={isReadOnly}
/>
<InlineButton
on:click={() => {
setFieldValue(name, [...stringList.slice(0, index), ...stringList.slice(index + 1)]);
}}
disabled={isReadOnly}
>
<FontIcon icon="icon delete" />
</InlineButton>
@@ -45,11 +48,12 @@
on:click={() => {
setFieldValue(name, [...stringList, '']);
}}
disabled={isReadOnly}
/>
</svelte:component>
<style>
.input-line-flex {
display: flex;
}
</style>
.input-line-flex {
display: flex;
}
</style>
+2 -2
View File
@@ -2,7 +2,7 @@
import { getContext } from 'svelte';
import FontIcon from '../icons/FontIcon.svelte';
import ToolbarButton from '../buttons/ToolbarButton.svelte';
import { _t } from '../translations';
import { _t, _tval } from '../translations';
export let onExecute;
@@ -13,7 +13,7 @@
<div class="header">
<FontIcon icon="img macro" />
<div class="ml-2">
{$selectedMacro?.title}
{_tval($selectedMacro?.title)}
</div>
</div>
<div class="buttons">
+2 -2
View File
@@ -5,7 +5,7 @@
import WidgetTitle from '../widgets/WidgetTitle.svelte';
import MacroParameters from './MacroParameters.svelte';
import { _t } from '../translations';
import { _t, _tval } from '../translations';
const selectedMacro = getContext('selectedMacro') as any;
@@ -31,7 +31,7 @@
<div class="section">
<WidgetTitle>{_t('common.description', { defaultMessage: 'Description' })}</WidgetTitle>
<div class="m-1">{$selectedMacro?.description}</div>
<div class="m-1">{_tval($selectedMacro?.description)}</div>
</div>
</div>
@@ -95,7 +95,6 @@
title: _t('common.exportDatabase', { defaultMessage: 'Export database' }),
description: _t('newObject.exportDescription', { defaultMessage: 'Export to file like CSV, JSON, Excel, or other DB' }),
command: 'database.export',
isProFeature: true,
testid: 'NewObjectModal_databaseExport',
disabledMessage: _t('newObject.exportDisabled', { defaultMessage: 'Export is not available for current database' }),
},
@@ -9,6 +9,7 @@
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import PerspectiveFiltersColumn from './PerspectiveFiltersColumn.svelte';
import { _t } from '../translations';
export let managerSize;
export let config: PerspectiveConfig;
@@ -25,8 +26,8 @@
<ManagerInnerContainer width={managerSize} isFlex={filterCount == 0}>
{#if filterCount == 0}
<div class="msg">
<div class="mb-3 bold">No Filters defined</div>
<div><FontIcon icon="img info" /> Use context menu, command "Add to filter" in table or in tree</div>
<div class="mb-3 bold">{_t('perspective.noFiltersDefined', { defaultMessage: "No Filters defined" })}</div>
<div><FontIcon icon="img info" /> {_t('perspective.useContextMenuAddToFilter', { defaultMessage: 'Use context menu, command "Add to filter" in table or in tree' })}</div>
</div>
{:else}
{#each config.nodes as nodeConfig}
@@ -3,8 +3,8 @@
registerCommand({
id: 'perspective.openJson',
category: 'Perspective',
name: 'Open JSON',
category: __t('command.perspective', { defaultMessage: 'Perspective' }),
name: __t('command.perspective.openJson', { defaultMessage: 'Open JSON' }),
isRelatedToTab: true,
testEnabled: () => getCurrentEditor()?.openJsonEnabled(),
onClick: () => getCurrentEditor().openJson(),
@@ -40,6 +40,7 @@
import openNewTab from '../utility/openNewTab';
import { getFilterValueExpression } from 'dbgate-filterparser';
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
import { __t } from '../translations';
const TABS_BY_FIELD = {
tables: {
@@ -3,8 +3,8 @@
registerCommand({
id: 'perspective.customJoin',
category: 'Perspective',
name: 'Custom join',
category: __t('perspective.category', { defaultMessage: 'Perspective' }),
name: __t('perspective.customJoin', { defaultMessage: 'Custom join' }),
keyText: 'CtrlOrCommand+J',
isRelatedToTab: true,
icon: 'icon custom-join',
@@ -65,6 +65,7 @@
import FontIcon from '../icons/FontIcon.svelte';
import InlineButton from '../buttons/InlineButton.svelte';
import { usePerspectiveDataPatterns } from '../utility/usePerspectiveDataPatterns';
import { _t, __t } from '../translations';
const dbg = debug('dbgate:PerspectiveView');
@@ -168,7 +169,7 @@
<HorizontalSplitter initialValue={getInitialManagerSize()} bind:size={managerSize} allowCollapseChild1>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title="Choose data" name="perspectiveTree" height={'70%'}>
<WidgetColumnBarItem title={_t('perspective.chooseData', { defaultMessage: "Choose data" })} name="perspectiveTree" height={'70%'}>
{#if tempRoot && tempRoot != root}
<div class="temp-root">
<div>
@@ -184,7 +185,7 @@
{/if}
<SearchBoxWrapper>
<SearchInput placeholder="Search column or table" bind:value={filter} />
<SearchInput placeholder={_t('perspective.searchColumnOrTable', { defaultMessage: "Search column or table" })} bind:value={filter} />
<CloseSearchButton bind:filter />
</SearchBoxWrapper>
@@ -195,7 +196,7 @@
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Filters" name="tableFilters">
<WidgetColumnBarItem title={_t('perspective.filters', { defaultMessage: "Filters" })} name="tableFilters">
<PerspectiveFilters {managerSize} {config} {setConfig} {conid} {database} {driver} {root} />
</WidgetColumnBarItem>
</WidgetColumnBar>
+4 -1
View File
@@ -12,7 +12,7 @@
import useEffect from '../utility/useEffect';
import { getContext } from 'svelte';
import { mountCodeCompletion } from './codeCompletion';
import { getCurrentSettings } from '../stores';
import { currentEditorWrapEnabled, getCurrentSettings } from '../stores';
export let engine = null;
export let conid = null;
export let database = null;
@@ -29,6 +29,8 @@
mode = engineToMode[match ? match[1] : engine] || 'sql';
}
$: enableWrap = $currentEditorWrapEnabled || false;
export function getEditor(): ace.Editor {
return domEditor.getEditor();
}
@@ -63,5 +65,6 @@
options={{
...$$props.options,
enableBasicAutocompletion: true,
wrap: enableWrap,
}}
/>
@@ -6,6 +6,8 @@
import FormArgumentList from '../forms/FormArgumentList.svelte';
import { _t } from '../translations';
export let isFormReadOnly;
const { values } = getFormContext();
$: engine = $values.engine;
@@ -17,9 +19,18 @@
$: advancedFields = driver?.getAdvancedConnectionFields ? driver?.getAdvancedConnectionFields() : null;
</script>
<FormTextAreaField label={_t('connection.allowedDatabases', { defaultMessage: 'Allowed databases, one per line' })} name="allowedDatabases" disabled={isConnected} rows={8} />
<FormTextField label={_t('connection.allowedDatabasesRegex', { defaultMessage: 'Allowed databases regular expression' })} name="allowedDatabasesRegex" disabled={isConnected} />
<FormTextAreaField
label={_t('connection.allowedDatabases', { defaultMessage: 'Allowed databases, one per line' })}
name="allowedDatabases"
disabled={isConnected || isFormReadOnly}
rows={8}
/>
<FormTextField
label={_t('connection.allowedDatabasesRegex', { defaultMessage: 'Allowed databases regular expression' })}
name="allowedDatabasesRegex"
disabled={isConnected || isFormReadOnly}
/>
{#if advancedFields}
<FormArgumentList args={advancedFields} />
<FormArgumentList args={advancedFields} isReadOnly={isFormReadOnly} />
{/if}
@@ -23,6 +23,7 @@
export let getDatabaseList;
export let currentConnection;
export let isFormReadOnly;
const { values, setFieldValue } = getFormContext();
const electron = getElectron();
@@ -90,10 +91,10 @@
label={_t('connection.type', { defaultMessage: 'Connection type' })}
name="engine"
isNative
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_connectionType"
options={[
{ label: _t('connection.selectType', { defaultMessage: '(select connection type)' })},
{ label: _t('connection.selectType', { defaultMessage: '(select connection type)' }) },
..._.sortBy(
$extensions.drivers
// .filter(driver => !driver.isElectronOnly || electron)
@@ -113,7 +114,7 @@
data-testid="ConnectionDriverFields_authType"
name="authType"
isNative
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
defaultValue={driver?.defaultAuthTypeName}
options={$authTypes.map(auth => ({
value: auth.name,
@@ -127,16 +128,18 @@
<FormClusterNodesField
label={_t('connection.clusterNodes', { defaultMessage: 'Cluster nodes' })}
name="clusterNodes"
disabled={isConnected || disabledFields.includes('clusterNodes')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('clusterNodes')}
data-testid="ConnectionDriverFields_clusterNodes"
/>
{/if}
{#if driver?.showConnectionField('autoDetectNatMap', $values, showConnectionFieldArgs)}
<FormCheckboxField
label={_t('connection.autoDetectNatMap', { defaultMessage: 'Auto detect NAT map (use for Redis Cluster in Docker network)' })}
label={_t('connection.autoDetectNatMap', {
defaultMessage: 'Auto detect NAT map (use for Redis Cluster in Docker network)',
})}
name="autoDetectNatMap"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_autoDetectNatMap"
/>
{/if}
@@ -146,13 +149,13 @@
<FormElectronFileSelector
label={_t('connection.databaseFile', { defaultMessage: 'Database file' })}
name="databaseFile"
disabled={isConnected || disabledFields.includes('databaseFile')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('databaseFile')}
/>
{:else}
<FormTextField
label={_t('connection.databaseFilePath', { defaultMessage: 'Database file (path on server)' })}
name="databaseFile"
disabled={isConnected || disabledFields.includes('databaseFile')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('databaseFile')}
/>
{/if}
{/if}
@@ -160,11 +163,15 @@
{#if driver?.showConnectionField('useDatabaseUrl', $values, showConnectionFieldArgs)}
<div class="radio">
<FormRadioGroupField
disabled={isConnected || disabledFields.includes('useDatabaseUrl')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('useDatabaseUrl')}
name="useDatabaseUrl"
matchValueToOption={(value, option) => !!option.value == !!value}
options={[
{ label: _t('connection.fillDetails', { defaultMessage: 'Fill database connection details' }), value: '', default: true },
{
label: _t('connection.fillDetails', { defaultMessage: 'Fill database connection details' }),
value: '',
default: true,
},
{ label: _t('connection.useUrl', { defaultMessage: 'Use database URL' }), value: '1' },
]}
/>
@@ -177,7 +184,7 @@
name="databaseUrl"
data-testid="ConnectionDriverFields_databaseUrl"
placeholder={driver?.databaseUrlPlaceholder}
disabled={isConnected || disabledFields.includes('databaseUrl')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('databaseUrl')}
/>
{/if}
@@ -187,7 +194,7 @@
name="localDataCenter"
data-testid="ConnectionDriverFields_localDataCenter"
placeholder={driver?.defaultLocalDataCenter}
disabled={isConnected || disabledFields.includes('localDataCenter')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('localDataCenter')}
/>
{/if}
@@ -196,7 +203,7 @@
label={_t('connection.authToken', { defaultMessage: 'Auth token' })}
name="authToken"
data-testid="ConnectionDriverFields_authToken"
disabled={isConnected || disabledFields.includes('authToken')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('authToken')}
/>
{/if}
@@ -207,7 +214,7 @@
data-testid="ConnectionDriverFields_authType"
name="authType"
isNative
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
defaultValue={driver?.defaultAuthTypeName}
options={$authTypes.map(auth => ({
value: auth.name,
@@ -219,9 +226,9 @@
{#if driver?.showConnectionField('endpoint', $values, showConnectionFieldArgs)}
<FormTextField
label='Endpoint'
label="Endpoint"
name="endpoint"
disabled={isConnected || disabledFields.includes('endpoint')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('endpoint')}
data-testid="ConnectionDriverFields_endpoint"
/>
{/if}
@@ -230,7 +237,7 @@
<FormTextField
label={_t('connection.endpointKey', { defaultMessage: 'Key' })}
name="endpointKey"
disabled={isConnected || disabledFields.includes('endpointKey')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('endpointKey')}
data-testid="ConnectionDriverFields_endpointKey"
/>
{/if}
@@ -239,7 +246,7 @@
<FormTextField
label={_t('connection.clientLibraryPath', { defaultMessage: 'Client library path' })}
name="clientLibraryPath"
disabled={isConnected || disabledFields.includes('clientLibraryPath')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('clientLibraryPath')}
data-testid="ConnectionDriverFields_clientLibraryPath"
/>
{/if}
@@ -250,7 +257,7 @@
<FormTextField
label={_t('connection.server', { defaultMessage: 'Server' })}
name="server"
disabled={isConnected || disabledFields.includes('server')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('server')}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_server"
/>
@@ -260,7 +267,7 @@
<FormTextField
label="Port"
name="port"
disabled={isConnected || disabledFields.includes('port')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('port')}
templateProps={{ noMargin: true }}
placeholder={driver?.defaultPort}
data-testid="ConnectionDriverFields_port"
@@ -271,7 +278,9 @@
{#if getCurrentConfig().isDocker}
<div class="row">
<FontIcon icon="img warn" padRight />
{ _t('connection.dockerWarning', { defaultMessage: 'Under docker, localhost and 127.0.0.1 will not work, use dockerhost instead' }) }
{_t('connection.dockerWarning', {
defaultMessage: 'Under docker, localhost and 127.0.0.1 will not work, use dockerhost instead',
})}
</div>
{/if}
{/if}
@@ -280,9 +289,11 @@
<div class="row">
<div class="col-9 mr-1">
<FormTextField
label={$values.serviceNameType == 'sid' ? 'SID' : _t('connection.serviceName', { defaultMessage: 'Service name' })}
label={$values.serviceNameType == 'sid'
? 'SID'
: _t('connection.serviceName', { defaultMessage: 'Service name' })}
name="serviceName"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_serviceName"
/>
@@ -293,7 +304,7 @@
isNative
name="serviceNameType"
defaultValue="serviceName"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
templateProps={{ noMargin: true }}
options={[
{ value: 'serviceName', label: _t('connection.serviceName', { defaultMessage: 'Service name' }) },
@@ -309,7 +320,7 @@
<FormTextField
label={_t('connection.socketPath', { defaultMessage: 'Socket path' })}
name="socketPath"
disabled={isConnected || disabledFields.includes('socketPath')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('socketPath')}
placeholder={driver?.defaultSocketPath}
data-testid="ConnectionDriverFields_scoketPath"
/>
@@ -322,7 +333,7 @@
<FormTextField
label={_t('connection.user', { defaultMessage: 'User' })}
name="user"
disabled={isConnected || disabledFields.includes('user')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('user')}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_user"
/>
@@ -333,7 +344,7 @@
<FormPasswordField
label={_t('connection.password', { defaultMessage: 'Password' })}
name="password"
disabled={isConnected || disabledFields.includes('password')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('password')}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_password"
/>
@@ -345,7 +356,7 @@
<FormTextField
label={_t('connection.user', { defaultMessage: 'User' })}
name="user"
disabled={isConnected || disabledFields.includes('user')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('user')}
data-testid="ConnectionDriverFields_user"
/>
{/if}
@@ -353,7 +364,7 @@
<FormPasswordField
label={_t('connection.password', { defaultMessage: 'Password' })}
name="password"
disabled={isConnected || disabledFields.includes('password')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('password')}
data-testid="ConnectionDriverFields_password"
/>
{/if}
@@ -380,7 +391,7 @@
<FormTextField
label={_t('connection.accessKeyId', { defaultMessage: 'Access Key ID' })}
name="accessKeyId"
disabled={isConnected || disabledFields.includes('accessKeyId')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('accessKeyId')}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_accesKeyId"
/>
@@ -391,7 +402,7 @@
<FormPasswordField
label={_t('connection.secretAccessKey', { defaultMessage: 'Secret access key' })}
name="secretAccessKey"
disabled={isConnected || disabledFields.includes('secretAccessKey')}
disabled={isConnected || isFormReadOnly || disabledFields.includes('secretAccessKey')}
templateProps={{ noMargin: true }}
data-testid="ConnectionDriverFields_secretAccessKey"
/>
@@ -405,12 +416,15 @@
isNative
name="passwordMode"
defaultValue="saveEncrypted"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
options={[
{ value: 'saveEncrypted', label: _t('connection.saveEncrypted', { defaultMessage: 'Save and encrypt' }) },
{ value: 'saveRaw', label: _t('connection.saveRaw', { defaultMessage: 'Save raw (UNSAFE!!)' }) },
{ value: 'askPassword', label: _t('connection.askPassword', { defaultMessage: "Don't save, ask for password" }) },
{ value: 'askUser', label: _t('connection.askUser', { defaultMessage: "Don't save, ask for login and password" }) },
{
value: 'askUser',
label: _t('connection.askUser', { defaultMessage: "Don't save, ask for login and password" }),
},
]}
data-testid="ConnectionDriverFields_passwordMode"
/>
@@ -420,7 +434,7 @@
<FormTextField
label={_t('connection.keySeparator', { defaultMessage: 'Key separator' })}
name="treeKeySeparator"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
placeholder=":"
data-testid="ConnectionDriverFields_treeKeySeparator"
/>
@@ -430,7 +444,7 @@
<FormTextField
label={_t('connection.windowsDomain', { defaultMessage: 'Domain (specify to use NTLM authentication)' })}
name="windowsDomain"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_windowsDomain"
/>
{/if}
@@ -439,7 +453,7 @@
<FormCheckboxField
label={_t('connection.isReadOnly', { defaultMessage: 'Is read only' })}
name="isReadOnly"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_isReadOnly"
/>
{/if}
@@ -448,7 +462,7 @@
<FormCheckboxField
label={_t('connection.trustServerCertificate', { defaultMessage: 'Trust server certificate' })}
name="trustServerCertificate"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_trustServerCertificate"
/>
{/if}
@@ -457,33 +471,42 @@
<FormDropDownTextField
label={_t('connection.defaultDatabase', { defaultMessage: 'Default database' })}
name="defaultDatabase"
disabled={isConnected}
disabled={isConnected || isFormReadOnly || disabledFields.includes('defaultDatabase')}
data-testid="ConnectionDriverFields_defaultDatabase"
asyncMenu={createDatabasesMenu}
placeholder={_t('common.notSelectedOptional', { defaultMessage : "(not selected - optional)"})}
placeholder={_t('common.notSelectedOptional', { defaultMessage: '(not selected - optional)' })}
/>
{/if}
{#if defaultDatabase && driver?.showConnectionField('singleDatabase', $values, showConnectionFieldArgs)}
<FormCheckboxField
label={_t('connection.singleDatabase', { defaultMessage: 'Use only database {defaultDatabase}', values: { defaultDatabase } })}
label={_t('connection.singleDatabase', {
defaultMessage: 'Use only database {defaultDatabase}',
values: { defaultDatabase },
})}
name="singleDatabase"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_singleDatabase"
/>
{/if}
{#if driver?.showConnectionField('useSeparateSchemas', $values, showConnectionFieldArgs)}
<FormCheckboxField
label={_t('connection.useSeparateSchemas', { defaultMessage: 'Use schemas separately (use this if you have many large schemas)' })}
label={_t('connection.useSeparateSchemas', {
defaultMessage: 'Use schemas separately (use this if you have many large schemas)',
})}
name="useSeparateSchemas"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_useSeparateSchemas"
/>
{/if}
{#if driver?.showConnectionField('connectionDefinition', $values, showConnectionFieldArgs)}
<FormFileInputField disabled={isConnected} label={_t('connection.connectionDefinition', { defaultMessage: 'Service account key JSON' })} name="connectionDefinition" />
<FormFileInputField
disabled={isConnected || isFormReadOnly}
label={_t('connection.connectionDefinition', { defaultMessage: 'Service account key JSON' })}
name="connectionDefinition"
/>
{/if}
{#if driver}
@@ -493,7 +516,7 @@
label={_t('connection.displayName', { defaultMessage: 'Display name' })}
name="displayName"
templateProps={{ noMargin: true }}
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_displayName"
placeholder={getConnectionLabel(currentConnection)}
/>
@@ -505,7 +528,7 @@
name="connectionColor"
emptyLabel="(not selected)"
templateProps={{ noMargin: true }}
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionDriverFields_connectionColor"
/>
</div>
@@ -14,6 +14,8 @@
import { extensions, openedConnections, openedSingleDatabaseConnections } from '../stores';
import { _t } from '../translations';
export let isFormReadOnly;
const { values, setFieldValue } = getFormContext();
const electron = getElectron();
@@ -30,9 +32,9 @@
</script>
<FormCheckboxField
label={_t('connection.sshTunnel.use', {defaultMessage: "Use SSH tunnel"})}
label={_t('connection.sshTunnel.use', { defaultMessage: 'Use SSH tunnel' })}
name="useSshTunnel"
disabled={isConnected}
disabled={isConnected || isFormReadOnly}
data-testid="ConnectionSshTunnelFields_useSshTunnel"
/>
@@ -41,7 +43,7 @@
<FormTextField
label="Host"
name="sshHost"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
data-testid="ConnectionSshTunnelFields_sshHost"
/>
@@ -50,23 +52,30 @@
<FormTextField
label="Port"
name="sshPort"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
placeholder="22"
data-testid="ConnectionSshTunnelFields_sshPort"
/>
</div>
</div>
<FormTextField label="Bastion host (Jump host)" name="sshBastionHost" disabled={isConnected || !useSshTunnel} />
<FormTextField
label="Bastion host (Jump host)"
name="sshBastionHost"
disabled={isConnected || !useSshTunnel || isFormReadOnly}
/>
<FormSelectField
label={_t('connection.sshTunnel.authentication', {defaultMessage: "SSH Authentication"})}
label={_t('connection.sshTunnel.authentication', { defaultMessage: 'SSH Authentication' })}
name="sshMode"
isNative
defaultSelectValue="userPassword"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
options={[
{ value: 'userPassword', label: _t('connection.sshTunnel.authMethod.userPassword', {defaultMessage: "Username & password"}) },
{
value: 'userPassword',
label: _t('connection.sshTunnel.authMethod.userPassword', { defaultMessage: 'Username & password' }),
},
{ value: 'agent', label: 'SSH agent' },
{ value: 'keyFile', label: 'Key file' },
]}
@@ -77,7 +86,7 @@
<FormTextField
label="Login"
name="sshLogin"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
data-testid="ConnectionSshTunnelFields_sshLogin"
/>
{/if}
@@ -88,16 +97,16 @@
<FormTextField
label="Login"
name="sshLogin"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
data-testid="ConnectionSshTunnelFields_sshLogin"
/>
</div>
<div class="col-6">
<FormPasswordField
label={_t('connection.password', {defaultMessage: 'Password'})}
label={_t('connection.password', { defaultMessage: 'Password' })}
name="sshPassword"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
data-testid="ConnectionSshTunnelFields_sshPassword"
/>
@@ -110,18 +119,18 @@
<div class="col-6 mr-1">
{#if electron}
<FormElectronFileSelector
label={_t('connection.sshTunnel.privateKeyFile', {defaultMessage: "Private key file"})}
label={_t('connection.sshTunnel.privateKeyFile', { defaultMessage: 'Private key file' })}
name="sshKeyfile"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
defaultFileName={$platformInfo?.defaultKeyfile}
data-testid="ConnectionSshTunnelFields_sshKeyfile"
/>
{:else}
<FormTextField
label={_t('connection.sshTunnel.privateKeyFilePath', {defaultMessage: "Private key file (path on server)"})}
label={_t('connection.sshTunnel.privateKeyFilePath', { defaultMessage: 'Private key file (path on server)' })}
name="sshKeyfile"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
placeholder={$platformInfo?.defaultKeyfile}
data-testid="ConnectionSshTunnelFields_sshKeyfile"
@@ -130,9 +139,9 @@
</div>
<div class="col-6">
<FormPasswordField
label={_t('connection.sshTunnel.keyFilePassphrase', {defaultMessage: "Key file passphrase"})}
label={_t('connection.sshTunnel.keyFilePassphrase', { defaultMessage: 'Key file passphrase' })}
name="sshKeyfilePassword"
disabled={isConnected || !useSshTunnel}
disabled={isConnected || !useSshTunnel || isFormReadOnly}
templateProps={{ noMargin: true }}
data-testid="ConnectionSshTunnelFields_sshKeyfilePassword"
/>
@@ -143,9 +152,10 @@
{#if useSshTunnel && $values.sshMode == 'agent'}
<div class="ml-3 mb-3">
{#if $platformInfo && $platformInfo.sshAuthSock}
<FontIcon icon="img ok" /> {_t('connection.sshTunnel.agentFound', {defaultMessage: "SSH Agent found"})}
<FontIcon icon="img ok" /> {_t('connection.sshTunnel.agentFound', { defaultMessage: 'SSH Agent found' })}
{:else}
<FontIcon icon="img error" /> {_t('connection.sshTunnel.agentNotFound', {defaultMessage: "SSH Agent not found"})}
<FontIcon icon="img error" />
{_t('connection.sshTunnel.agentNotFound', { defaultMessage: 'SSH Agent not found' })}
{/if}
</div>
{/if}
@@ -9,6 +9,8 @@
import { openedConnections, openedSingleDatabaseConnections } from '../stores';
import { _t } from '../translations';
export let isFormReadOnly;
const { values, setFieldValue } = getFormContext();
const electron = getElectron();
@@ -16,21 +18,35 @@
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
</script>
<FormCheckboxField label={_t('connection.ssl.use', {defaultMessage: "Use SSL"})} name="useSsl" disabled={isConnected} />
<FormElectronFileSelector label={_t('connection.ssl.caCert', {defaultMessage: "CA Cert (optional)"})} name="sslCaFile" disabled={isConnected || !useSsl || !electron} />
<FormCheckboxField
label={_t('connection.ssl.use', { defaultMessage: 'Use SSL' })}
name="useSsl"
disabled={isConnected || isFormReadOnly}
/>
<FormElectronFileSelector
label={_t('connection.ssl.certificate', {defaultMessage: "Certificate (optional)"})}
label={_t('connection.ssl.caCert', { defaultMessage: 'CA Cert (optional)' })}
name="sslCaFile"
disabled={isConnected || !useSsl || !electron || isFormReadOnly}
/>
<FormElectronFileSelector
label={_t('connection.ssl.certificate', { defaultMessage: 'Certificate (optional)' })}
name="sslCertFile"
disabled={isConnected || !useSsl || !electron}
disabled={isConnected || !useSsl || !electron || isFormReadOnly}
/>
<FormPasswordField
label={_t('connection.ssl.certificateKeyFilePassword', {defaultMessage: "Certificate key file password (optional)"})}
label={_t('connection.ssl.certificateKeyFilePassword', {
defaultMessage: 'Certificate key file password (optional)',
})}
name="sslCertFilePassword"
disabled={isConnected || !useSsl || !electron}
disabled={isConnected || !useSsl || !electron || isFormReadOnly}
/>
<FormElectronFileSelector
label={_t('connection.ssl.keyFile', {defaultMessage: "Key file (optional)"})}
label={_t('connection.ssl.keyFile', { defaultMessage: 'Key file (optional)' })}
name="sslKeyFile"
disabled={isConnected || !useSsl || !electron}
disabled={isConnected || !useSsl || !electron || isFormReadOnly}
/>
<FormCheckboxField
label={_t('connection.ssl.rejectUnauthorized', { defaultMessage: 'Reject unauthorized' })}
name="sslRejectUnauthorized"
disabled={isConnected || !useSsl || isFormReadOnly}
/>
<FormCheckboxField label={_t('connection.ssl.rejectUnauthorized', {defaultMessage: "Reject unauthorized"})} name="sslRejectUnauthorized" disabled={isConnected || !useSsl} />
@@ -15,8 +15,6 @@
<div class="wrapper">
<div class="heading">{_t('settings.sqlEditor', { defaultMessage: 'SQL editor' })}</div>
<div class="flex">
<div class="col-3">
<FormSelectField
label={_t('settings.sqlEditor.sqlCommandsCase', { defaultMessage: 'SQL commands case' })}
name="sqlEditor.sqlCommandsCase"
@@ -28,8 +26,6 @@
]}
data-testid="SQLEditorSettings_sqlCommandsCase"
/>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.editor.keybinds', { defaultMessage: 'Editor keybinds' })}
type="combo"
@@ -42,53 +38,49 @@
on:change={e => ($currentEditorKeybindigMode = e.detail)}
/>
</FormFieldTemplateLarge>
</div>
<div class="col-3">
<FormFieldTemplateLarge
label={_t('settings.editor.wordWrap', { defaultMessage: 'Enable word wrap' })}
type="combo"
type="checkbox"
>
<CheckboxField
checked={$currentEditorWrapEnabled}
on:change={e => ($currentEditorWrapEnabled = e.target.checked)}
/>
</FormFieldTemplateLarge>
</div>
</div>
<FormTextField
name="sqlEditor.limitRows"
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
/>
<FormTextField
name="sqlEditor.limitRows"
label={_t('settings.sqlEditor.limitRows', { defaultMessage: 'Return only N rows from query' })}
placeholder={_t('settings.sqlEditor.limitRowsPlaceholder', { defaultMessage: '(No rows limit)' })}
/>
<FormCheckboxField
name="sqlEditor.showTableAliasesInCodeCompletion"
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
defaultMessage: 'Show table aliases in code completion',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.showTableAliasesInCodeCompletion"
label={_t('settings.sqlEditor.showTableAliasesInCodeCompletion', {
defaultMessage: 'Show table aliases in code completion',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableSplitByEmptyLine"
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableSplitByEmptyLine"
label={_t('settings.sqlEditor.disableSplitByEmptyLine', { defaultMessage: 'Disable split by empty line' })}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableExecuteCurrentLine"
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
defaultMessage: 'Disable current line execution (Execute current)',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.disableExecuteCurrentLine"
label={_t('settings.sqlEditor.disableExecuteCurrentLine', {
defaultMessage: 'Disable current line execution (Execute current)',
})}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.hideColumnsPanel"
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
defaultValue={false}
/>
<FormCheckboxField
name="sqlEditor.hideColumnsPanel"
label={_t('settings.sqlEditor.hideColumnsPanel', { defaultMessage: 'Hide Columns/Filters panel by default' })}
defaultValue={false}
/>
</div>
<style>
@@ -102,4 +94,8 @@ defaultValue={false}
.wrapper :global(input){
max-width: 400px;
}
.wrapper :global(select) {
max-width: 400px;
}
</style>
-1
View File
@@ -168,7 +168,6 @@ export const nullStore = readable(null, () => {});
export const currentArchive = writableWithStorage('default', 'currentArchive');
export const currentApplication = writableWithStorage(null, 'currentApplication');
export const isFileDragActive = writable(false);
export const selectedCellsCallback = writable(null);
export const loadingPluginStore = writable({
loaded: false,
loadingPackageName: null,
+5 -1
View File
@@ -1,3 +1,7 @@
<script lang="ts" context="module">
let tabContentCounter = 0;
</script>
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
@@ -19,7 +23,7 @@
$: tabFocusedStore.set(tabFocused);
</script>
<div class:tabVisible>
<div class:tabVisible data-testid={`TabContent_${tabContentCounter++}`}>
<svelte:component this={tabComponent} {...$$restProps} {tabid} {tabVisible} {tabFocused} {tabPreviewMode} />
</div>
@@ -221,5 +221,6 @@
<ToolStripExportButton {quickExportHandlerRef} command="collectionDataGrid.export" />
<ToolStripCommandButton command="collectionJsonView.expandAll" hideDisabled />
<ToolStripCommandButton command="collectionJsonView.collapseAll" hideDisabled />
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled data-testid="CollectionDataTab_toggleCellDataView" />
</svelte:fragment>
</ToolStripContainer>
+8 -2
View File
@@ -59,6 +59,8 @@
}
);
$: isFormReadOnly = !!$values.import_source_id;
// $: console.log('ConnectionTab.$values', $values);
// $: console.log('ConnectionTab.driver', driver);
@@ -302,22 +304,25 @@
{
label: _t('common.general', { defaultMessage: 'General' }),
component: ConnectionDriverFields,
props: { getDatabaseList, currentConnection },
props: { getDatabaseList, currentConnection, isFormReadOnly },
testid: 'ConnectionTab_tabGeneral',
},
driver?.showConnectionTab('sshTunnel', $values) && {
label: 'SSH Tunnel',
component: ConnectionSshTunnelFields,
props: { isFormReadOnly },
testid: 'ConnectionTab_tabSshTunnel',
},
driver?.showConnectionTab('ssl', $values) && {
label: 'SSL',
component: ConnectionSslFields,
props: { isFormReadOnly },
testid: 'ConnectionTab_tabSsl',
},
{
label: _t('common.advanced', { defaultMessage: 'Advanced' }),
component: ConnectionAdvancedDriverFields,
props: { isFormReadOnly },
testid: 'ConnectionTab_tabAdvanced',
},
]}
@@ -383,7 +388,8 @@
{/if}
{#if isTesting}
<div>
<FontIcon icon="icon loading" /> {_t('common.testingConnection', { defaultMessage: 'Testing connection' })}
<FontIcon icon="icon loading" />
{_t('common.testingConnection', { defaultMessage: 'Testing connection' })}
</div>
{/if}
</div>
+1 -2
View File
@@ -141,11 +141,10 @@
/>
</svelte:fragment>
<svelte:fragment slot="2">
<WidgetColumnBar>
<WidgetColumnBar storageName="diagramSettingsWidget">
<WidgetColumnBarItem
title="Settings"
name="diagramSettings"
storageName="diagramSettingsWidget"
onClose={() => {
styleStore.update(x => ({ ...x, settingsVisible: false }));
}}
+36 -21
View File
@@ -1,17 +1,19 @@
<script lang="ts" context="module">
const getCurrentEditor = () => getActiveComponent('ImportExportTab');
registerFileCommands({
idPrefix: 'impexp',
category: 'Import & Export',
getCurrentEditor,
folder: 'impexp',
format: 'json',
fileExtension: 'impexp',
if (isProApp()) {
registerFileCommands({
idPrefix: 'impexp',
category: 'Import & Export',
getCurrentEditor,
folder: 'impexp',
format: 'json',
fileExtension: 'impexp',
// undoRedo: true,
defaultTeamFolder: true,
});
// undoRedo: true,
defaultTeamFolder: true,
});
}
</script>
<script lang="ts">
@@ -55,6 +57,7 @@
import { tick } from 'svelte';
import { showSnackbarError } from '../utility/snackbar';
import { _t } from '../translations';
import { isProApp } from '../utility/proTools';
let busy = false;
let executeNumber = 0;
@@ -290,21 +293,24 @@
/>
{#if busy}
<LoadingInfo wrapper message={_t('importExport.processingImportExport', { defaultMessage: "Processing import/export ..." })} />
<LoadingInfo
wrapper
message={_t('importExport.processingImportExport', { defaultMessage: 'Processing import/export ...' })}
/>
{/if}
</div>
<svelte:fragment slot="2">
<WidgetColumnBar>
<WidgetColumnBarItem
title={_t('importExport.outputFiles', { defaultMessage: "Output files" })}
title={_t('importExport.outputFiles', { defaultMessage: 'Output files' })}
name="output"
height="20%"
data-testid="ImportExportTab_outputFiles"
>
<RunnerOutputFiles {runnerId} {executeNumber} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('importExport.messages', { defaultMessage: "Messages" })} name="messages">
<WidgetColumnBarItem title={_t('importExport.messages', { defaultMessage: 'Messages' })} name="messages">
<SocketMessageView
eventName={runnerId ? `runner-info-${runnerId}` : null}
{executeNumber}
@@ -313,16 +319,23 @@
/>
</WidgetColumnBarItem>
<WidgetColumnBarItem
title={_t('importExport.preview', { defaultMessage: "Preview" })}
title={_t('importExport.preview', { defaultMessage: 'Preview' })}
name="preview"
skip={!$previewReaderStore}
data-testid="ImportExportTab_preview"
>
<PreviewDataGrid reader={$previewReaderStore} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('importExport.advancedConfiguration', { defaultMessage: "Advanced configuration" })} name="config" collapsed>
<FormTextField label={_t('importExport.schedule', { defaultMessage: "Schedule" })} name="schedule" />
<FormTextField label={_t('importExport.startVariableIndex', { defaultMessage: "Start variable index" })} name="startVariableIndex" />
<WidgetColumnBarItem
title={_t('importExport.advancedConfiguration', { defaultMessage: 'Advanced configuration' })}
name="config"
collapsed
>
<FormTextField label={_t('importExport.schedule', { defaultMessage: 'Schedule' })} name="schedule" />
<FormTextField
label={_t('importExport.startVariableIndex', { defaultMessage: 'Start variable index' })}
name="startVariableIndex"
/>
</WidgetColumnBarItem>
</WidgetColumnBar>
</svelte:fragment>
@@ -331,17 +344,19 @@
<svelte:fragment slot="toolstrip">
{#if busy}
<ToolStripButton icon="icon stop" on:click={handleCancel} data-testid="ImportExportTab_stopButton"
>{_t('importExport.stop', { defaultMessage: "Stop" })}</ToolStripButton
>{_t('importExport.stop', { defaultMessage: 'Stop' })}</ToolStripButton
>
{:else}
<ToolStripButton on:click={handleExecute} icon="icon run" data-testid="ImportExportTab_executeButton"
>{_t('importExport.run', { defaultMessage: "Run" })}</ToolStripButton
>{_t('importExport.run', { defaultMessage: 'Run' })}</ToolStripButton
>
{/if}
<ToolStripButton icon="img shell" on:click={handleGenerateScript} data-testid="ImportExportTab_generateScriptButton"
>{_t('importExport.generateScript', { defaultMessage: "Generate script" })}</ToolStripButton
>{_t('importExport.generateScript', { defaultMessage: 'Generate script' })}</ToolStripButton
>
<ToolStripSaveButton idPrefix="impexp" />
{#if isProApp()}
<ToolStripSaveButton idPrefix="impexp" />
{/if}
</svelte:fragment>
</ToolStripContainer>
@@ -16,7 +16,7 @@
registerCommand({
id: 'jsonl.save',
group: 'save',
category: 'JSON Lines editor',
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
name: __t('command.jsonl.save', { defaultMessage: 'Save' }),
toolbar: true,
isRelatedToTab: true,
@@ -27,7 +27,7 @@
registerCommand({
id: 'jsonl.preview',
category: 'JSON Lines editor',
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
name: __t('command.jsonl.preview', { defaultMessage: 'Preview' }),
icon: 'icon preview',
keyText: 'F5',
@@ -37,7 +37,7 @@
registerCommand({
id: 'jsonl.previewNewTab',
category: 'JSON Lines editor',
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
name: __t('command.jsonl.previewNewTab', { defaultMessage: 'Preview in new tab' }),
icon: 'icon preview',
testEnabled: () => getCurrentEditor() != null,
@@ -46,7 +46,7 @@
registerCommand({
id: 'jsonl.closePreview',
category: 'JSON Lines editor',
category: __t('command.jsonl', { defaultMessage: 'JSON Lines editor' }),
name: __t('command.jsonl.closePreview', { defaultMessage: 'Close preview' }),
icon: 'icon close',
testEnabled: () => getCurrentEditor()?.isPreview(),
+50 -32
View File
@@ -170,6 +170,9 @@
import QueryAiAssistant from '../ai/QueryAiAssistant.svelte';
import { getCurrentSettings } from '../stores';
import { Messages } from 'openai/resources/chat/completions';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetsInnerContainer from '../widgets/WidgetsInnerContainer.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
export let tabid;
export let conid;
@@ -717,9 +720,6 @@
...driver?.getQuerySplitterOptions('editor'),
splitByEmptyLine: !$settingsValue?.['sqlEditor.disableSplitByEmptyLine'],
}}
options={{
wrap: enableWrap,
}}
value={$editorState.value || ''}
menu={createMenu()}
on:input={e => {
@@ -791,33 +791,45 @@
</VerticalSplitter>
</svelte:fragment>
<svelte:fragment slot="2">
<QueryAiAssistant
bind:this={domAiAssistant}
{conid}
{database}
{driver}
onClose={() => {
isAiAssistantVisible = false;
}}
text={$editorValue}
getLine={() => domEditor.getEditor().getSelectionRange().start.row}
onInsertAtCursor={text => {
const editor = domEditor.getEditor();
editor.session.insert(editor.getCursorPosition(), text);
domEditor?.getEditor()?.focus();
}}
getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
onSetSelectedText={text => {
const editor = domEditor.getEditor();
if (editor.getSelectedText()) {
const range = editor.selection.getRange();
editor.session.replace(range, text);
} else {
editor.setValue(text);
}
}}
{tabid}
/>
<WidgetColumnBar>
<WidgetColumnBarItem
title={_t('query.AiAssistant', { defaultMessage: 'AI Assistant' })}
onClose={() => {
isAiAssistantVisible = false;
}}
name='aiAssistant'
>
<WidgetsInnerContainer skipDefineWidth flexContainer>
<QueryAiAssistant
bind:this={domAiAssistant}
{conid}
{database}
{driver}
onClose={() => {
isAiAssistantVisible = false;
}}
text={$editorValue}
getLine={() => domEditor.getEditor().getSelectionRange().start.row}
onInsertAtCursor={text => {
const editor = domEditor.getEditor();
editor.session.insert(editor.getCursorPosition(), text);
domEditor?.getEditor()?.focus();
}}
getTextOrSelectedText={() => domEditor.getEditor().getSelectedText() || $editorValue}
onSetSelectedText={text => {
const editor = domEditor.getEditor();
if (editor.getSelectedText()) {
const range = editor.selection.getRange();
editor.session.replace(range, text);
} else {
editor.setValue(text);
}
}}
{tabid}
/>
</WidgetsInnerContainer>
</WidgetColumnBarItem>
</WidgetColumnBar>
</svelte:fragment>
</HorizontalSplitter>
<svelte:fragment slot="toolstrip">
@@ -839,11 +851,17 @@
},
})}
>
{queryRowsLimit ? _t('query.limitRows', { defaultMessage: 'Limit {queryRowsLimit} rows', values: { queryRowsLimit } }) : _t('query.unlimitedRows', { defaultMessage: 'Unlimited rows' })}</ToolStripButton
{queryRowsLimit
? _t('query.limitRows', { defaultMessage: 'Limit {queryRowsLimit} rows', values: { queryRowsLimit } })
: _t('query.unlimitedRows', { defaultMessage: 'Unlimited rows' })}</ToolStripButton
>
{/if}
{#if resultCount == 1}
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} label={_t('export.result', { defaultMessage: 'Export result' })} />
<ToolStripExportButton
command="jslTableGrid.export"
{quickExportHandlerRef}
label={_t('export.result', { defaultMessage: 'Export result' })}
/>
{/if}
<ToolStripDropDownButton
menu={() =>
+33 -7
View File
@@ -23,7 +23,7 @@
time: 60,
name: __t('command.datagrid.setAutoRefresh.60', { defaultMessage: 'Refresh every 60 seconds' }),
},
]
];
registerCommand({
id: 'tableData.save',
@@ -172,7 +172,10 @@
const resp = await apiCall('database-connections/run-script', { conid, database, sql, useTransaction: true });
const { errorMessage } = resp || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }), message: errorMessage });
showModal(ErrorMessageModal, {
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
message: errorMessage,
});
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
cache.update(reloadDataCacheFunc);
@@ -192,7 +195,10 @@
});
const { errorMessage } = resp || {};
if (errorMessage) {
showModal(ErrorMessageModal, { title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }), message: errorMessage });
showModal(ErrorMessageModal, {
title: _t('tableData.errorWhenSaving', { defaultMessage: 'Error when saving' }),
message: errorMessage,
});
} else {
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
cache.update(reloadDataCacheFunc);
@@ -284,7 +290,10 @@
{ command: 'tableData.stopAutoRefresh', hideDisabled: true },
{ command: 'tableData.startAutoRefresh', hideDisabled: true },
'tableData.setAutoRefresh.1',
...INTERVALS.map(seconds => ({ command: `tableData.setAutoRefresh.${seconds}`, text: `...${seconds}` + ' ' + _t('command.datagrid.autoRefresh.seconds', { defaultMessage: 'seconds' }) })),
...INTERVALS.map(seconds => ({
command: `tableData.setAutoRefresh.${seconds}`,
text: `...${seconds}` + ' ' + _t('command.datagrid.autoRefresh.seconds', { defaultMessage: 'seconds' }),
})),
];
}
</script>
@@ -360,13 +369,23 @@
>
<ToolStripCommandSplitButton
buttonLabel={autoRefreshStarted ? _t('tableData.refreshEvery', { defaultMessage: 'Refresh (every {autoRefreshInterval}s)', values: { autoRefreshInterval } }) : null}
buttonLabel={autoRefreshStarted
? _t('tableData.refreshEvery', {
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
values: { autoRefreshInterval },
})
: null}
commands={['dataGrid.refresh', ...createAutoRefreshMenu()]}
hideDisabled
data-testid="TableDataTab_refreshGrid"
/>
<ToolStripCommandSplitButton
buttonLabel={autoRefreshStarted ? _t('tableData.refreshEvery', { defaultMessage: 'Refresh (every {autoRefreshInterval}s)', values: { autoRefreshInterval } }) : null}
buttonLabel={autoRefreshStarted
? _t('tableData.refreshEvery', {
defaultMessage: 'Refresh (every {autoRefreshInterval}s)',
values: { autoRefreshInterval },
})
: null}
commands={['dataForm.refresh', ...createAutoRefreshMenu()]}
hideDisabled
data-testid="TableDataTab_refreshForm"
@@ -402,7 +421,14 @@
<ToolStripButton
icon={$collapsedLeftColumnStore ? 'icon columns-outline' : 'icon columns'}
on:click={() => collapsedLeftColumnStore.update(x => !x)}>{_t('tableData.viewColumns', { defaultMessage: 'View columns' })}</ToolStripButton
on:click={() => collapsedLeftColumnStore.update(x => !x)}
>{_t('tableData.viewColumns', { defaultMessage: 'View columns' })}</ToolStripButton
>
<ToolStripCommandButton
command="dataGrid.toggleCellDataView"
hideDisabled
data-testid="TableDataTab_toggleCellDataView"
/>
</svelte:fragment>
</ToolStripContainer>
+1
View File
@@ -129,6 +129,7 @@
<ToolStripCommandButton command="dataGrid.refresh" />
<ToolStripExportButton {quickExportHandlerRef} />
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled />
</svelte:fragment>
</ToolStripContainer>
{/if}
+2 -2
View File
@@ -124,7 +124,7 @@ export function __t(key: string, options: TranslateOptions): DefferedTranslation
};
}
export function _tval(x: string | DefferedTranslationResult): string {
export function _tval(x: any | DefferedTranslationResult): string {
if (typeof x === 'string') return x;
if (typeof x?._transKey === 'string') {
return _t(x._transKey, x._transOptions);
@@ -132,7 +132,7 @@ export function _tval(x: string | DefferedTranslationResult): string {
if (typeof x?._transCallback === 'function') {
return x._transCallback();
}
return '';
return x?.toString() || '';
}
export function isDefferedTranslationResult(x: string | DefferedTranslationResult): x is DefferedTranslationResult {
@@ -7,7 +7,7 @@ import { isProApp } from './proTools';
export function createQuickExportMenuItems(handler: (fmt: QuickExportDefinition) => Function, advancedExportMenuItem) {
const extensions = getExtensions();
return [
isProApp() && {
{
text: _t('export.exportAdvanced', { defaultMessage : 'Export advanced...'}),
...advancedExportMenuItem,
},
+3
View File
@@ -1,6 +1,7 @@
const cache = {};
export function getLocalStorage(key, defaultValue = undefined) {
if (!key) return defaultValue;
if (key in cache) return cache[key];
const item = localStorage.getItem(key);
if (item) {
@@ -16,11 +17,13 @@ export function getLocalStorage(key, defaultValue = undefined) {
}
export function setLocalStorage(key, value) {
if (!key) return;
localStorage.setItem(key, JSON.stringify(value));
delete cache[key];
}
export function removeLocalStorage(key) {
if (!key) return;
localStorage.removeItem(key);
delete cache[key];
}
@@ -0,0 +1,308 @@
import { newMatcherFn } from 'diff2html/lib/rematch';
import _ from 'lodash';
export interface WidgetBarStoredProps {
contentHeight: number;
collapsed: boolean;
}
export interface WidgetBarStoredPropsResult {
[name: string]: WidgetBarStoredProps;
}
export interface WidgetBarComputedProps {
contentHeight: number;
storedHeight?: number;
visibleItemsCount: number;
splitterVisible: boolean;
collapsed: boolean;
clickableTitle: boolean;
}
export interface WidgetBarComputedResult {
[name: string]: WidgetBarComputedProps;
}
export interface WidgetBarItemDefinition {
name: string;
height?: string; // e.g. '200px' or '30%'
collapsed: boolean; // initial value of collapsing status
skip: boolean;
minimalContentHeight: number;
storeHeight: boolean;
}
export type PushWidgetBarItemDefinitionFunction = (def: WidgetBarItemDefinition) => void;
export type UpdateWidgetBarItemDefinitionFunction = (name: string, def: Partial<WidgetBarItemDefinition>) => void;
export type ResizeWidgetItemFunction = (name: string, deltaY: number) => void;
export type ToggleCollapseWidgetItemFunction = (name: string) => void;
export interface WidgetBarContainerProps {
clientHeight: number;
titleHeight: number;
splitterHeight: number;
}
// accordeon mode - only one item can be expanded at a time
export function widgetShouldBeInAccordeonMode(
container: WidgetBarContainerProps,
definitions: WidgetBarItemDefinition[]
): boolean {
const visibleItems = definitions.filter(def => !def.skip);
const availableContentHeight =
container.clientHeight -
visibleItems.length * container.titleHeight -
Math.max(0, visibleItems.length - 1) * container.splitterHeight;
const minimalRequiredContentHeight = _.sum(visibleItems.map(def => def.minimalContentHeight));
return availableContentHeight < minimalRequiredContentHeight;
}
export function computeInitialWidgetBarProps(
container: WidgetBarContainerProps,
definitions: WidgetBarItemDefinition[],
currentProps: WidgetBarComputedResult
): WidgetBarComputedResult {
if (!container.clientHeight) {
return currentProps;
}
const visibleItems = definitions.filter(def => !def.skip);
const expandedItems = visibleItems.filter(def => !(currentProps[def.name]?.collapsed ?? def.collapsed));
const res: WidgetBarComputedResult = {};
const availableContentHeight =
container.clientHeight -
visibleItems.length * container.titleHeight -
Math.max(0, expandedItems.length - 1) * container.splitterHeight;
if (widgetShouldBeInAccordeonMode(container, definitions)) {
// In accordeon mode, only the first expanded item is shown, others are collapsed
const expandedItem = visibleItems.find(def => !def.collapsed);
for (const def of visibleItems) {
const isExpanded = def.name === expandedItem?.name;
res[def.name] = {
contentHeight: isExpanded ? availableContentHeight : 0,
storedHeight: currentProps[def.name]?.contentHeight,
visibleItemsCount: visibleItems.length,
splitterVisible: false,
collapsed: !isExpanded,
clickableTitle: !isExpanded,
};
}
return res;
}
// First pass: calculate base heights
let totalContentHeight = 0;
const itemHeights = {};
const flexibleItems = [];
for (const def of expandedItems) {
if (def.storeHeight && currentProps[def.name]?.storedHeight > 0) {
const storedHeight = currentProps[def.name].storedHeight;
itemHeights[def.name] = storedHeight;
totalContentHeight += storedHeight;
} else if (def.height) {
let height = 0;
if (_.isString(def.height) && def.height.endsWith('px')) {
height = parseInt(def.height.slice(0, -2));
} else if (_.isString(def.height) && def.height.endsWith('%'))
height = (availableContentHeight * parseFloat(def.height.slice(0, -1))) / 100;
else {
height = parseInt(def.height);
}
if (height < def.minimalContentHeight) {
height = def.minimalContentHeight;
}
totalContentHeight += height;
itemHeights[def.name] = height;
} else {
flexibleItems.push(def);
}
}
// Second pass - distribute remaining height
if (flexibleItems.length > 0) {
let remainingHeight = availableContentHeight - totalContentHeight;
for (const def of flexibleItems) {
let height = remainingHeight / flexibleItems.length;
if (height < def.minimalContentHeight) height = def.minimalContentHeight;
itemHeights[def.name] = height;
}
}
// Third pass - update heights to match available height
totalContentHeight = _.sum(Object.values(itemHeights));
if (totalContentHeight != availableContentHeight) {
const scale = availableContentHeight / totalContentHeight;
for (const def of expandedItems) {
itemHeights[def.name] = itemHeights[def.name] * scale;
}
}
// Final assembly of results
let visibleIndex = 0;
for (const def of visibleItems) {
res[def.name] = {
contentHeight: Math.round(itemHeights[def.name] || 0),
visibleItemsCount: visibleItems.length,
splitterVisible: visibleItems.length > 1 && visibleIndex < visibleItems.length - 1,
collapsed: !expandedItems.includes(def),
storedHeight: currentProps[def.name]?.storedHeight,
clickableTitle: true,
};
visibleIndex += 1;
}
return res;
}
export function handleResizeWidgetBar(
container: WidgetBarContainerProps,
definitions: WidgetBarItemDefinition[],
currentProps: WidgetBarComputedResult,
resizedItemName: string,
deltaY: number
): WidgetBarComputedResult {
const res = _.cloneDeep(currentProps);
const visibleItems = definitions.filter(def => !def.skip);
const currentItemDef = definitions.find(def => def.name === resizedItemName);
if (!currentItemDef || currentItemDef.collapsed) return res;
const currentItemProps = res[resizedItemName];
let itemIndex = visibleItems.findIndex(def => def.name === resizedItemName);
const itemProps = res[currentItemDef.name];
const nextItemDef = visibleItems[itemIndex + 1];
const currentHeight = itemProps.contentHeight;
const nextItemProps = res[nextItemDef.name];
if (!nextItemDef) return res;
if (deltaY < 0) {
let newHeight = currentHeight + deltaY;
if (newHeight < currentItemDef.minimalContentHeight) {
newHeight = currentItemDef.minimalContentHeight;
}
const actualDeltaY = newHeight - currentHeight;
nextItemProps.contentHeight -= actualDeltaY;
currentItemProps.contentHeight += actualDeltaY;
// // moving up - reduce height of resized item, if too small, reduce height of previous items
// let remainingDeltaY = -deltaY;
// let itemIndex = visibleItems.findIndex(def => def.name === resizedItemName);
// while (remainingDeltaY > 0 && itemIndex >= 0) {
// const itemDef = visibleItems[itemIndex];
// const itemProps = res[itemDef.name];
// const currentHeight = itemProps.contentHeight;
// const minimalHeight = itemDef.minimalContentHeight;
// const reducibleHeight = currentHeight - minimalHeight;
// if (reducibleHeight > 0) {
// const reduction = Math.min(reducibleHeight, remainingDeltaY);
// itemProps.contentHeight -= reduction;
// remainingDeltaY -= reduction;
// }
// itemIndex -= 1;
// }
} else {
let newHeight = nextItemProps.contentHeight - deltaY;
if (newHeight < nextItemDef.minimalContentHeight) {
newHeight = nextItemDef.minimalContentHeight;
}
const actualDeltaY = nextItemProps.contentHeight - newHeight;
nextItemProps.contentHeight -= actualDeltaY;
currentItemProps.contentHeight += actualDeltaY;
// moving down - increase height of resized item, reduce size of next item, if too small, reduce size of further items
// if all items below are at minimal height, stop
// let remainingDeltaY = deltaY;
// let itemIndex = visibleItems.findIndex(def => def.name === resizedItemName);
// while (remainingDeltaY > 0 && itemIndex < visibleItems.length) {
// const itemDef = visibleItems[itemIndex];
// const itemProps = res[itemDef.name];
// const currentHeight = itemProps.contentHeight;
// const minimalHeight = itemDef.minimalContentHeight;
// const reducibleHeight = currentHeight - minimalHeight;
// if (reducibleHeight > 0) {
// const reduction = Math.min(reducibleHeight, remainingDeltaY);
// itemProps.contentHeight -= reduction;
// resizedItemProps.contentHeight += reduction;
// remainingDeltaY -= reduction;
// }
// itemIndex += 1;
// }
}
if (currentItemDef.storeHeight) {
currentItemProps.storedHeight = currentItemProps.contentHeight;
}
if (nextItemDef.storeHeight) {
nextItemProps.storedHeight = nextItemProps.contentHeight;
}
return res;
}
export function toggleCollapseWidgetBar(
container: WidgetBarContainerProps,
definitions: WidgetBarItemDefinition[],
currentProps: WidgetBarComputedResult,
toggledItemName: string
): WidgetBarComputedResult {
const visibleItems = definitions.filter(def => !def.skip);
if (widgetShouldBeInAccordeonMode(container, definitions)) {
// In accordeon mode, only the first expanded item is shown, others are collapsed
const res: WidgetBarComputedResult = {};
for (const def of visibleItems) {
const isExpanded = def.name === toggledItemName;
res[def.name] = {
contentHeight: undefined,
visibleItemsCount: visibleItems.length,
splitterVisible: false,
collapsed: !isExpanded,
clickableTitle: !isExpanded,
};
}
return res;
}
const res = _.cloneDeep(currentProps);
res[toggledItemName].collapsed = !res[toggledItemName].collapsed;
return computeInitialWidgetBarProps(container, definitions, res);
}
export function extractStoredWidgetBarProps(
definitions: WidgetBarItemDefinition[],
currentProps: WidgetBarComputedResult
): WidgetBarStoredPropsResult {
const res: WidgetBarStoredPropsResult = {};
for (const key in currentProps) {
const def = definitions.find(d => d.name === key);
if (!def) continue;
res[key] = {
contentHeight: def.storeHeight ? currentProps[key]?.storedHeight : undefined,
collapsed: currentProps[key]?.collapsed,
};
}
return res;
}
export function createWidgetBarComputedResultFromStored(stored: WidgetBarStoredPropsResult): WidgetBarComputedResult {
const res: WidgetBarComputedResult = {};
if (!stored) return res;
let visibleIndex = 0;
const visibleCount = Object.keys(stored).length;
for (const key in stored) {
res[key] = {
storedHeight: stored[key]?.contentHeight,
contentHeight: 0,
collapsed: stored[key]?.collapsed,
clickableTitle: false,
splitterVisible: visibleCount > 1 && visibleIndex < visibleCount - 1,
visibleItemsCount: 0,
};
visibleIndex += 1;
}
return res;
}
@@ -7,11 +7,11 @@
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title="Archive folders, DB models" name="folders" height="50%" storageName='archiveFoldersWidget'>
<WidgetColumnBar storageName="archiveWidget">
<WidgetColumnBarItem title="Archive folders, DB models" name="folders" height="50%">
<ArchiveFolderList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Files, Tables, Views, Functions" name="files" storageName='archiveFilesWidget'>
<WidgetColumnBarItem title="Files, Tables, Views, Functions" name="files">
<ArchiveFilesList />
</WidgetColumnBarItem>
</WidgetColumnBar>
+31 -13
View File
@@ -1,6 +1,4 @@
<script lang="ts" context="module">
import { isWktGeometry } from 'dbgate-tools';
const formats = [
{
type: 'textWrap',
@@ -14,6 +12,12 @@
component: TextCellViewNoWrap,
single: false,
},
{
type: 'form',
title: 'Form',
component: FormCellView,
single: false,
},
{
type: 'json',
title: 'Json',
@@ -59,10 +63,13 @@
];
function autodetect(selection) {
if (selection[0]?.isSelectedFullRow) {
return 'form';
}
if (selectionCouldBeShownOnMap(selection)) {
return 'map';
}
if (selection[0]?.engine?.databaseEngineTypes?.includes('document')) {
return 'jsonRow';
}
@@ -92,32 +99,31 @@
import JsonRowView from '../celldata/JsonRowView.svelte';
import MapCellView from '../celldata/MapCellView.svelte';
import PictureCellView from '../celldata/PictureCellView.svelte';
import FormCellView from '../celldata/FormCellView.svelte';
import TextCellViewNoWrap from '../celldata/TextCellViewNoWrap.svelte';
import TextCellViewWrap from '../celldata/TextCellViewWrap.svelte';
import ErrorInfo from '../elements/ErrorInfo.svelte';
import { selectionCouldBeShownOnMap } from '../elements/SelectionMapView.svelte';
import SelectField from '../forms/SelectField.svelte';
import { selectedCellsCallback } from '../stores';
import WidgetTitle from './WidgetTitle.svelte';
import JsonExpandedCellView from '../celldata/JsonExpandedCellView.svelte';
import XmlCellView from '../celldata/XmlCellView.svelte';
import { _t } from '../translations';
let selectedFormatType = 'autodetect';
export let onClose;
export let selection;
export let selection = undefined;
let selectedFormatType = 'autodetect';
$: autodetectFormatType = autodetect(selection);
$: autodetectFormat = formats.find(x => x.type == autodetectFormatType);
$: usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
$: usedFormat = formats.find(x => x.type == usedFormatType);
$: selection = $selectedCellsCallback ? $selectedCellsCallback() : [];
</script>
<div class="wrapper">
<WidgetTitle>{_t('cellDataWidget.title', { defaultMessage: "Cell data view" })}</WidgetTitle>
<WidgetTitle {onClose}>{_t('cellDataWidget.title', { defaultMessage: 'Cell data view' })}</WidgetTitle>
<div class="main">
<div class="toolbar">
Format:<span>&nbsp;</span>
@@ -127,18 +133,30 @@
on:change={e => (selectedFormatType = e.detail)}
data-testid="CellDataWidget_selectFormat"
options={[
{ value: 'autodetect', label: _t('cellDataWidget.autodetect', { defaultMessage: "Autodetect - {autoDetectTitle}", values : { autoDetectTitle: autodetectFormat.title } }) },
{
value: 'autodetect',
label: _t('cellDataWidget.autodetect', {
defaultMessage: 'Autodetect - {autoDetectTitle}',
values: { autoDetectTitle: autodetectFormat.title },
}),
},
...formats.map(fmt => ({ label: fmt.title, value: fmt.type })),
]}
/>
</div>
<div class="data">
{#if usedFormat.single && selection?.length != 1}
<ErrorInfo message={_t('cellDataWidget.mustSelectOneCell', { defaultMessage: "Must be selected one cell" })} alignTop />
<ErrorInfo
message={_t('cellDataWidget.mustSelectOneCell', { defaultMessage: 'Must be selected one cell' })}
alignTop
/>
{:else if usedFormat == null}
<ErrorInfo message={_t('cellDataWidget.formatNotSelected', { defaultMessage: "Format not selected" })} alignTop />
<ErrorInfo
message={_t('cellDataWidget.formatNotSelected', { defaultMessage: 'Format not selected' })}
alignTop
/>
{:else if !selection || selection.length == 0}
<ErrorInfo message={_t('cellDataWidget.noDataSelected', { defaultMessage: "No data selected" })} alignTop />
<ErrorInfo message={_t('cellDataWidget.noDataSelected', { defaultMessage: 'No data selected' })} alignTop />
{:else}
<svelte:component this={usedFormat?.component} {selection} />
{/if}
@@ -17,14 +17,9 @@
$: cloudContentList = useCloudContentList();
</script>
<WidgetColumnBar {hidden}>
<WidgetColumnBar {hidden} storageName="databaseWidget">
{#if $config?.singleConnection}
<WidgetColumnBarItem
title={_t('widget.databases', { defaultMessage: 'Databases' })}
name="databases"
height="35%"
storageName="databasesWidget"
>
<WidgetColumnBarItem title={_t('widget.databases', { defaultMessage: 'Databases' })} name="databases" height="35%">
<SingleConnectionDatabaseList connection={$config?.singleConnection} />
</WidgetColumnBarItem>
{:else if !$config?.singleDbConnection}
@@ -32,7 +27,7 @@
title={_t('common.connections', { defaultMessage: 'Connections' })}
name="connections"
height="35%"
storageName="connectionsWidget"
storeHeight
>
<ConnectionList
passProps={{
@@ -34,7 +34,6 @@
title={_t('widget.pinned', { defaultMessage: 'Pinned' })}
name="pinned"
height="15%"
storageName="pinnedItemsWidget"
skip={!_.compact($pinnedDatabases).length &&
!$pinnedTables.some(x => x && x.conid == conid && x.database == $currentDatabase?.name)}
positiveCondition={correctCloudStatus}
@@ -46,8 +45,7 @@
title={driver?.databaseEngineTypes?.includes('document')
? _t('widget.collectionsContainers', { defaultMessage: 'Collections/containers' })
: _t('widget.tablesViewsFunctions', { defaultMessage: 'Tables, views, functions' })}
name="dbObjects"
storageName="dbObjectsWidget"
name="dbObjectsSql"
skip={!(
conid &&
(database || singleDatabase) &&
@@ -60,8 +58,7 @@
<WidgetColumnBarItem
title={_t('widget.keys', { defaultMessage: 'Keys' })}
name="dbObjects"
storageName="dbObjectsWidget"
name="dbObjectsKeyValue"
skip={!(conid && (database || singleDatabase) && driver?.databaseEngineTypes?.includes('keyvalue'))}
positiveCondition={correctCloudStatus}
>
@@ -70,8 +67,7 @@
<WidgetColumnBarItem
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
name="dbObjects"
storageName="dbObjectsWidget"
name="dbObjectsFocused"
skip={conid && (database || singleDatabase)}
positiveCondition={correctCloudStatus}
>
@@ -84,8 +80,7 @@
<WidgetColumnBarItem
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
name="dbObjects"
storageName="dbObjectsWidget"
name="dbObjectsError"
skip={!(conid && (database || singleDatabase) && !driver)}
positiveCondition={correctCloudStatus}
>
@@ -102,7 +97,6 @@
title={_t('widget.databaseContent', { defaultMessage: 'Database content' })}
name="incorrectClaudStatus"
height="15%"
storageName="incorrectClaudStatusWidget"
skip={correctCloudStatus}
>
<WidgetsInnerContainer>
+3 -3
View File
@@ -18,13 +18,13 @@
$: favorites = useFavorites();
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('files.savedFiles', { defaultMessage: "Saved files" })} name="files" height="70%" storageName="savedFilesWidget">
<WidgetColumnBar storageName="filesWidget">
<WidgetColumnBarItem title={_t('files.savedFiles', { defaultMessage: 'Saved files' })} name="files" height="70%">
<SavedFilesList />
</WidgetColumnBarItem>
{#if hasPermission('files/favorites/read')}
<WidgetColumnBarItem title={_t('files.favorites', { defaultMessage: "Favorites" })} name="favorites" storageName="favoritesWidget">
<WidgetColumnBarItem title={_t('files.favorites', { defaultMessage: 'Favorites' })} name="favorites">
<WidgetsInnerContainer>
<AppObjectList list={$favorites || []} module={favoriteFileAppObject} />
</WidgetsInnerContainer>
@@ -16,11 +16,13 @@
import { _t } from '../translations';
$: favorites = useFavorites();
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('history.recentlyClosedTabs', { defaultMessage: "Recently closed tabs" })} name="closedTabs" storageName='closedTabsWidget'>
<WidgetColumnBar storageName="historyWidget">
<WidgetColumnBarItem
title={_t('history.recentlyClosedTabs', { defaultMessage: 'Recently closed tabs' })}
name="closedTabs"
>
<WidgetsInnerContainer>
<AppObjectList
list={_.sortBy(
@@ -31,7 +33,7 @@
/>
</WidgetsInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('history.queryHistory', { defaultMessage: "Query history" })} name="queryHistory" storageName='queryHistoryWidget'>
<WidgetColumnBarItem title={_t('history.queryHistory', { defaultMessage: 'Query history' })} name="queryHistory">
<QueryHistoryList />
</WidgetColumnBarItem>
</WidgetColumnBar>
@@ -8,11 +8,15 @@
import { _t } from '../translations';
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('widgets.installedExtensions', { defaultMessage: 'Installed extensions' })} name="installed" height="50%" storageName='installedPluginsWidget'>
<WidgetColumnBar storageName="pluginsWidget">
<WidgetColumnBarItem
title={_t('widgets.installedExtensions', { defaultMessage: 'Installed extensions' })}
name="installed"
height="50%"
>
<InstalledPluginsList />
</WidgetColumnBarItem>
<WidgetColumnBarItem title={_t('widgets.availableExtensions', { defaultMessage: 'Available extensions' })} name="all" storageName='allPluginsWidget'>
<WidgetColumnBarItem title={_t('widgets.availableExtensions', { defaultMessage: 'Available extensions' })} name="all">
<AvailablePluginsList />
</WidgetColumnBarItem>
</WidgetColumnBar>
@@ -162,7 +162,9 @@
text: _t('privateCloudWidget.addExistingFolder', { defaultMessage: 'Add existing folder (from link)' }),
onClick: () => {
showModal(InputTextModal, {
label: _t('privateCloudWidget.yourInviteLink', { defaultMessage: 'Your invite link (in form dbgate://folder/xxx)' }),
label: _t('privateCloudWidget.yourInviteLink', {
defaultMessage: 'Your invite link (in form dbgate://folder/xxx)',
}),
header: _t('privateCloudWidget.addExistingSharedFolder', { defaultMessage: 'Add existing shared folder' }),
onConfirm: async newFolder => {
apiCall('cloud/grant-folder', {
@@ -192,7 +194,10 @@
const handleDelete = () => {
showModal(ConfirmModal, {
message: _t('privateCloudWidget.deleteFolderConfirm', { defaultMessage: 'Really delete folder {folder}? All folder content will be deleted!', values: { folder: contentGroupMap[folder]?.name } }),
message: _t('privateCloudWidget.deleteFolderConfirm', {
defaultMessage: 'Really delete folder {folder}? All folder content will be deleted!',
values: { folder: contentGroupMap[folder]?.name },
}),
header: _t('privateCloudWidget.deleteFolder', { defaultMessage: 'Delete folder' }),
onConfirm: () => {
apiCall('cloud/delete-folder', {
@@ -240,19 +245,26 @@
}
</script>
<WidgetColumnBar>
<WidgetColumnBarItem
title="DbGate Cloud"
name="privateCloud"
height="50%"
storageName="privateCloudItems"
skip={!$cloudSigninTokenHolder}
>
<WidgetColumnBar storageName="privateCloudItems">
<WidgetColumnBarItem title="DbGate Cloud" name="privateCloud" height="50%" skip={!$cloudSigninTokenHolder}>
<SearchBoxWrapper>
<SearchInput placeholder={_t('privateCloudWidget.searchPlaceholder', { defaultMessage: 'Search cloud connections and files' })} bind:value={filter} />
<SearchInput
placeholder={_t('privateCloudWidget.searchPlaceholder', {
defaultMessage: 'Search cloud connections and files',
})}
bind:value={filter}
/>
<CloseSearchButton bind:filter />
<DropDownButton icon="icon plus-thick" menu={createAddItemMenu} title={_t('privateCloudWidget.addNewConnectionOrFile', { defaultMessage: 'Add new connection or file' })} />
<DropDownButton icon="icon add-folder" menu={createAddFolderMenu} title={_t('privateCloudWidget.addNewFolder', { defaultMessage: 'Add new folder' })} />
<DropDownButton
icon="icon plus-thick"
menu={createAddItemMenu}
title={_t('privateCloudWidget.addNewConnectionOrFile', { defaultMessage: 'Add new connection or file' })}
/>
<DropDownButton
icon="icon add-folder"
menu={createAddFolderMenu}
title={_t('privateCloudWidget.addNewFolder', { defaultMessage: 'Add new folder' })}
/>
<InlineButton
on:click={handleRefreshContent}
title={_t('privateCloudWidget.refreshFiles', { defaultMessage: 'Refresh files' })}
@@ -289,7 +301,10 @@
/>
{#if !cloudContentFlat?.length}
<ErrorInfo message={_t('privateCloudWidget.noContent', { defaultMessage: 'You have no content on DbGate cloud' })} icon="img info" />
<ErrorInfo
message={_t('privateCloudWidget.noContent', { defaultMessage: 'You have no content on DbGate cloud' })}
icon="img info"
/>
<div class="error-info">
<div class="m-1"></div>
<FormStyledButton
@@ -26,15 +26,21 @@
}
</script>
<WidgetColumnBar>
<WidgetColumnBarItem title={_t('publicCloudWidget.publicKnowledgeBase', { defaultMessage: "Public Knowledge Base" })} name="publicCloud" storageName="publicCloudItems">
<WidgetColumnBar storageName="publicCloudItems">
<WidgetColumnBarItem
title={_t('publicCloudWidget.publicKnowledgeBase', { defaultMessage: 'Public Knowledge Base' })}
name="publicCloud"
>
<WidgetsInnerContainer>
<SearchBoxWrapper>
<SearchInput placeholder={_t('publicCloudWidget.searchPublicFiles', { defaultMessage: "Search public files" })} bind:value={filter} />
<SearchInput
placeholder={_t('publicCloudWidget.searchPublicFiles', { defaultMessage: 'Search public files' })}
bind:value={filter}
/>
<CloseSearchButton bind:filter />
<InlineButton
on:click={handleRefreshPublic}
title={_t('publicCloudWidget.refreshFiles', { defaultMessage: "Refresh files" })}
title={_t('publicCloudWidget.refreshFiles', { defaultMessage: 'Refresh files' })}
data-testid="CloudItemsWidget_buttonRefreshPublic"
>
<FontIcon icon="icon refresh" />
@@ -49,12 +55,21 @@
/>
{#if !$publicFiles?.length}
<ErrorInfo message={_t('publicCloudWidget.noFilesFound', { defaultMessage: "No files found for your configuration" })} />
<ErrorInfo
message={_t('publicCloudWidget.noFilesFound', { defaultMessage: 'No files found for your configuration' })}
/>
<div class="error-info">
<div class="m-1">
{_t('publicCloudWidget.onlyRelevantFilesListed', { defaultMessage: "Only files relevant for your connections, platform and DbGate edition are listed. Please define connections at first." })}
{_t('publicCloudWidget.onlyRelevantFilesListed', {
defaultMessage:
'Only files relevant for your connections, platform and DbGate edition are listed. Please define connections at first.',
})}
</div>
<FormStyledButton value={_t('publicCloudWidget.refreshList', { defaultMessage: "Refresh list" })} skipWidth on:click={handleRefreshPublic} />
<FormStyledButton
value={_t('publicCloudWidget.refreshList', { defaultMessage: 'Refresh list' })}
skipWidth
on:click={handleRefreshPublic}
/>
</div>
{/if}
</WidgetsInnerContainer>
+64 -25
View File
@@ -1,38 +1,77 @@
<script lang="ts">
import { setContext } from 'svelte';
import { onMount, setContext } from 'svelte';
import { writable } from 'svelte/store';
import createRef from '../utility/createRef';
import _, { get } from 'lodash';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import {
computeInitialWidgetBarProps,
createWidgetBarComputedResultFromStored,
extractStoredWidgetBarProps,
handleResizeWidgetBar,
toggleCollapseWidgetBar,
WidgetBarItemDefinition,
} from '../utility/widgetBarResizing';
export let hidden = false;
export let storageName = null;
let definitions = [];
const dynamicPropsCollection = [];
let definitions: WidgetBarItemDefinition[] = [];
let clientHeight;
const widgetColumnBarHeight = writable(0);
// const widgetColumnBarHeight = writable(0);
const widgetColumnBarComputed = writable(createWidgetBarComputedResultFromStored(getLocalStorage(storageName)));
setContext('widgetColumnBarHeight', widgetColumnBarHeight);
setContext('pushWidgetItemDefinition', (item, dynamicProps) => {
dynamicPropsCollection.push(dynamicProps);
definitions = [...definitions, item];
return definitions.length - 1;
});
setContext('updateWidgetItemDefinition', (index, item) => {
definitions[index] = item;
});
$: containerProps = {
clientHeight,
titleHeight: 30,
splitterHeight: 3,
};
$: $widgetColumnBarHeight = clientHeight;
$: computeDynamicProps(definitions);
function computeDynamicProps(defs: any[]) {
const visibleItemsCount = defs.filter(x => !x.collapsed && !x.skip).length;
for (let index = 0; index < defs.length; index++) {
const definition = defs[index];
const splitterVisible = !!defs.slice(index + 1).find(x => x && !x.collapsed && !x.skip && x.positiveCondition);
dynamicPropsCollection[index].set({ splitterVisible, visibleItemsCount });
}
function saveStorage() {
if (!storageName) return;
setLocalStorage(storageName, extractStoredWidgetBarProps(definitions, $widgetColumnBarComputed));
}
// setContext('widgetColumnBarHeight', widgetColumnBarHeight);
setContext('pushWidgetItemDefinition', item => {
definitions = [...definitions, item];
});
setContext('updateWidgetItemDefinition', (name, item) => {
// console.log('WidgetColumnBar updateWidgetItemDefinition', name, item);
definitions = definitions.map(def => (def.name === name ? { ...def, ...item } : def));
});
setContext('widgetColumnBarComputed', widgetColumnBarComputed);
setContext('widgetResizeItem', (name, deltaY) => {
$widgetColumnBarComputed = handleResizeWidgetBar(
containerProps,
definitions,
$widgetColumnBarComputed,
name,
deltaY
);
saveStorage();
});
setContext('toggleWidgetCollapse', name => {
$widgetColumnBarComputed = toggleCollapseWidgetBar(containerProps, definitions, $widgetColumnBarComputed, name);
saveStorage();
});
// $: $widgetColumnBarHeight = clientHeight;
$: {
definitions;
containerProps;
recompute();
}
function recompute() {
$widgetColumnBarComputed = computeInitialWidgetBarProps(containerProps, definitions, $widgetColumnBarComputed);
saveStorage();
}
onMount(() => {
recompute();
});
</script>
<div class="main-container" bind:clientHeight class:hidden>
@@ -2,92 +2,89 @@
import _ from 'lodash';
import { getContext } from 'svelte';
import { writable } from 'svelte/store';
import type { Readable } from 'svelte/store';
import WidgetTitle from './WidgetTitle.svelte';
import splitterDrag from '../utility/splitterDrag';
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
import {
PushWidgetBarItemDefinitionFunction,
UpdateWidgetBarItemDefinitionFunction,
WidgetBarComputedResult,
WidgetBarComputedProps,
ToggleCollapseWidgetItemFunction,
ResizeWidgetItemFunction,
} from '../utility/widgetBarResizing';
// import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
export let title;
export let name;
export let skip = false;
export let positiveCondition = true;
export let height = null;
export let collapsed = null;
export let storeHeight = false;
export let storageName = null;
// export let storageName = null;
export let onClose = null;
export let minimalHeight = 50;
export let name;
let size = 0;
// let size = 0;
const dynamicProps = writable({
splitterVisible: false,
visibleItemsCount: 0,
// const dynamicProps = writable({
// splitterVisible: false,
// visibleItemsCount: 0,
// });
const pushWidgetItemDefinition = getContext('pushWidgetItemDefinition') as PushWidgetBarItemDefinitionFunction;
const updateWidgetItemDefinition = getContext('updateWidgetItemDefinition') as UpdateWidgetBarItemDefinitionFunction;
// const widgetColumnBarHeight = getContext('widgetColumnBarHeight') as any;
const widgetResizeItem = getContext('widgetResizeItem') as ResizeWidgetItemFunction;
const widgetColumnBarComputed = getContext('widgetColumnBarComputed') as Readable<WidgetBarComputedResult>;
const toggleWidgetCollapse = getContext('toggleWidgetCollapse') as ToggleCollapseWidgetItemFunction;
pushWidgetItemDefinition({
name,
collapsed,
height,
skip: skip || !positiveCondition,
minimalContentHeight: minimalHeight,
storeHeight,
});
const pushWidgetItemDefinition = getContext('pushWidgetItemDefinition') as any;
const updateWidgetItemDefinition = getContext('updateWidgetItemDefinition') as any;
const widgetColumnBarHeight = getContext('widgetColumnBarHeight') as any;
const widgetItemIndex = pushWidgetItemDefinition(
{
collapsed,
height,
skip,
positiveCondition,
},
dynamicProps
);
$: updateWidgetItemDefinition(name, { collapsed, height, skip: skip || !positiveCondition });
$: updateWidgetItemDefinition(widgetItemIndex, { collapsed: !visible, height, skip, positiveCondition });
// $: setInitialSize(height, $widgetColumnBarHeight);
$: setInitialSize(height, $widgetColumnBarHeight);
// $: if (storageName && $widgetColumnBarHeight > 0) {
// setLocalStorage(storageName, { relativeHeight: size / $widgetColumnBarHeight, visible });
// }
$: if (storageName && $widgetColumnBarHeight > 0) {
setLocalStorage(storageName, { relativeHeight: size / $widgetColumnBarHeight, visible });
}
function setInitialSize(initialSize, parentHeight) {
if (storageName) {
const storage = getLocalStorage(storageName);
if (storage) {
size = parentHeight * storage.relativeHeight;
return;
}
}
if (_.isString(initialSize) && initialSize.endsWith('px')) size = parseInt(initialSize.slice(0, -2));
else if (_.isString(initialSize) && initialSize.endsWith('%'))
size = (parentHeight * parseFloat(initialSize.slice(0, -1))) / 100;
else size = parentHeight / 3;
}
let visible =
storageName && getLocalStorage(storageName) && getLocalStorage(storageName).visible != null
? getLocalStorage(storageName).visible
: !collapsed;
$: collapsible = $dynamicProps.visibleItemsCount != 1 || !visible;
$: computed = $widgetColumnBarComputed[name] || ({} as WidgetBarComputedProps);
</script>
{#if !skip && positiveCondition}
<WidgetTitle
clickable={collapsible}
on:click={collapsible ? () => (visible = !visible) : null}
clickable={computed.clickableTitle}
on:click={computed.clickableTitle ? () => toggleWidgetCollapse(name) : null}
data-testid={$$props['data-testid']}
{onClose}>{title}</WidgetTitle
>
{#if visible}
{#if !computed.collapsed}
<div
class="wrapper"
style={$dynamicProps.splitterVisible ? `height:${size}px` : 'flex: 1 1 0'}
style={computed.splitterVisible ? `height:${computed.contentHeight}px` : 'flex: 1 1 0'}
data-testid={$$props['data-testid'] ? `${$$props['data-testid']}_content` : undefined}
>
<slot />
</div>
{#if $dynamicProps.splitterVisible}
<div class="vertical-split-handle" use:splitterDrag={'clientY'} on:resizeSplitter={e => (size += e.detail)} />
{#if computed.splitterVisible}
<div
class="vertical-split-handle"
use:splitterDrag={'clientY'}
on:resizeSplitter={e => widgetResizeItem(name, e.detail)}
/>
{/if}
{/if}
{/if}
@@ -4,7 +4,6 @@
import DatabaseWidget from './DatabaseWidget.svelte';
import FilesWidget from './FilesWidget.svelte';
import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte';
import AdminMenuWidget from './AdminMenuWidget.svelte';
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
@@ -29,9 +28,6 @@
{#if $visibleSelectedWidget == 'plugins' && hasPermission('widgets/plugins')}
<PluginsWidget />
{/if}
{#if $visibleSelectedWidget == 'cell-data' && hasPermission('widgets/cell-data')}
<CellDataWidget />
{/if}
{#if $visibleSelectedWidget == 'admin' && hasPermission('widgets/admin')}
<AdminMenuWidget />
{/if}
@@ -72,11 +72,6 @@
// name: 'plugins',
// title: 'Extensions & Plugins',
// },
{
icon: 'icon cell-data',
name: 'cell-data',
title: _t('widgets.selectedCellDataDetailView', { defaultMessage: 'Selected cell data detail view' }),
},
{
name: 'cloud-public',
title: _t('widgets.dbgateCloud', { defaultMessage: 'DbGate Cloud' }),
@@ -4,6 +4,7 @@
export let hideContent = false;
export let fixedWidth = 0;
export let skipDefineWidth = false;
export let flexContainer = false;
export function scrollTop() {
domDiv.scrollTop = 0;
@@ -14,6 +15,7 @@
on:drop
bind:this={domDiv}
class:hideContent
class:flexContainer
class:leftFixedWidth={!fixedWidth && !skipDefineWidth}
data-testid={$$props['data-testid']}
style:width={fixedWidth ? `${fixedWidth}px` : undefined}
@@ -35,4 +37,8 @@
div.hideContent {
visibility: hidden;
}
div.flexContainer {
display: flex;
}
</style>
@@ -6,6 +6,56 @@ const lineReader = require('line-reader');
let dbgateApi;
class StripUtf8BomTransform extends stream.Transform {
constructor(options) {
super(options);
this._checkedBOM = false;
this._pending = Buffer.alloc(0); // store initial bytes until we know if BOM is present
}
_transform(chunk, encoding, callback) {
if (this._checkedBOM) {
// We already handled BOM decision, just pass through
this.push(chunk);
return callback();
}
// Accumulate into pending until we can decide
this._pending = Buffer.concat([this._pending, chunk]);
if (this._pending.length < 3) {
// Still don't know if it's BOM or not (need at least 3 bytes)
return callback();
}
// Now we can check the first 3 bytes
const BOM = [0xef, 0xbb, 0xbf];
const hasBom = this._pending[0] === BOM[0] && this._pending[1] === BOM[1] && this._pending[2] === BOM[2];
if (hasBom) {
// Drop the BOM, push the rest
this.push(this._pending.slice(3));
} else {
// No BOM, push everything as-is
this.push(this._pending);
}
this._pending = Buffer.alloc(0);
this._checkedBOM = true;
callback();
}
_flush(callback) {
// Stream ended but we never had enough bytes to decide (length < 3)
if (!this._checkedBOM && this._pending.length > 0) {
// If it's less than 3 bytes, it can't be a UTF-8 BOM, so just pass it through
this.push(this._pending);
}
this._pending = Buffer.alloc(0);
callback();
}
}
function readFirstLine(file) {
return new Promise((resolve, reject) => {
lineReader.open(file, (err, reader) => {
@@ -95,7 +145,7 @@ async function reader({ fileName, encoding = 'utf-8', header = true, delimiter,
});
const fileStream = fs.createReadStream(downloadedFile, encoding);
const csvPrepare = new CsvPrepareStream({ header });
return [fileStream, csvStream, csvPrepare];
return [fileStream, new StripUtf8BomTransform(), csvStream, csvPrepare];
// fileStream.pipe(csvStream);
// csvStream.pipe(csvPrepare);
// return csvPrepare;
@@ -46,6 +46,8 @@ const dialect = {
dropReferencesWhenDropTable: false,
requireStandaloneSelectForScopeIdentity: true,
dropColumnDependencies: ['dependencies'],
columnProperties: {
columnComment: true,
isUnsigned: true,
@@ -64,6 +64,21 @@ class Dumper extends SqlDumper {
this.putCmd('^alter ^table %f ^rename ^column %i ^to %i', column, column.columnName, newcol);
}
createForeignKeyFore(fk) {
if (fk.constraintName != null && !this.dialect.anonymousForeignKey) {
this.put('^constraint %i ', fk.constraintName);
}
this.put(
'^foreign ^key (%,i) ^references %f (%,i)',
fk.columns.map(x => x.columnName),
{ schemaName: fk.refSchemaName, pureName: fk.refTableName },
fk.columns.map(x => x.refColumnName)
);
if (fk.deleteAction && fk.deleteAction.toUpperCase() !== 'NO ACTION') {
this.put(' ^on ^delete %k', fk.deleteAction);
}
}
// dropTable(obj, options = {}) {
// this.put('^drop ^table');
// if (options.testIfExists) this.put(' ^if ^exists');
@@ -264,7 +264,15 @@ const driver = {
async listDatabases(dbhan) {
const info = await this.info(dbhan);
return _.range(16).map((index) => ({ name: `db${index}`, extInfo: info[`db${index}`], sortOrder: index }));
let databaseCount = 16;
try {
const configResult = await dbhan.client.config('GET', 'databases');
if (Array.isArray(configResult) && configResult.length >= 2) {
databaseCount = parseInt(configResult[1], 10) || 16;
}
} catch {}
return _.range(databaseCount).map((index) => ({ name: `db${index}`, extInfo: info[`db${index}`], sortOrder: index }));
},
async scanKeys(dbhan, pattern, cursor = 0, count) {
@@ -129,6 +129,7 @@ class Analyser extends DatabaseAnalyser {
updateAction: fkcol.on_update,
deleteAction: fkcol.on_delete,
constraintName: `FK_${tableName}_${fkcol.id}`,
constraintType: 'foreignKey',
};
return fk;
});
+4
View File
@@ -8,6 +8,10 @@ on:
- 'feature/**'
- hotfix/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
e2e-tests:
runs-on: ubuntu-latest
+1 -1
View File
@@ -7,7 +7,7 @@ checkout-and-merge-pro:
repository: dbgate/dbgate-pro
token: ${{ secrets.GH_TOKEN }}
path: dbgate-pro
ref: ae1fcf6e61c6f7dfbb21005daa259c68e899a80a
ref: e5234ea5bb21330ac7d31127e0fb5e2fd5e8b0a5
- name: Merge dbgate/dbgate-pro
run: |
mkdir ../dbgate-pro
+4
View File
@@ -8,6 +8,10 @@ on:
- 'feature/**'
- hotfix/**
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
all-tests:
runs-on: ubuntu-latest