Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5ebc01978 | |||
| f29b468fc1 | |||
| e796fbb990 | |||
| 37a122c981 | |||
| 4f426a73f6 | |||
| 1bcb74cd85 | |||
| cd88c8de78 | |||
| eee288b45b | |||
| 797cb7615d | |||
| ca4667ff1e | |||
| 66d1143ca0 | |||
| f310916c76 | |||
| 5d3d8ab932 | |||
| fd91c18460 | |||
| 2a12c04518 | |||
| d08cae6fa3 | |||
| d7f9de1881 | |||
| 962190cc57 | |||
| 4527866276 | |||
| 088dfcd4dc | |||
| 6c317b6e64 | |||
| 6b66c273b4 | |||
| 60f31008c0 | |||
| 078f74db97 | |||
| a0b025cf59 | |||
| bc695f5af9 | |||
| 9685e63b09 | |||
| 142791360c | |||
| e004ed2f4b | |||
| 23ed487252 | |||
| efefec3c20 | |||
| 3d2ad1cb9b | |||
| 90d3016938 | |||
| 438f9fc94d | |||
| 82ec88cc2f | |||
| 149611041e | |||
| b12c79462e | |||
| fbf34fb730 | |||
| e1fe3eb710 | |||
| 76ae2e0e5a | |||
| a57063adf7 | |||
| ff0157e624 | |||
| af9701feb8 | |||
| 93c1f31588 | |||
| 1964e54476 | |||
| 4682255d5f | |||
| a503898b21 | |||
| 21352dae07 | |||
| 8470c7ac6b | |||
| 28aa86f0aa | |||
| 3ed214269a | |||
| a71129df4b | |||
| de6acfa1ce | |||
| ccf075dc65 | |||
| 1d8ac3cf86 | |||
| 7a8ff89c5c | |||
| eda70def2a | |||
| 08fd75edc7 | |||
| 15ea53864f | |||
| 377cd64556 | |||
| b37744d574 | |||
| a7f21fe0c6 | |||
| 955ca99cf3 | |||
| 98f5bb4124 | |||
| b3943f005d | |||
| 8d4178b984 | |||
| 2a88ed38c4 | |||
| 52dce7dfd3 | |||
| 6ebee92542 | |||
| 1b5646f526 | |||
| 7024e4b40d | |||
| bc2e27d7da | |||
| 189da2bfe2 | |||
| 12e6afbaad | |||
| 142ebe3d27 | |||
| 7579f6e42a | |||
| 38c25cae74 | |||
| 408496eb7c | |||
| 4d61c74a8b | |||
| 190c610466 | |||
| 85b7e3ebe3 | |||
| d220525ac7 | |||
| 5e4a631ff2 | |||
| 9099ce42b9 | |||
| df226fea22 | |||
| 851d2e9151 | |||
| 89121a2608 | |||
| 23cf264d4d | |||
| b3130225b5 | |||
| 65512defed | |||
| 3b1c8748f1 | |||
| aba660eddb | |||
| 137eac7dbf | |||
| fdbd08f511 | |||
| ace1cec1f6 | |||
| fa5fda0c3b | |||
| 251609e274 | |||
| c0287e49d8 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
Vendored
+46
@@ -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
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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>
|
||||
@@ -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'])}
|
||||
/>
|
||||
|
||||
@@ -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);
|
||||
// }
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }));
|
||||
}}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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={() =>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -129,6 +129,7 @@
|
||||
|
||||
<ToolStripCommandButton command="dataGrid.refresh" />
|
||||
<ToolStripExportButton {quickExportHandlerRef} />
|
||||
<ToolStripCommandButton command="dataGrid.toggleCellDataView" hideDisabled />
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
{/if}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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> </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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -8,6 +8,10 @@ on:
|
||||
- 'feature/**'
|
||||
- hotfix/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
e2e-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,6 +8,10 @@ on:
|
||||
- 'feature/**'
|
||||
- hotfix/**
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
all-tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
Reference in New Issue
Block a user