diff --git a/CHANGELOG.md b/CHANGELOG.md index f78fe074f..921e1835a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,23 @@ Builds: - linux - application for linux - win - application for Windows +### 5.0.8 +- ADDED: SQL Server - support using domain logins under Linux and Mac #305 +- ADDED: Permissions for connections #318 +- ADDED: Ability to change editor front #308 +- ADDED: Custom expression in query designer #306 +- ADDED: OR conditions in query designer #321 +- ADDED: Ability to configure settings view environment variables #304 + +### 5.0.7 +- FIXED: Fixed some problems with SSH tunnel (upgraded SSH client) #315 +- FIXED: Fixed MognoDB executing find query #312 +- ADDED: Interval filters for date/time columns #311 +- ADDED: Ability to clone rows #309 +- ADDED: connecting option Trust server certificate for SQL Server #305 +- ADDED: Autorefresh, reload table every x second #303 +- FIXED(app): Changing editor theme and font size in Editor Themes #300 + ### 5.0.6 - ADDED: Search in columns - CHANGED: Upgraded mongodb driver diff --git a/package.json b/package.json index 42c9040dc..ff4b11d4d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "private": true, - "version": "5.0.7-beta.4", + "version": "5.0.8", "name": "dbgate-all", "workspaces": [ "packages/*", diff --git a/packages/api/env/portal/.env b/packages/api/env/portal/.env index 6d85d69ec..578102633 100644 --- a/packages/api/env/portal/.env +++ b/packages/api/env/portal/.env @@ -48,4 +48,15 @@ PASSWORD_relational=relational ENGINE_relational=mariadb@dbgate-plugin-mysql READONLY_relational=1 +# SETTINGS_dataGrid.showHintColumns=1 + # docker run -p 3000:3000 -e CONNECTIONS=mongo -e URL_mongo=mongodb://localhost:27017 -e ENGINE_mongo=mongo@dbgate-plugin-mongo -e LABEL_mongo=mongo dbgate/dbgate:beta + +# LOGINS=x,y +# LOGIN_PASSWORD_x=x +# LOGIN_PASSWORD_y=LOGIN_PASSWORD_y +# LOGIN_PERMISSIONS_x=~* +# LOGIN_PERMISSIONS_y=~* + +# PERMISSIONS=~*,connections/relational +# PERMISSIONS=~* diff --git a/packages/api/src/controllers/config.js b/packages/api/src/controllers/config.js index f96877d5d..34e3e3c8b 100644 --- a/packages/api/src/controllers/config.js +++ b/packages/api/src/controllers/config.js @@ -29,7 +29,7 @@ module.exports = { async get(_params, req) { const logins = getLogins(); const login = logins ? logins.find(x => x.login == (req.auth && req.auth.user)) : null; - const permissions = login ? login.permissions : null; + const permissions = login ? login.permissions : process.env.PERMISSIONS; return { runAsPortal: !!connections.portalConnections, @@ -73,6 +73,14 @@ module.exports = { // res['app.useNativeMenu'] = os.platform() == 'darwin' ? true : false; res['app.useNativeMenu'] = false; } + for (const envVar in process.env) { + if (envVar.startsWith('SETTINGS_')) { + const key = envVar.substring('SETTINGS_'.length); + if (!res[key]) { + res[key] = process.env[envVar]; + } + } + } return res; }, diff --git a/packages/api/src/controllers/connections.js b/packages/api/src/controllers/connections.js index 4c64177de..7aad6ebb1 100644 --- a/packages/api/src/controllers/connections.js +++ b/packages/api/src/controllers/connections.js @@ -13,6 +13,7 @@ const JsonLinesDatabase = require('../utility/JsonLinesDatabase'); const processArgs = require('../utility/processArgs'); const { safeJsonParse } = require('dbgate-tools'); const platformInfo = require('../utility/platformInfo'); +const { connectionHasPermission, testConnectionPermission } = require('../utility/hasPermission'); function getNamedArgs() { const res = {}; @@ -165,12 +166,12 @@ module.exports = { }, list_meta: true, - async list() { + async list(_params, req) { if (portalConnections) { if (platformInfo.allowShellConnection) return portalConnections; - return portalConnections.map(maskConnection); + return portalConnections.map(maskConnection).filter(x => connectionHasPermission(x, req)); } - return this.datastore.find(); + return (await this.datastore.find()).filter(x => connectionHasPermission(x, req)); }, test_meta: true, @@ -217,16 +218,18 @@ module.exports = { }, update_meta: true, - async update({ _id, values }) { + async update({ _id, values }, req) { if (portalConnections) return; + testConnectionPermission(_id, req); const res = await this.datastore.patch(_id, values); socket.emitChanged('connection-list-changed'); return res; }, updateDatabase_meta: true, - async updateDatabase({ conid, database, values }) { + async updateDatabase({ conid, database, values }, req) { if (portalConnections) return; + testConnectionPermission(conid, req); const conn = await this.datastore.get(conid); let databases = (conn && conn.databases) || []; if (databases.find(x => x.name == database)) { @@ -242,8 +245,9 @@ module.exports = { }, delete_meta: true, - async delete(connection) { + async delete(connection, req) { if (portalConnections) return; + testConnectionPermission(connection, req); const res = await this.datastore.remove(connection._id); socket.emitChanged('connection-list-changed'); return res; @@ -260,7 +264,8 @@ module.exports = { }, get_meta: true, - async get({ conid }) { + async get({ conid }, req) { + testConnectionPermission(conid, req); return this.getCore({ conid, mask: true }); }, diff --git a/packages/api/src/controllers/databaseConnections.js b/packages/api/src/controllers/databaseConnections.js index 1078d4416..6be1a25c7 100644 --- a/packages/api/src/controllers/databaseConnections.js +++ b/packages/api/src/controllers/databaseConnections.js @@ -26,6 +26,7 @@ const generateDeploySql = require('../shell/generateDeploySql'); const { createTwoFilesPatch } = require('diff'); const diff2htmlPage = require('../utility/diff2htmlPage'); const processArgs = require('../utility/processArgs'); +const { testConnectionPermission } = require('../utility/hasPermission'); module.exports = { /** @type {import('dbgate-types').OpenedDatabaseConnection[]} */ @@ -130,7 +131,8 @@ module.exports = { }, queryData_meta: true, - async queryData({ conid, database, sql }) { + async queryData({ conid, database, sql }, req) { + testConnectionPermission(conid, req); console.log(`Processing query, conid=${conid}, database=${database}, sql=${sql}`); const opened = await this.ensureOpened(conid, database); // if (opened && opened.status && opened.status.name == 'error') { @@ -141,14 +143,16 @@ module.exports = { }, sqlSelect_meta: true, - async sqlSelect({ conid, database, select }) { + async sqlSelect({ conid, database, select }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'sqlSelect', select }); return res; }, runScript_meta: true, - async runScript({ conid, database, sql }) { + async runScript({ conid, database, sql }, req) { + testConnectionPermission(conid, req); console.log(`Processing script, conid=${conid}, database=${database}, sql=${sql}`); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'runScript', sql }); @@ -156,13 +160,15 @@ module.exports = { }, collectionData_meta: true, - async collectionData({ conid, database, options }) { + async collectionData({ conid, database, options }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'collectionData', options }); return res.result || null; }, - async loadDataCore(msgtype, { conid, database, ...args }) { + async loadDataCore(msgtype, { conid, database, ...args }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype, ...args }); if (res.errorMessage) { @@ -176,32 +182,38 @@ module.exports = { }, loadKeys_meta: true, - async loadKeys({ conid, database, root, filter }) { + async loadKeys({ conid, database, root, filter }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('loadKeys', { conid, database, root, filter }); }, exportKeys_meta: true, - async exportKeys({ conid, database, options }) { + async exportKeys({ conid, database, options }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('exportKeys', { conid, database, options }); }, loadKeyInfo_meta: true, - async loadKeyInfo({ conid, database, key }) { + async loadKeyInfo({ conid, database, key }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('loadKeyInfo', { conid, database, key }); }, loadKeyTableRange_meta: true, - async loadKeyTableRange({ conid, database, key, cursor, count }) { + async loadKeyTableRange({ conid, database, key, cursor, count }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('loadKeyTableRange', { conid, database, key, cursor, count }); }, loadFieldValues_meta: true, - async loadFieldValues({ conid, database, schemaName, pureName, field, search }) { + async loadFieldValues({ conid, database, schemaName, pureName, field, search }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('loadFieldValues', { conid, database, schemaName, pureName, field, search }); }, callMethod_meta: true, - async callMethod({ conid, database, method, args }) { + async callMethod({ conid, database, method, args }, req) { + testConnectionPermission(conid, req); return this.loadDataCore('callMethod', { conid, database, method, args }); // const opened = await this.ensureOpened(conid, database); @@ -213,7 +225,8 @@ module.exports = { }, updateCollection_meta: true, - async updateCollection({ conid, database, changeSet }) { + async updateCollection({ conid, database, changeSet }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'updateCollection', changeSet }); if (res.errorMessage) { @@ -225,7 +238,14 @@ module.exports = { }, status_meta: true, - async status({ conid, database }) { + async status({ conid, database }, req) { + if (!conid) { + return { + name: 'error', + message: 'No connection', + }; + } + testConnectionPermission(conid, req); const existing = this.opened.find(x => x.conid == conid && x.database == database); if (existing) { return { @@ -247,7 +267,8 @@ module.exports = { }, ping_meta: true, - async ping({ conid, database }) { + async ping({ conid, database }, req) { + testConnectionPermission(conid, req); let existing = this.opened.find(x => x.conid == conid && x.database == database); if (existing) { @@ -263,7 +284,8 @@ module.exports = { }, refresh_meta: true, - async refresh({ conid, database, keepOpen }) { + async refresh({ conid, database, keepOpen }, req) { + testConnectionPermission(conid, req); if (!keepOpen) this.close(conid, database); await this.ensureOpened(conid, database); @@ -271,7 +293,8 @@ module.exports = { }, syncModel_meta: true, - async syncModel({ conid, database, isFullRefresh }) { + async syncModel({ conid, database, isFullRefresh }, req) { + testConnectionPermission(conid, req); const conn = await this.ensureOpened(conid, database); conn.subprocess.send({ msgtype: 'syncModel', isFullRefresh }); return { status: 'ok' }; @@ -301,13 +324,15 @@ module.exports = { }, disconnect_meta: true, - async disconnect({ conid, database }) { + async disconnect({ conid, database }, req) { + testConnectionPermission(conid, req); await this.close(conid, database, true); return { status: 'ok' }; }, structure_meta: true, - async structure({ conid, database }) { + async structure({ conid, database }, req) { + testConnectionPermission(conid, req); if (conid == '__model') { const model = await importDbModel(database); return model; @@ -324,14 +349,19 @@ module.exports = { }, serverVersion_meta: true, - async serverVersion({ conid, database }) { + async serverVersion({ conid, database }, req) { + if (!conid) { + return null; + } + testConnectionPermission(conid, req); if (!conid) return null; const opened = await this.ensureOpened(conid, database); return opened.serverVersion || null; }, sqlPreview_meta: true, - async sqlPreview({ conid, database, objects, options }) { + async sqlPreview({ conid, database, objects, options }, req) { + testConnectionPermission(conid, req); // wait for structure await this.structure({ conid, database }); @@ -341,7 +371,8 @@ module.exports = { }, exportModel_meta: true, - async exportModel({ conid, database }) { + async exportModel({ conid, database }, req) { + testConnectionPermission(conid, req); const archiveFolder = await archive.getNewArchiveFolder({ database }); await fs.mkdir(path.join(archivedir(), archiveFolder)); const model = await this.structure({ conid, database }); @@ -351,7 +382,8 @@ module.exports = { }, generateDeploySql_meta: true, - async generateDeploySql({ conid, database, archiveFolder }) { + async generateDeploySql({ conid, database, archiveFolder }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid, database); const res = await this.sendRequest(opened, { msgtype: 'generateDeploySql', diff --git a/packages/api/src/controllers/serverConnections.js b/packages/api/src/controllers/serverConnections.js index 28fd52cde..7f6ad2b6d 100644 --- a/packages/api/src/controllers/serverConnections.js +++ b/packages/api/src/controllers/serverConnections.js @@ -7,6 +7,7 @@ const { handleProcessCommunication } = require('../utility/processComm'); const lock = new AsyncLock(); const config = require('./config'); const processArgs = require('../utility/processArgs'); +const { testConnectionPermission } = require('../utility/hasPermission'); module.exports = { opened: [], @@ -90,19 +91,22 @@ module.exports = { }, disconnect_meta: true, - async disconnect({ conid }) { + async disconnect({ conid }, req) { + testConnectionPermission(conid, req); await this.close(conid, true); return { status: 'ok' }; }, listDatabases_meta: true, - async listDatabases({ conid }) { + async listDatabases({ conid }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); return opened.databases; }, version_meta: true, - async version({ conid }) { + async version({ conid }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); return opened.version; }, @@ -132,7 +136,8 @@ module.exports = { }, refresh_meta: true, - async refresh({ conid, keepOpen }) { + async refresh({ conid, keepOpen }, req) { + testConnectionPermission(conid, req); if (!keepOpen) this.close(conid); await this.ensureOpened(conid); @@ -140,7 +145,8 @@ module.exports = { }, createDatabase_meta: true, - async createDatabase({ conid, name }) { + async createDatabase({ conid, name }, req) { + testConnectionPermission(conid, req); const opened = await this.ensureOpened(conid); if (opened.connection.isReadOnly) return false; opened.subprocess.send({ msgtype: 'createDatabase', name }); diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index 54696b5b1..04d28112e 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -4,12 +4,21 @@ const _ = require('lodash'); const userPermissions = {}; function hasPermission(tested, req) { + if (!req) { + // request object not available, allow all + return true; + } const { user } = (req && req.auth) || {}; const key = user || ''; const logins = getLogins(); - if (!userPermissions[key] && logins) { - const login = logins.find(x => x.login == user); - userPermissions[key] = compilePermissions(login ? login.permissions : null); + + if (!userPermissions[key]) { + if (logins) { + const login = logins.find(x => x.login == user); + userPermissions[key] = compilePermissions(login ? login.permissions : null); + } else { + userPermissions[key] = compilePermissions(process.env.PERMISSIONS); + } } return testPermission(tested, userPermissions[key]); } @@ -50,7 +59,26 @@ function getLogins() { return loginsCache; } +function connectionHasPermission(connection, req) { + if (!connection) { + return true; + } + if (_.isString(connection)) { + return hasPermission(`connections/${connection}`, req); + } else { + return hasPermission(`connections/${connection._id}`, req); + } +} + +function testConnectionPermission(connection, req) { + if (!connectionHasPermission(connection, req)) { + throw new Error('Connection permission not granted'); + } +} + module.exports = { hasPermission, getLogins, + connectionHasPermission, + testConnectionPermission, }; diff --git a/packages/api/src/utility/useController.js b/packages/api/src/utility/useController.js index 6ab16c676..8ee431a42 100644 --- a/packages/api/src/utility/useController.js +++ b/packages/api/src/utility/useController.js @@ -47,7 +47,6 @@ module.exports = function useController(app, electron, route, controller) { let method = 'post'; let raw = false; - let rawParams = false; // if (_.isString(meta)) { // method = meta; @@ -55,7 +54,6 @@ module.exports = function useController(app, electron, route, controller) { if (_.isPlainObject(meta)) { method = meta.method; raw = meta.raw; - rawParams = meta.rawParams; } if (raw) { @@ -67,9 +65,7 @@ module.exports = function useController(app, electron, route, controller) { // controller._init_called = true; // } try { - let params = [{ ...req.body, ...req.query }, req]; - if (rawParams) params = [req, res]; - const data = await controller[key](...params); + const data = await controller[key]({ ...req.body, ...req.query }, req); res.json(data); } catch (e) { console.log(e); diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 775df46ed..ae241f989 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -56,7 +56,7 @@ id: 'dataGrid.cloneRows', category: 'Data grid', name: 'Clone rows', - toolbarName: 'Clone', + toolbarName: 'Clone row(s)', keyText: 'CtrlOrCommand+Shift+C', testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable, onClick: () => getCurrentDataGrid().cloneRows(), diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte index 815338cc4..b879050a8 100644 --- a/packages/web/src/designer/Designer.svelte +++ b/packages/web/src/designer/Designer.svelte @@ -167,8 +167,8 @@ async function detectSize(tables, domTables) { await tick(); const rects = _.values(domTables).map(x => x.getRect()); - const maxX = _.max(rects.map(x => x.right)); - const maxY = _.max(rects.map(x => x.bottom)); + const maxX = rects.length > 0 ? _.max(rects.map(x => x.right)) : 0; + const maxY = rects.length > 0 ? _.max(rects.map(x => x.bottom)) : 0; canvasWidth = Math.max(3000, maxX + 50); canvasHeight = Math.max(3000, maxY + 50); diff --git a/packages/web/src/designer/DesignerQueryDumper.ts b/packages/web/src/designer/DesignerQueryDumper.ts index d25bb2f22..c1930861c 100644 --- a/packages/web/src/designer/DesignerQueryDumper.ts +++ b/packages/web/src/designer/DesignerQueryDumper.ts @@ -8,6 +8,7 @@ import { mergeConditions, Source, ResultField, + Expression, } from 'dbgate-sqltree'; import { EngineDriver } from 'dbgate-types'; import { DesignerInfo, DesignerTableInfo, DesignerReferenceInfo, DesignerJoinType } from './types'; @@ -78,25 +79,27 @@ export class DesignerQueryDumper { return select; } - addConditions(select: Select, tables: DesignerTableInfo[]) { + buildConditionFromFilterField(tables: DesignerTableInfo[], filterField: string, getExpression?: Function): Condition { + const conditions = []; + for (const column of this.designer.columns || []) { - if (!column.filter) continue; - const table = (this.designer.tables || []).find(x => x.designerId == column.designerId); - if (!table) continue; - if (!tables.find(x => x.designerId == table.designerId)) continue; + if (!column[filterField]) continue; + + if (!column.isCustomExpression) { + const table = (this.designer.tables || []).find(x => x.designerId == column.designerId); + if (!table) continue; + if (!tables.find(x => x.designerId == table.designerId)) continue; + } try { - const condition = parseFilter(column.filter, findDesignerFilterType(column, this.designer)); + const condition = parseFilter(column[filterField], findDesignerFilterType(column, this.designer)); if (condition) { - select.where = mergeConditions( - select.where, + conditions.push( _.cloneDeepWith(condition, expr => { - if (expr.exprType == 'placeholder') - return { - exprType: 'column', - columnName: column.columnName, - source: findQuerySource(this.designer, column.designerId), - }; + if (expr.exprType == 'placeholder') { + if (getExpression) return getExpression(column); + return this.getColumnExpression(column); + } }) ); } @@ -105,33 +108,79 @@ export class DesignerQueryDumper { continue; } } + + if (conditions.length == 0) { + return null; + } + + if (conditions.length == 1) { + return conditions[0]; + } + + return { + conditionType: 'and', + conditions, + }; + } + + addConditionsCore(select: Select, tables: DesignerTableInfo[], filterFields, selectField, getExpression?) { + const conditions: Condition[] = _.compact( + filterFields.map(field => this.buildConditionFromFilterField(tables, field, getExpression)) + ); + + if (conditions.length == 0) { + return; + } + if (conditions.length == 0) { + select[selectField] = mergeConditions(select[selectField], conditions[0]); + return; + } + select[selectField] = mergeConditions(select[selectField], { + conditionType: 'or', + conditions, + }); + } + + addConditions(select: Select, tables: DesignerTableInfo[]) { + const additionalFilterCount = this.designer.settings?.additionalFilterCount || 0; + const filterFields = ['filter', ..._.range(additionalFilterCount).map(index => `additionalFilter${index + 1}`)]; + this.addConditionsCore(select, tables, filterFields, 'where'); } addGroupConditions(select: Select, tables: DesignerTableInfo[], selectIsGrouped: boolean) { - for (const column of this.designer.columns || []) { - if (!column.groupFilter) continue; - const table = (this.designer.tables || []).find(x => x.designerId == column.designerId); - if (!table) continue; - if (!tables.find(x => x.designerId == table.designerId)) continue; - - const condition = parseFilter(column.groupFilter, findDesignerFilterType(column, this.designer)); - if (condition) { - select.having = mergeConditions( - select.having, - _.cloneDeepWith(condition, expr => { - if (expr.exprType == 'placeholder') { - return this.getColumnOutputExpression(column, selectIsGrouped); - } - }) - ); - } - } + const additionalGroupFilterCount = this.designer.settings?.additionalGroupFilterCount || 0; + const filterFields = [ + 'groupFilter', + ..._.range(additionalGroupFilterCount).map(index => `additionalGroupFilter${index + 1}`), + ]; + this.addConditionsCore(select, tables, filterFields, 'having', column => + this.getColumnResultField(column, selectIsGrouped) + ); } - getColumnOutputExpression(col, selectIsGrouped): ResultField { + getColumnExpression(col): Expression { const source = findQuerySource(this.designer, col.designerId); + const { columnName, isCustomExpression, customExpression } = col; + + const res: Expression = isCustomExpression + ? { + exprType: 'raw', + sql: customExpression, + } + : { + exprType: 'column', + columnName, + source, + }; + return res; + } + + getColumnResultField(col, selectIsGrouped): ResultField { const { columnName } = col; let { alias } = col; + + const exprCore = this.getColumnExpression(col); + if (selectIsGrouped && !col.isGrouped) { // use aggregate const aggregate = col.aggregate == null || col.aggregate == '---' ? 'MAX' : col.aggregate; @@ -142,20 +191,12 @@ export class DesignerQueryDumper { func: aggregate == 'COUNT DISTINCT' ? 'COUNT' : aggregate, argsPrefix: aggregate == 'COUNT DISTINCT' ? 'DISTINCT' : null, alias, - args: [ - { - exprType: 'column', - columnName, - source, - }, - ], + args: [exprCore], }; } else { return { - exprType: 'column', - columnName, + ...exprCore, alias, - source, }; } } @@ -179,24 +220,21 @@ export class DesignerQueryDumper { } } - const topLevelColumns = (this.designer.columns || []).filter(col => - topLevelTables.find(tbl => tbl.designerId == col.designerId) + const topLevelColumns = (this.designer.columns || []).filter( + col => + topLevelTables.find(tbl => tbl.designerId == col.designerId) || (col.isCustomExpression && col.customExpression) ); const selectIsGrouped = !!topLevelColumns.find(x => x.isGrouped || (x.aggregate && x.aggregate != '---')); const outputColumns = topLevelColumns.filter(x => x.isOutput); if (outputColumns.length == 0) { res.selectAll = true; } else { - res.columns = outputColumns.map(col => this.getColumnOutputExpression(col, selectIsGrouped)); + res.columns = outputColumns.map(col => this.getColumnResultField(col, selectIsGrouped)); } const groupedColumns = topLevelColumns.filter(x => x.isGrouped); if (groupedColumns.length > 0) { - res.groupBy = groupedColumns.map(col => ({ - exprType: 'column', - columnName: col.columnName, - source: findQuerySource(this.designer, col.designerId), - })); + res.groupBy = groupedColumns.map(col => this.getColumnExpression(col)); } const orderColumns = _.sortBy( @@ -205,10 +243,8 @@ export class DesignerQueryDumper { ); if (orderColumns.length > 0) { res.orderBy = orderColumns.map(col => ({ - exprType: 'column', + ...this.getColumnExpression(col), direction: col.sortOrder < 0 ? 'DESC' : 'ASC', - columnName: col.columnName, - source: findQuerySource(this.designer, col.designerId), })); } diff --git a/packages/web/src/designer/types.ts b/packages/web/src/designer/types.ts index 000549de2..d42f2e076 100644 --- a/packages/web/src/designer/types.ts +++ b/packages/web/src/designer/types.ts @@ -31,10 +31,14 @@ export type DesignerColumnInfo = { sortOrder?: number; filter?: string; groupFilter?: string; + isCustomExpression?: boolean; + customExpression?: string; }; export type DesignerSettings = { isDistinct?: boolean; + additionalFilterCount?: number; + additionalGroupFilterCount?: number; }; export type DesignerInfo = { diff --git a/packages/web/src/elements/QueryDesignColumns.svelte b/packages/web/src/elements/QueryDesignColumns.svelte index c643d2ec1..d9bc26d4d 100644 --- a/packages/web/src/elements/QueryDesignColumns.svelte +++ b/packages/web/src/elements/QueryDesignColumns.svelte @@ -13,8 +13,11 @@ import SelectField from '../forms/SelectField.svelte'; import TextField from '../forms/TextField.svelte'; import InlineButton from '../buttons/InlineButton.svelte'; + import uuidv1 from 'uuid/v1'; import TableControl from './TableControl.svelte'; + import FormStyledButton from '../buttons/FormStyledButton.svelte'; + import _ from 'lodash'; export let value; export let onChange; @@ -35,8 +38,56 @@ })); }; + const addExpressionColumn = () => { + onChange(current => ({ + ...current, + columns: [...(current.columns || []), { isCustomExpression: true, isOutput: true, designerId: uuidv1() }], + })); + }; + + const addOrCondition = () => { + onChange(current => ({ + ...current, + settings: { + ...current?.settings, + additionalFilterCount: (current?.settings?.additionalFilterCount ?? 0) + 1, + }, + })); + }; + + const removeOrCondition = () => { + onChange(current => ({ + ...current, + settings: { + ...current?.settings, + additionalFilterCount: (current?.settings?.additionalFilterCount ?? 1) - 1, + }, + })); + }; + + const addGroupOrCondition = () => { + onChange(current => ({ + ...current, + settings: { + ...current?.settings, + additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 0) + 1, + }, + })); + }; + + const removeGroupOrCondition = () => { + onChange(current => ({ + ...current, + settings: { + ...current?.settings, + additionalGroupFilterCount: (current?.settings?.additionalGroupFilterCount ?? 1) - 1, + }, + })); + }; + $: columns = value?.columns; $: tables = value?.tables; + $: settings = value?.settings; $: hasGroupedColumn = !!(columns || []).find(x => x.isGrouped); @@ -44,18 +95,49 @@ getTableDisplayName(row, tables) }, { fieldName: 'isOutput', header: 'Output', slot: 0 }, { fieldName: 'alias', header: 'Alias', slot: 1 }, { fieldName: 'isGrouped', header: 'Group by', slot: 2 }, { fieldName: 'aggregate', header: 'Aggregate', slot: 3 }, { fieldName: 'sortOrder', header: 'Sort order', slot: 4 }, - { fieldName: 'filter', header: 'Filter', slot: 5 }, - hasGroupedColumn && { fieldName: 'groupFilter', header: 'Group filter', slot: 6 }, + { fieldName: 'filter', header: 'Filter', slot: 5, props: { filterField: 'filter' } }, + ..._.range(settings?.additionalFilterCount || 0).map(index => ({ + fieldName: `additionalFilter${index + 1}`, + header: `OR Filter ${index + 2}`, + slot: 5, + props: { filterField: `additionalFilter${index + 1}` }, + })), + hasGroupedColumn && { + fieldName: 'groupFilter', + header: 'Group filter', + slot: 5, + props: { filterField: 'groupFilter' }, + }, + ..._.range(hasGroupedColumn ? settings?.additionalGroupFilterCount || 0 : 0).map(index => ({ + fieldName: `additionalGroupFilter${index + 1}`, + header: `OR group filter ${index + 2}`, + slot: 5, + props: { filterField: `additionalGroupFilter${index + 1}` }, + })), { fieldName: 'actions', header: '', slot: 7 }, ]} > + + {#if row.isCustomExpression} + { + changeColumn({ ...row, customExpression: e.target.value }); + }} + /> + {:else} + {row.columnName} + {/if} + + { changeColumn({ ...row, alias: e.target.value }); @@ -86,6 +169,7 @@ {#if !row.isGrouped} { changeColumn({ ...row, aggregate: e.detail }); @@ -97,6 +181,7 @@ { changeColumn({ ...row, sortOrder: parseInt(e.detail) }); @@ -112,21 +197,12 @@ ]} /> - + { - changeColumn({ ...row, filter }); - }} - /> - - - { - changeColumn({ ...row, groupFilter }); + changeColumn({ ...row, [filterField]: filter }); }} /> @@ -134,6 +210,17 @@ removeColumn(row)}>Remove + + + {#if settings?.additionalFilterCount > 0} + + {/if} + {#if hasGroupedColumn} + + {/if} + {#if hasGroupedColumn && settings?.additionalGroupFilterCount > 0} + + {/if} \ No newline at end of file + diff --git a/packages/web/src/elements/TableControl.svelte b/packages/web/src/elements/TableControl.svelte index d22a01524..1542b72cd 100644 --- a/packages/web/src/elements/TableControl.svelte +++ b/packages/web/src/elements/TableControl.svelte @@ -4,6 +4,7 @@ header: string; component?: any; getProps?: any; + props?: any; formatter?: any; slot?: number; isHighlighted?: Function; @@ -25,6 +26,7 @@ export let clickable = false; export let disableFocusOutline = false; export let emptyMessage = null; + export let noCellPadding = false; export let domTable = undefined; @@ -77,21 +79,24 @@ }} > {#each columnList as col} - + {@const rowProps = { ...col.props, ...(col.getProps ? col.getProps(row) : null) }} + {#if col.component} - + {:else if col.formatter} {col.formatter(row)} {:else if col.slot != null} {#if col.slot == -1} - {:else if col.slot == 0} - {:else if col.slot == 1} - {:else if col.slot == 2} - {:else if col.slot == 3} - {:else if col.slot == 4} - {:else if col.slot == 5} - {:else if col.slot == 6} - {:else if col.slot == 7} + {:else if col.slot == 0} + {:else if col.slot == 1} + {:else if col.slot == 2} + {:else if col.slot == 3} + {:else if col.slot == 4} + {:else if col.slot == 5} + {:else if col.slot == 6} + {:else if col.slot == 7} + {:else if col.slot == 8} + {:else if col.slot == 9} {/if} {:else} {row[col.fieldName] || ''} @@ -136,6 +141,9 @@ } tbody td { border: 1px solid var(--theme-border); + } + + tbody td:not(.noCellPadding) { padding: 5px; } diff --git a/packages/web/src/query/AceEditor.svelte b/packages/web/src/query/AceEditor.svelte index e4527485c..abcb000d3 100644 --- a/packages/web/src/query/AceEditor.svelte +++ b/packages/web/src/query/AceEditor.svelte @@ -114,7 +114,13 @@ import 'ace-builds/src-noconflict/theme-tomorrow_night'; import 'ace-builds/src-noconflict/theme-twilight'; - import { currentDropDownMenu, currentEditorFontSize, currentEditorTheme, currentThemeDefinition } from '../stores'; + import { + currentDropDownMenu, + currentEditorFontSize, + currentEditorFont, + currentEditorTheme, + currentThemeDefinition, + } from '../stores'; import _ from 'lodash'; import { handleCommandKeyDown } from '../commands/CommandListener.svelte'; import resizeObserver from '../utility/resizeObserver'; @@ -223,12 +229,15 @@ } } - $: watchOptions(options); - function watchOptions(newOption: any) { + $: watchOptions(options, $currentEditorFont); + function watchOptions(newOption: any, fontFamily) { if (editor) { editor.setOptions({ ...stdOptions, ...newOption, + fontFamily: fontFamily || 'Menlo, Monaco, Ubuntu Mono, Consolas, source-code-pro, monospace', + // fontFamily: 'tahoma,Menlo', + // fontSize: '10pt', }); } } diff --git a/packages/web/src/settings/ConnectionDriverFields.svelte b/packages/web/src/settings/ConnectionDriverFields.svelte index cc5b04d77..997b78c7d 100644 --- a/packages/web/src/settings/ConnectionDriverFields.svelte +++ b/packages/web/src/settings/ConnectionDriverFields.svelte @@ -164,6 +164,10 @@ /> {/if} +{#if driver?.showConnectionField('windowsDomain', $values)} + +{/if} + {#if driver?.showConnectionField('isReadOnly', $values)} {/if} diff --git a/packages/web/src/settings/SettingsModal.svelte b/packages/web/src/settings/SettingsModal.svelte index 8c0dd5d4b..05c9f6d7f 100644 --- a/packages/web/src/settings/SettingsModal.svelte +++ b/packages/web/src/settings/SettingsModal.svelte @@ -127,7 +127,7 @@ ORDER BY
Editor theme
-
+
-
+
+ +
+ +
diff --git a/packages/web/src/stores.ts b/packages/web/src/stores.ts index 4f24fe84b..1407396e5 100644 --- a/packages/web/src/stores.ts +++ b/packages/web/src/stores.ts @@ -68,6 +68,7 @@ export const currentEditorTheme = getElectron() export const currentEditorFontSize = getElectron() ? writableSettingsValue(null, 'currentEditorFontSize') : writableWithStorage(null, 'currentEditorFontSize'); +export const currentEditorFont = writableSettingsValue(null, 'editor.fontFamily'); export const activeTabId = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)?.tabid); export const activeTab = derived([openedTabs], ([$openedTabs]) => $openedTabs.find(x => x.selected)); export const recentDatabases = writableWithStorage([], 'recentDatabases'); diff --git a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js index 67f6a4ed6..56b467530 100644 --- a/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js +++ b/plugins/dbgate-plugin-mssql/src/backend/tediousDriver.js @@ -22,7 +22,7 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) { return res; } -async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate }) { +async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate, windowsDomnain }) { return new Promise((resolve, reject) => { const connectionOptions = { encrypt: !!ssl, @@ -43,10 +43,11 @@ async function tediousConnect({ server, port, user, password, database, ssl, tru server, authentication: { - type: 'default', + type: windowsDomnain ? 'ntlm' : 'default', options: { userName: user, password: password, + ...(windowsDomnain ? { domain: windowsDomnain } : {}), }, }, diff --git a/plugins/dbgate-plugin-mssql/src/frontend/driver.js b/plugins/dbgate-plugin-mssql/src/frontend/driver.js index f696916cc..14711e1fe 100644 --- a/plugins/dbgate-plugin-mssql/src/frontend/driver.js +++ b/plugins/dbgate-plugin-mssql/src/frontend/driver.js @@ -127,13 +127,16 @@ const driver = { ['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes( field ) || - (field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi'), + (field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi') || + (field == 'windowsDomain' && values.authType != 'sql' && values.authType != 'sspi'), + // (field == 'useDatabaseUrl' && values.authType != 'sql' && values.authType != 'sspi') getQuerySplitterOptions: () => mssqlSplitterOptions, engine: 'mssql@dbgate-plugin-mssql', title: 'Microsoft SQL Server', defaultPort: 1433, defaultAuthTypeName: 'tedious', + // databaseUrlPlaceholder: 'e.g. server=localhost&authentication.type=default&authentication.type.user=myuser&authentication.type.password=pwd&options.database=mydb', getNewObjectTemplates() { return [