mirror of
https://github.com/dbgate/dbgate
synced 2024-11-08 04:35:58 +00:00
Merge branch 'master' into develop
This commit is contained in:
commit
2b4d5c026e
@ -8,6 +8,13 @@ Builds:
|
||||
- linux - application for linux
|
||||
- win - application for Windows
|
||||
|
||||
### 5.0.6
|
||||
- ADDED: Search in columns
|
||||
- CHANGED: Upgraded mongodb driver
|
||||
- ADDED: Ability to reset view, when data load fails
|
||||
- FIXED: Filtering works for complex types (geography, xml under MSSQL)
|
||||
- FIXED: Fixed some NPM package problems
|
||||
|
||||
### 5.0.5
|
||||
- ADDED: Visualisation geographics objects on map #288
|
||||
- ADDED: Support for native SQL as default value inside yaml files #296
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"private": true,
|
||||
"version": "5.0.6-beta.6",
|
||||
"version": "5.0.7-beta.4",
|
||||
"name": "dbgate-all",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
@ -10,6 +10,10 @@
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:app": "cd app && yarn start",
|
||||
"start:api:debug": "cross-env DEBUG=* yarn workspace dbgate-api start",
|
||||
"start:app:debug": "cd app && cross-env DEBUG=* yarn start",
|
||||
"start:api:debug:ssh": "cross-env DEBUG=ssh yarn workspace dbgate-api start",
|
||||
"start:app:debug:ssh": "cd app && cross-env DEBUG=ssh yarn start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:singledb": "yarn workspace dbgate-api start:singledb",
|
||||
"start:web": "yarn workspace dbgate-web dev",
|
||||
|
@ -28,6 +28,7 @@
|
||||
"dbgate-query-splitter": "^4.9.0",
|
||||
"dbgate-sqltree": "^5.0.0-alpha.1",
|
||||
"dbgate-tools": "^5.0.0-alpha.1",
|
||||
"debug": "^4.3.4",
|
||||
"diff": "^5.0.0",
|
||||
"diff2html": "^3.4.13",
|
||||
"eslint": "^6.8.0",
|
||||
@ -45,9 +46,9 @@
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-ssh-forward": "^0.7.2",
|
||||
"portfinder": "^1.0.28",
|
||||
"simple-encryptor": "^4.0.0",
|
||||
"ssh2": "^1.11.0",
|
||||
"tar": "^6.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
|
@ -59,13 +59,10 @@ module.exports = {
|
||||
|
||||
getSettings_meta: true,
|
||||
async getSettings() {
|
||||
try {
|
||||
return this.fillMissingSettings(
|
||||
JSON.parse(await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' }))
|
||||
);
|
||||
} catch (err) {
|
||||
return this.fillMissingSettings({});
|
||||
}
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
return await this.loadSettings();
|
||||
});
|
||||
return res;
|
||||
},
|
||||
|
||||
fillMissingSettings(value) {
|
||||
@ -79,12 +76,21 @@ module.exports = {
|
||||
return res;
|
||||
},
|
||||
|
||||
async loadSettings() {
|
||||
try {
|
||||
const settingsText = await fs.readFile(path.join(datadir(), 'settings.json'), { encoding: 'utf-8' });
|
||||
return this.fillMissingSettings(JSON.parse(settingsText));
|
||||
} catch (err) {
|
||||
return this.fillMissingSettings({});
|
||||
}
|
||||
},
|
||||
|
||||
updateSettings_meta: true,
|
||||
async updateSettings(values, req) {
|
||||
if (!hasPermission(`settings/change`, req)) return false;
|
||||
|
||||
const res = await lock.acquire('update', async () => {
|
||||
const currentValue = await this.getSettings();
|
||||
const res = await lock.acquire('settings', async () => {
|
||||
const currentValue = await this.loadSettings();
|
||||
try {
|
||||
const updated = {
|
||||
...currentValue,
|
||||
|
@ -1,8 +1,8 @@
|
||||
const fs = require('fs-extra');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const childProcessChecker = require('../utility/childProcessChecker');
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const { SSHConnection } = require('../utility/SSHConnection');
|
||||
|
||||
async function getSshConnection(connection) {
|
||||
const sshConfig = {
|
||||
@ -35,6 +35,8 @@ async function handleStart({ connection, tunnelConfig }) {
|
||||
tunnelConfig,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error creating SSH tunnel connection:', err.message);
|
||||
|
||||
process.send({
|
||||
msgtype: 'error',
|
||||
connection,
|
||||
|
251
packages/api/src/utility/SSHConnection.js
Normal file
251
packages/api/src/utility/SSHConnection.js
Normal file
@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2018 Stocard GmbH.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
const { Client } = require('ssh2');
|
||||
const net = require('net');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const debug = require('debug');
|
||||
|
||||
// interface Options {
|
||||
// username?: string;
|
||||
// password?: string;
|
||||
// privateKey?: string | Buffer;
|
||||
// agentForward?: boolean;
|
||||
// bastionHost?: string;
|
||||
// passphrase?: string;
|
||||
// endPort?: number;
|
||||
// endHost: string;
|
||||
// agentSocket?: string;
|
||||
// skipAutoPrivateKey?: boolean;
|
||||
// noReadline?: boolean;
|
||||
// }
|
||||
|
||||
// interface ForwardingOptions {
|
||||
// fromPort: number;
|
||||
// toPort: number;
|
||||
// toHost?: string;
|
||||
// }
|
||||
|
||||
class SSHConnection {
|
||||
constructor(options) {
|
||||
this.options = options;
|
||||
this.debug = debug('ssh');
|
||||
this.connections = [];
|
||||
this.isWindows = process.platform === 'win32';
|
||||
if (!options.username) {
|
||||
this.options.username = process.env['SSH_USERNAME'] || process.env['USER'];
|
||||
}
|
||||
if (!options.endPort) {
|
||||
this.options.endPort = 22;
|
||||
}
|
||||
if (!options.privateKey && !options.agentForward && !options.skipAutoPrivateKey) {
|
||||
const defaultFilePath = path.join(os.homedir(), '.ssh', 'id_rsa');
|
||||
if (fs.existsSync(defaultFilePath)) {
|
||||
this.options.privateKey = fs.readFileSync(defaultFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async shutdown() {
|
||||
this.debug('Shutdown connections');
|
||||
for (const connection of this.connections) {
|
||||
connection.removeAllListeners();
|
||||
connection.end();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
if (this.server) {
|
||||
this.server.close(resolve);
|
||||
}
|
||||
return resolve();
|
||||
});
|
||||
}
|
||||
|
||||
async tty() {
|
||||
const connection = await this.establish();
|
||||
this.debug('Opening tty');
|
||||
await this.shell(connection);
|
||||
}
|
||||
|
||||
async executeCommand(command) {
|
||||
const connection = await this.establish();
|
||||
this.debug('Executing command "%s"', command);
|
||||
await this.shell(connection, command);
|
||||
}
|
||||
|
||||
async shell(connection, command) {
|
||||
return new Promise((resolve, reject) => {
|
||||
connection.shell((err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
stream
|
||||
.on('close', async () => {
|
||||
stream.end();
|
||||
process.stdin.unpipe(stream);
|
||||
process.stdin.destroy();
|
||||
connection.end();
|
||||
await this.shutdown();
|
||||
return resolve();
|
||||
})
|
||||
.stderr.on('data', data => {
|
||||
return reject(data);
|
||||
});
|
||||
stream.pipe(process.stdout);
|
||||
|
||||
if (command) {
|
||||
stream.end(`${command}\nexit\n`);
|
||||
} else {
|
||||
process.stdin.pipe(stream);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async establish() {
|
||||
let connection;
|
||||
if (this.options.bastionHost) {
|
||||
connection = await this.connectViaBastion(this.options.bastionHost);
|
||||
} else {
|
||||
connection = await this.connect(this.options.endHost);
|
||||
}
|
||||
return connection;
|
||||
}
|
||||
|
||||
async connectViaBastion(bastionHost) {
|
||||
this.debug('Connecting to bastion host "%s"', bastionHost);
|
||||
const connectionToBastion = await this.connect(bastionHost);
|
||||
return new Promise((resolve, reject) => {
|
||||
connectionToBastion.forwardOut(
|
||||
'127.0.0.1',
|
||||
22,
|
||||
this.options.endHost,
|
||||
this.options.endPort || 22,
|
||||
async (err, stream) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
const connection = await this.connect(this.options.endHost, stream);
|
||||
return resolve(connection);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async connect(host, stream) {
|
||||
this.debug('Connecting to "%s"', host);
|
||||
const connection = new Client();
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const options = {
|
||||
host,
|
||||
port: this.options.endPort,
|
||||
username: this.options.username,
|
||||
password: this.options.password,
|
||||
privateKey: this.options.privateKey,
|
||||
};
|
||||
if (this.options.agentForward) {
|
||||
options['agentForward'] = true;
|
||||
|
||||
// see https://github.com/mscdex/ssh2#client for agents on Windows
|
||||
// guaranteed to give the ssh agent sock if the agent is running (posix)
|
||||
let agentDefault = process.env['SSH_AUTH_SOCK'];
|
||||
if (this.isWindows) {
|
||||
// null or undefined
|
||||
if (agentDefault == null) {
|
||||
agentDefault = 'pageant';
|
||||
}
|
||||
}
|
||||
|
||||
const agentSock = this.options.agentSocket ? this.options.agentSocket : agentDefault;
|
||||
if (agentSock == null) {
|
||||
throw new Error('SSH Agent Socket is not provided, or is not set in the SSH_AUTH_SOCK env variable');
|
||||
}
|
||||
options['agent'] = agentSock;
|
||||
}
|
||||
if (stream) {
|
||||
options['sock'] = stream;
|
||||
}
|
||||
// PPK private keys can be encrypted, but won't contain the word 'encrypted'
|
||||
// in fact they always contain a `encryption` header, so we can't do a simple check
|
||||
options['passphrase'] = this.options.passphrase;
|
||||
const looksEncrypted = this.options.privateKey
|
||||
? this.options.privateKey.toString().toLowerCase().includes('encrypted')
|
||||
: false;
|
||||
if (looksEncrypted && !options['passphrase'] && !this.options.noReadline) {
|
||||
// options['passphrase'] = await this.getPassphrase();
|
||||
}
|
||||
connection.on('ready', () => {
|
||||
this.connections.push(connection);
|
||||
return resolve(connection);
|
||||
});
|
||||
|
||||
connection.on('error', error => {
|
||||
reject(error);
|
||||
});
|
||||
try {
|
||||
connection.connect(options);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// private async getPassphrase() {
|
||||
// return new Promise(resolve => {
|
||||
// const rl = readline.createInterface({
|
||||
// input: process.stdin,
|
||||
// output: process.stdout,
|
||||
// });
|
||||
// rl.question('Please type in the passphrase for your private key: ', answer => {
|
||||
// return resolve(answer);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
async forward(options) {
|
||||
const connection = await this.establish();
|
||||
return new Promise((resolve, reject) => {
|
||||
this.server = net
|
||||
.createServer(socket => {
|
||||
this.debug(
|
||||
'Forwarding connection from "localhost:%d" to "%s:%d"',
|
||||
options.fromPort,
|
||||
options.toHost,
|
||||
options.toPort
|
||||
);
|
||||
connection.forwardOut(
|
||||
'localhost',
|
||||
options.fromPort,
|
||||
options.toHost || 'localhost',
|
||||
options.toPort,
|
||||
(error, stream) => {
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
socket.pipe(stream);
|
||||
stream.pipe(socket);
|
||||
}
|
||||
);
|
||||
})
|
||||
.listen(options.fromPort, 'localhost', () => {
|
||||
return resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { SSHConnection };
|
@ -1,8 +1,5 @@
|
||||
const { SSHConnection } = require('node-ssh-forward');
|
||||
const portfinder = require('portfinder');
|
||||
const fs = require('fs-extra');
|
||||
const { decryptConnection } = require('./crypting');
|
||||
const { getSshTunnel } = require('./sshTunnel');
|
||||
const { getSshTunnelProxy } = require('./sshTunnelProxy');
|
||||
const platformInfo = require('../utility/platformInfo');
|
||||
const connections = require('../controllers/connections');
|
||||
|
279
packages/filterparser/src/datetimeParser.ts
Normal file
279
packages/filterparser/src/datetimeParser.ts
Normal file
@ -0,0 +1,279 @@
|
||||
import P from 'parsimmon';
|
||||
import moment from 'moment';
|
||||
import { FilterType } from './types';
|
||||
import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
|
||||
const compoudCondition = conditionType => conditions => {
|
||||
if (conditions.length == 1) return conditions[0];
|
||||
return {
|
||||
[conditionType]: conditions,
|
||||
};
|
||||
};
|
||||
|
||||
function getTransformCondition(transform: TransformType, value) {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'transform',
|
||||
transform,
|
||||
expr: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const yearCondition = () => value => {
|
||||
return getTransformCondition('YEAR', value);
|
||||
};
|
||||
|
||||
const yearMonthCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
|
||||
};
|
||||
};
|
||||
|
||||
const yearMonthDayCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
getTransformCondition('YEAR', m[1]),
|
||||
getTransformCondition('MONTH', m[2]),
|
||||
getTransformCondition('DAY', m[3]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const yearEdge = edgeFunction => value => {
|
||||
return moment(new Date(parseInt(value), 0, 1))
|
||||
[edgeFunction]('year')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, 1))
|
||||
[edgeFunction]('month')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return moment(new Date(parseInt(m[1]), parseInt(m[2]) - 1, parseInt(m[3])))
|
||||
[edgeFunction]('day')
|
||||
.format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return moment(dateObject)[edgeFunction]('minute').format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteSecondEdge = edgeFunction => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return moment(dateObject)[edgeFunction]('second').format('YYYY-MM-DDTHH:mm:ss.SSS');
|
||||
};
|
||||
|
||||
const createIntervalCondition = (start, end) => {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '>=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: start,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '<=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: end,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createDateIntervalCondition = (start, end) => {
|
||||
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
|
||||
};
|
||||
|
||||
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
|
||||
return createDateIntervalCondition(
|
||||
moment().add(intervalType, diff).startOf(intervalType),
|
||||
moment().add(intervalType, diff).endOf(intervalType)
|
||||
);
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
|
||||
};
|
||||
|
||||
const yearMonthDaySecondCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
|
||||
};
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
operator,
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
});
|
||||
|
||||
const createParser = () => {
|
||||
const langDef = {
|
||||
comma: () => word(','),
|
||||
|
||||
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
yearNumStart: () => P.regexp(/\d\d\d\d/).map(yearEdge('startOf')),
|
||||
yearNumEnd: () => P.regexp(/\d\d\d\d/).map(yearEdge('endOf')),
|
||||
yearMonthStart: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('startOf')),
|
||||
yearMonthEnd: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthEdge('endOf')),
|
||||
yearMonthDayStart: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('startOf')),
|
||||
yearMonthDayEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayEdge('endOf')),
|
||||
yearMonthDayMinuteStart: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('startOf')),
|
||||
yearMonthDayMinuteEnd: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteEdge('endOf')),
|
||||
yearMonthDayMinuteSecondStart: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('startOf')),
|
||||
yearMonthDayMinuteSecondEnd: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDayMinuteSecondEdge('endOf')),
|
||||
|
||||
this: () => word('THIS'),
|
||||
last: () => word('LAST'),
|
||||
next: () => word('NEXT'),
|
||||
week: () => word('WEEK'),
|
||||
month: () => word('MONTH'),
|
||||
year: () => word('YEAR'),
|
||||
|
||||
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
|
||||
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
|
||||
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
|
||||
|
||||
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
|
||||
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
|
||||
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
|
||||
|
||||
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
|
||||
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
|
||||
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
|
||||
|
||||
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
|
||||
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
|
||||
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
|
||||
|
||||
valueStart: r =>
|
||||
P.alt(
|
||||
r.yearMonthDayMinuteSecondStart,
|
||||
r.yearMonthDayMinuteStart,
|
||||
r.yearMonthDayStart,
|
||||
r.yearMonthStart,
|
||||
r.yearNumStart
|
||||
),
|
||||
valueEnd: r =>
|
||||
P.alt(r.yearMonthDayMinuteSecondEnd, r.yearMonthDayMinuteEnd, r.yearMonthDayEnd, r.yearMonthEnd, r.yearNumEnd),
|
||||
|
||||
le: r => word('<=').then(r.valueEnd).map(binaryCondition('<=')),
|
||||
ge: r => word('>=').then(r.valueStart).map(binaryCondition('>=')),
|
||||
lt: r => word('<').then(r.valueStart).map(binaryCondition('<')),
|
||||
gt: r => word('>').then(r.valueEnd).map(binaryCondition('>')),
|
||||
|
||||
element: r =>
|
||||
P.alt(
|
||||
r.yearMonthDaySecond,
|
||||
r.yearMonthDayMinute,
|
||||
r.yearMonthDayNum,
|
||||
r.yearMonthNum,
|
||||
r.yearNum,
|
||||
r.yesterday,
|
||||
r.today,
|
||||
r.tomorrow,
|
||||
r.lastWeek,
|
||||
r.thisWeek,
|
||||
r.nextWeek,
|
||||
r.lastMonth,
|
||||
r.thisMonth,
|
||||
r.nextMonth,
|
||||
r.lastYear,
|
||||
r.thisYear,
|
||||
r.nextYear,
|
||||
r.le,
|
||||
r.lt,
|
||||
r.ge,
|
||||
r.gt
|
||||
).trim(whitespace),
|
||||
factor: r => r.element.sepBy(whitespace).map(compoudCondition('$and')),
|
||||
list: r => r.factor.sepBy(r.comma).map(compoudCondition('$or')),
|
||||
};
|
||||
|
||||
return P.createLanguage(langDef);
|
||||
};
|
||||
|
||||
export const datetimeParser = createParser();
|
@ -5,6 +5,7 @@ import { Condition } from 'dbgate-sqltree';
|
||||
import { TransformType } from 'dbgate-types';
|
||||
import { interpretEscapes, token, word, whitespace } from './common';
|
||||
import { mongoParser } from './mongoParser';
|
||||
import { datetimeParser } from './datetimeParser';
|
||||
|
||||
const binaryCondition = operator => value => ({
|
||||
conditionType: 'binary',
|
||||
@ -67,116 +68,6 @@ const negateCondition = condition => {
|
||||
};
|
||||
};
|
||||
|
||||
function getTransformCondition(transform: TransformType, value) {
|
||||
return {
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'transform',
|
||||
transform,
|
||||
expr: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const yearCondition = () => value => {
|
||||
return getTransformCondition('YEAR', value);
|
||||
};
|
||||
|
||||
const yearMonthCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [getTransformCondition('YEAR', m[1]), getTransformCondition('MONTH', m[2])],
|
||||
};
|
||||
};
|
||||
|
||||
const yearMonthDayCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)/);
|
||||
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
getTransformCondition('YEAR', m[1]),
|
||||
getTransformCondition('MONTH', m[2]),
|
||||
getTransformCondition('DAY', m[3]),
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createIntervalCondition = (start, end) => {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '>=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: start,
|
||||
},
|
||||
},
|
||||
{
|
||||
conditionType: 'binary',
|
||||
operator: '<=',
|
||||
left: {
|
||||
exprType: 'placeholder',
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: end,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
const createDateIntervalCondition = (start, end) => {
|
||||
return createIntervalCondition(start.format('YYYY-MM-DDTHH:mm:ss.SSS'), end.format('YYYY-MM-DDTHH:mm:ss.SSS'));
|
||||
};
|
||||
|
||||
const fixedMomentIntervalCondition = (intervalType, diff) => () => {
|
||||
return createDateIntervalCondition(
|
||||
moment().add(intervalType, diff).startOf(intervalType),
|
||||
moment().add(intervalType, diff).endOf(intervalType)
|
||||
);
|
||||
};
|
||||
|
||||
const yearMonthDayMinuteCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)\s+(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[4];
|
||||
const minute = m[5];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('minute'), moment(dateObject).endOf('minute'));
|
||||
};
|
||||
|
||||
const yearMonthDaySecondCondition = () => value => {
|
||||
const m = value.match(/(\d\d\d\d)-(\d\d?)-(\d\d?)(T|\s+)(\d\d?):(\d\d?):(\d\d?)/);
|
||||
const year = m[1];
|
||||
const month = m[2];
|
||||
const day = m[3];
|
||||
const hour = m[5];
|
||||
const minute = m[6];
|
||||
const second = m[7];
|
||||
const dateObject = new Date(year, month - 1, day, hour, minute, second);
|
||||
|
||||
return createDateIntervalCondition(moment(dateObject).startOf('second'), moment(dateObject).endOf('second'));
|
||||
};
|
||||
|
||||
const createParser = (filterType: FilterType) => {
|
||||
const langDef = {
|
||||
string1: () =>
|
||||
@ -206,13 +97,6 @@ const createParser = (filterType: FilterType) => {
|
||||
|
||||
noQuotedString: () => P.regexp(/[^\s^,^'^"]+/).desc('string unquoted'),
|
||||
|
||||
yearNum: () => P.regexp(/\d\d\d\d/).map(yearCondition()),
|
||||
yearMonthNum: () => P.regexp(/\d\d\d\d-\d\d?/).map(yearMonthCondition()),
|
||||
yearMonthDayNum: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?/).map(yearMonthDayCondition()),
|
||||
yearMonthDayMinute: () => P.regexp(/\d\d\d\d-\d\d?-\d\d?\s+\d\d?:\d\d?/).map(yearMonthDayMinuteCondition()),
|
||||
yearMonthDaySecond: () =>
|
||||
P.regexp(/\d\d\d\d-\d\d?-\d\d?(\s+|T)\d\d?:\d\d?:\d\d?/).map(yearMonthDaySecondCondition()),
|
||||
|
||||
value: r => P.alt(...allowedValues.map(x => r[x])),
|
||||
valueTestEq: r => r.value.map(binaryCondition('=')),
|
||||
valueTestStr: r => r.value.map(likeCondition('like', '%#VALUE#%')),
|
||||
@ -228,29 +112,6 @@ const createParser = (filterType: FilterType) => {
|
||||
trueNum: () => word('1').map(binaryFixedValueCondition('1')),
|
||||
falseNum: () => word('0').map(binaryFixedValueCondition('0')),
|
||||
|
||||
this: () => word('THIS'),
|
||||
last: () => word('LAST'),
|
||||
next: () => word('NEXT'),
|
||||
week: () => word('WEEK'),
|
||||
month: () => word('MONTH'),
|
||||
year: () => word('YEAR'),
|
||||
|
||||
yesterday: () => word('YESTERDAY').map(fixedMomentIntervalCondition('day', -1)),
|
||||
today: () => word('TODAY').map(fixedMomentIntervalCondition('day', 0)),
|
||||
tomorrow: () => word('TOMORROW').map(fixedMomentIntervalCondition('day', 1)),
|
||||
|
||||
lastWeek: r => r.last.then(r.week).map(fixedMomentIntervalCondition('week', -1)),
|
||||
thisWeek: r => r.this.then(r.week).map(fixedMomentIntervalCondition('week', 0)),
|
||||
nextWeek: r => r.next.then(r.week).map(fixedMomentIntervalCondition('week', 1)),
|
||||
|
||||
lastMonth: r => r.last.then(r.month).map(fixedMomentIntervalCondition('month', -1)),
|
||||
thisMonth: r => r.this.then(r.month).map(fixedMomentIntervalCondition('month', 0)),
|
||||
nextMonth: r => r.next.then(r.month).map(fixedMomentIntervalCondition('month', 1)),
|
||||
|
||||
lastYear: r => r.last.then(r.year).map(fixedMomentIntervalCondition('year', -1)),
|
||||
thisYear: r => r.this.then(r.year).map(fixedMomentIntervalCondition('year', 0)),
|
||||
nextYear: r => r.next.then(r.year).map(fixedMomentIntervalCondition('year', 1)),
|
||||
|
||||
eq: r => word('=').then(r.value).map(binaryCondition('=')),
|
||||
ne: r => word('!=').then(r.value).map(binaryCondition('<>')),
|
||||
ne2: r => word('<>').then(r.value).map(binaryCondition('<>')),
|
||||
@ -294,27 +155,7 @@ const createParser = (filterType: FilterType) => {
|
||||
if (filterType == 'eval') {
|
||||
allowedElements.push('true', 'false');
|
||||
}
|
||||
if (filterType == 'datetime') {
|
||||
allowedElements.push(
|
||||
'yearMonthDaySecond',
|
||||
'yearMonthDayMinute',
|
||||
'yearMonthDayNum',
|
||||
'yearMonthNum',
|
||||
'yearNum',
|
||||
'yesterday',
|
||||
'today',
|
||||
'tomorrow',
|
||||
'lastWeek',
|
||||
'thisWeek',
|
||||
'nextWeek',
|
||||
'lastMonth',
|
||||
'thisMonth',
|
||||
'nextMonth',
|
||||
'lastYear',
|
||||
'thisYear',
|
||||
'nextYear'
|
||||
);
|
||||
}
|
||||
|
||||
// must be last
|
||||
if (filterType == 'string' || filterType == 'eval') {
|
||||
allowedElements.push('valueTestStr');
|
||||
@ -328,10 +169,10 @@ const createParser = (filterType: FilterType) => {
|
||||
const parsers = {
|
||||
number: createParser('number'),
|
||||
string: createParser('string'),
|
||||
datetime: createParser('datetime'),
|
||||
logical: createParser('logical'),
|
||||
eval: createParser('eval'),
|
||||
mongo: mongoParser,
|
||||
datetime: datetimeParser,
|
||||
};
|
||||
|
||||
export function parseFilter(value: string, filterType: FilterType): Condition {
|
||||
|
@ -16,6 +16,7 @@
|
||||
export let command;
|
||||
export let component = ToolStripButton;
|
||||
export let hideDisabled = false;
|
||||
export let buttonLabel = null;
|
||||
|
||||
$: cmd = Object.values($commandsCustomized).find((x: any) => x.id == command) as any;
|
||||
</script>
|
||||
@ -29,6 +30,6 @@
|
||||
disabled={!cmd.enabled}
|
||||
{...$$restProps}
|
||||
>
|
||||
{cmd.toolbarName || cmd.name}
|
||||
{buttonLabel || cmd.toolbarName || cmd.name}
|
||||
</svelte:component>
|
||||
{/if}
|
||||
|
@ -5,7 +5,16 @@
|
||||
import ToolStripSplitDropDownButton from './ToolStripSplitDropDownButton.svelte';
|
||||
|
||||
export let commands;
|
||||
$: menu = _.compact(commands).map(command => ({ command }));
|
||||
export let hideDisabled = false;
|
||||
export let buttonLabel = null;
|
||||
|
||||
$: menu = _.compact(commands).map(command => (_.isString(command) ? { command } : command));
|
||||
</script>
|
||||
|
||||
<ToolStripCommandButton command={commands[0]} component={ToolStripSplitDropDownButton} {menu} />
|
||||
<ToolStripCommandButton
|
||||
command={commands[0]}
|
||||
component={ToolStripSplitDropDownButton}
|
||||
{menu}
|
||||
{hideDisabled}
|
||||
{buttonLabel}
|
||||
/>
|
||||
|
@ -5,7 +5,7 @@
|
||||
id: 'dataGrid.refresh',
|
||||
category: 'Data grid',
|
||||
name: 'Refresh',
|
||||
keyText: 'F5',
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon reload',
|
||||
@ -17,7 +17,7 @@
|
||||
id: 'dataGrid.revertRowChanges',
|
||||
category: 'Data grid',
|
||||
name: 'Revert row changes',
|
||||
keyText: 'CtrlOrCommand+R',
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.containsChanges,
|
||||
onClick: () => getCurrentDataGrid().revertRowChanges(),
|
||||
});
|
||||
@ -52,6 +52,16 @@
|
||||
onClick: () => getCurrentDataGrid().insertNewRow(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.cloneRows',
|
||||
category: 'Data grid',
|
||||
name: 'Clone rows',
|
||||
toolbarName: 'Clone',
|
||||
keyText: 'CtrlOrCommand+Shift+C',
|
||||
testEnabled: () => getCurrentDataGrid()?.getGrider()?.editable,
|
||||
onClick: () => getCurrentDataGrid().cloneRows(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'dataGrid.setNull',
|
||||
category: 'Data grid',
|
||||
@ -418,16 +428,44 @@
|
||||
}
|
||||
|
||||
export async function insertNewRow() {
|
||||
if (grider.canInsert) {
|
||||
const rowIndex = grider.insertRow();
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
if (!grider.canInsert) return;
|
||||
const rowIndex = grider.insertRow();
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
}
|
||||
|
||||
export async function cloneRows() {
|
||||
if (!grider.canInsert) return;
|
||||
|
||||
let rowIndex = null;
|
||||
grider.beginUpdate();
|
||||
for (const index of _.sortBy(getSelectedRowIndexes(), x => x)) {
|
||||
if (_.isNumber(index)) {
|
||||
rowIndex = grider.insertRow();
|
||||
|
||||
for (const column of display.columns) {
|
||||
if (column.uniquePath.length > 1) continue;
|
||||
if (column.autoIncrement) continue;
|
||||
|
||||
grider.setCellValue(rowIndex, column.uniqueName, grider.getRowData(index)[column.uniqueName]);
|
||||
}
|
||||
}
|
||||
}
|
||||
grider.endUpdate();
|
||||
|
||||
if (rowIndex == null) return;
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
currentCell = cell;
|
||||
// @ts-ignore
|
||||
selectedCells = [cell];
|
||||
await tick();
|
||||
scrollIntoView(cell);
|
||||
}
|
||||
|
||||
export function setFixedValue(value) {
|
||||
@ -1171,7 +1209,20 @@
|
||||
|
||||
handleCursorMove(event);
|
||||
|
||||
if (event.shiftKey && event.keyCode != keycodes.shift && event.keyCode != keycodes.tab) {
|
||||
if (
|
||||
event.shiftKey &&
|
||||
event.keyCode != keycodes.shift &&
|
||||
event.keyCode != keycodes.tab &&
|
||||
event.keyCode != keycodes.ctrl &&
|
||||
event.keyCode != keycodes.leftWindowKey &&
|
||||
event.keyCode != keycodes.rightWindowKey &&
|
||||
!(
|
||||
(event.keyCode >= keycodes.a && event.keyCode <= keycodes.z) ||
|
||||
(event.keyCode >= keycodes.n0 && event.keyCode <= keycodes.n9) ||
|
||||
(event.keyCode >= keycodes.numPad0 && event.keyCode <= keycodes.numPad9) ||
|
||||
event.keyCode == keycodes.dash
|
||||
)
|
||||
) {
|
||||
selectedCells = getCellRange(shiftDragStartCell || currentCell, currentCell);
|
||||
}
|
||||
}
|
||||
@ -1432,6 +1483,7 @@
|
||||
{ command: 'dataGrid.revertAllChanges', hideDisabled: true },
|
||||
{ command: 'dataGrid.deleteSelectedRows' },
|
||||
{ command: 'dataGrid.insertNewRow' },
|
||||
{ command: 'dataGrid.cloneRows' },
|
||||
{ command: 'dataGrid.setNull' },
|
||||
{ placeTag: 'edit' },
|
||||
{ divider: true },
|
||||
@ -1498,12 +1550,16 @@
|
||||
<div>
|
||||
<ErrorInfo
|
||||
alignTop
|
||||
message="No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command."
|
||||
message={grider.editable
|
||||
? 'No rows loaded, check filter or add new documents. You could copy documents from ohter collections/tables with Copy advanved/Copy as JSON command.'
|
||||
: 'No rows loaded'}
|
||||
/>
|
||||
{#if display.filterCount > 0}
|
||||
<FormStyledButton value="Reset filter" on:click={() => display.clearFilters()} />
|
||||
{/if}
|
||||
<FormStyledButton value="Add document" on:click={addJsonDocument} />
|
||||
{#if grider.editable}
|
||||
<FormStyledButton value="Add document" on:click={addJsonDocument} />
|
||||
{/if}
|
||||
</div>
|
||||
{:else if grider.errors && grider.errors.length > 0}
|
||||
<div>
|
||||
|
@ -16,7 +16,7 @@
|
||||
id: 'dataForm.refresh',
|
||||
category: 'Data form',
|
||||
name: 'Refresh',
|
||||
keyText: 'F5',
|
||||
keyText: 'F5 | CtrlOrCommand+R',
|
||||
toolbar: true,
|
||||
isRelatedToTab: true,
|
||||
icon: 'icon reload',
|
||||
@ -38,7 +38,7 @@
|
||||
id: 'dataForm.revertRowChanges',
|
||||
category: 'Data form',
|
||||
name: 'Revert row changes',
|
||||
keyText: 'CtrlOrCommand+R',
|
||||
keyText: 'CtrlOrCommand+U',
|
||||
testEnabled: () => getCurrentDataForm()?.getFormer()?.containsChanges,
|
||||
onClick: () => getCurrentDataForm().getFormer().revertRowChanges(),
|
||||
});
|
||||
|
@ -168,6 +168,10 @@
|
||||
<FormCheckboxField label="Is read only" name="isReadOnly" disabled={isConnected} />
|
||||
{/if}
|
||||
|
||||
{#if driver?.showConnectionField('trustServerCertificate', $values)}
|
||||
<FormCheckboxField label="Trust server certificate" name="trustServerCertificate" disabled={isConnected} />
|
||||
{/if}
|
||||
|
||||
{#if driver?.showConnectionField('defaultDatabase', $values)}
|
||||
<FormTextField label="Default database" name="defaultDatabase" disabled={isConnected} />
|
||||
{/if}
|
||||
|
@ -91,6 +91,12 @@ ORDER BY
|
||||
|
||||
<FormCheckboxField name="dataGrid.thousandsSeparator" label="Use thousands separator for numbers" />
|
||||
|
||||
<FormTextField
|
||||
name="dataGrid.defaultAutoRefreshInterval"
|
||||
label="Default grid auto refresh interval in seconds"
|
||||
defaultValue="10"
|
||||
/>
|
||||
|
||||
<div class="heading">Connection</div>
|
||||
<FormCheckboxField
|
||||
name="connection.autoRefresh"
|
||||
@ -99,7 +105,7 @@ ORDER BY
|
||||
/>
|
||||
<FormTextField
|
||||
name="connection.autoRefreshInterval"
|
||||
label="Interval between automatic refreshes in seconds"
|
||||
label="Interval between automatic DB structure reloads in seconds"
|
||||
defaultValue="30"
|
||||
disabled={values['connection.autoRefresh'] === false}
|
||||
/>
|
||||
|
@ -29,7 +29,11 @@ export function writableWithStorage<T>(defaultValue: T, storageName) {
|
||||
}
|
||||
|
||||
export function writableSettingsValue<T>(defaultValue: T, storageName) {
|
||||
const res = derived(useSettings(), $settings => ($settings || {})[storageName] ?? defaultValue);
|
||||
const res = derived(useSettings(), $settings => {
|
||||
const obj = $settings || {};
|
||||
// console.log('GET SETTINGS', $settings, storageName, obj[storageName]);
|
||||
return obj[storageName] ?? defaultValue;
|
||||
});
|
||||
return {
|
||||
...res,
|
||||
set: value => apiCall('config/update-settings', { [storageName]: value }),
|
||||
|
@ -167,7 +167,7 @@
|
||||
|
||||
$: isConnected = $openedConnections.includes($values._id) || $openedSingleDatabaseConnections.includes($values._id);
|
||||
|
||||
$: console.log('CONN VALUES', $values);
|
||||
// $: console.log('CONN VALUES', $values);
|
||||
</script>
|
||||
|
||||
<FormProviderCore template={FormFieldTemplateLarge} {values}>
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script lang="ts" context="module">
|
||||
const getCurrentEditor = () => getActiveComponent('TableDataTab');
|
||||
const INTERVALS = [5, 10, 15, 13, 60];
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.save',
|
||||
@ -14,6 +15,46 @@
|
||||
onClick: () => getCurrentEditor().save(),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.setAutoRefresh.1',
|
||||
category: 'Data grid',
|
||||
name: 'Refresh every 1 second',
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(1),
|
||||
});
|
||||
|
||||
for (const seconds of INTERVALS) {
|
||||
registerCommand({
|
||||
id: `tableData.setAutoRefresh.${seconds}`,
|
||||
category: 'Data grid',
|
||||
name: `Refresh every ${seconds} seconds`,
|
||||
isRelatedToTab: true,
|
||||
testEnabled: () => !!getCurrentEditor(),
|
||||
onClick: () => getCurrentEditor().setAutoRefresh(seconds),
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.stopAutoRefresh',
|
||||
category: 'Data grid',
|
||||
name: 'Stop auto refresh',
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === true,
|
||||
onClick: () => getCurrentEditor().stopAutoRefresh(null),
|
||||
});
|
||||
|
||||
registerCommand({
|
||||
id: 'tableData.startAutoRefresh',
|
||||
category: 'Data grid',
|
||||
name: 'Start auto refresh',
|
||||
isRelatedToTab: true,
|
||||
keyText: 'CtrlOrCommand+Shift+R',
|
||||
testEnabled: () => getCurrentEditor()?.isAutoRefresh() === false,
|
||||
onClick: () => getCurrentEditor().startAutoRefresh(),
|
||||
});
|
||||
|
||||
export const matchingProps = ['conid', 'database', 'schemaName', 'pureName'];
|
||||
export const allowAddToFavorites = props => true;
|
||||
</script>
|
||||
@ -50,12 +91,14 @@
|
||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||
import StatusBarTabItem from '../widgets/StatusBarTabItem.svelte';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { setContext } from 'svelte';
|
||||
import { onDestroy, setContext } from 'svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { getLocalStorage, setLocalStorage } from '../utility/storageCache';
|
||||
import ToolStripContainer from '../buttons/ToolStripContainer.svelte';
|
||||
import ToolStripCommandButton from '../buttons/ToolStripCommandButton.svelte';
|
||||
import ToolStripExportButton, { createQuickExportHandlerRef } from '../buttons/ToolStripExportButton.svelte';
|
||||
import ToolStripCommandSplitButton from '../buttons/ToolStripCommandSplitButton.svelte';
|
||||
import { getIntSettingsValue } from '../settings/settingsTools';
|
||||
|
||||
export let tabid;
|
||||
export let conid;
|
||||
@ -68,6 +111,11 @@
|
||||
const config = useGridConfig(tabid);
|
||||
const cache = writable(createGridCache());
|
||||
const dbinfo = useDatabaseInfo({ conid, database });
|
||||
|
||||
let autoRefreshInterval = getIntSettingsValue('dataGrid.defaultAutoRefreshInterval', 10, 1, 3600);
|
||||
let autoRefreshStarted = false;
|
||||
let autoRefreshTimer = null;
|
||||
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
|
||||
const [changeSetStore, dispatchChangeSet] = createUndoReducer(createChangeSet());
|
||||
@ -106,6 +154,38 @@
|
||||
return changeSetContainsChanges($changeSetStore?.value);
|
||||
}
|
||||
|
||||
export function setAutoRefresh(interval) {
|
||||
autoRefreshInterval = interval;
|
||||
startAutoRefresh();
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function isAutoRefresh() {
|
||||
return autoRefreshStarted;
|
||||
}
|
||||
|
||||
export function startAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshTimer = setInterval(() => {
|
||||
cache.update(reloadDataCacheFunc);
|
||||
}, autoRefreshInterval * 1000);
|
||||
autoRefreshStarted = true;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
export function stopAutoRefresh() {
|
||||
closeRefreshTimer();
|
||||
autoRefreshStarted = false;
|
||||
invalidateCommands();
|
||||
}
|
||||
|
||||
function closeRefreshTimer() {
|
||||
if (autoRefreshTimer) {
|
||||
clearInterval(autoRefreshTimer);
|
||||
autoRefreshTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$changeSetStore;
|
||||
invalidateCommands();
|
||||
@ -117,7 +197,21 @@
|
||||
setContext('collapsedLeftColumnStore', collapsedLeftColumnStore);
|
||||
$: setLocalStorage('dataGrid_collapsedLeftColumn', $collapsedLeftColumnStore);
|
||||
|
||||
onDestroy(() => {
|
||||
closeRefreshTimer();
|
||||
});
|
||||
|
||||
const quickExportHandlerRef = createQuickExportHandlerRef();
|
||||
|
||||
function createAutoRefreshMenu() {
|
||||
return [
|
||||
{ divider: true },
|
||||
{ command: 'tableData.stopAutoRefresh', hideDisabled: true },
|
||||
{ command: 'tableData.startAutoRefresh', hideDisabled: true },
|
||||
'tableData.setAutoRefresh.1',
|
||||
...INTERVALS.map(seconds => ({ command: `tableData.setAutoRefresh.${seconds}`, text: `...${seconds} seconds` })),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
@ -134,8 +228,19 @@
|
||||
/>
|
||||
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled />
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted ? `Refresh (every ${autoRefreshInterval}s)` : null}
|
||||
commands={['dataGrid.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
/>
|
||||
<ToolStripCommandSplitButton
|
||||
buttonLabel={autoRefreshStarted ? `Refresh (every ${autoRefreshInterval}s)` : null}
|
||||
commands={['dataForm.refresh', ...createAutoRefreshMenu()]}
|
||||
hideDisabled
|
||||
/>
|
||||
|
||||
<!-- <ToolStripCommandButton command="dataGrid.refresh" hideDisabled />
|
||||
<ToolStripCommandButton command="dataForm.refresh" hideDisabled /> -->
|
||||
<ToolStripCommandButton command="tableData.save" />
|
||||
<ToolStripCommandButton command="dataGrid.insertNewRow" hideDisabled />
|
||||
<ToolStripCommandButton command="dataGrid.deleteSelectedRows" hideDisabled />
|
||||
|
@ -56,8 +56,8 @@ export default {
|
||||
y: 89,
|
||||
z: 90,
|
||||
leftWindowKey: 91,
|
||||
rightWindowKey: 92,
|
||||
selectKey: 93,
|
||||
rightWindowKey: 93,
|
||||
// selectKey: 93,
|
||||
numPad0: 96,
|
||||
numPad1: 97,
|
||||
numPad2: 98,
|
||||
|
@ -189,25 +189,29 @@ async function getCore(loader, args) {
|
||||
function useCore(loader, args) {
|
||||
const { url, params, reloadTrigger, transform, onLoaded } = loader(args);
|
||||
const cacheKey = stableStringify({ url, ...params });
|
||||
let closed = false;
|
||||
let openedCount = 0;
|
||||
|
||||
return {
|
||||
subscribe: onChange => {
|
||||
async function handleReload() {
|
||||
const res = await getCore(loader, args);
|
||||
if (!closed) {
|
||||
if (openedCount > 0) {
|
||||
onChange(res);
|
||||
}
|
||||
}
|
||||
|
||||
openedCount += 1;
|
||||
handleReload();
|
||||
|
||||
if (reloadTrigger) {
|
||||
subscribeCacheChange(reloadTrigger, cacheKey, handleReload);
|
||||
return () => {
|
||||
closed = true;
|
||||
openedCount -= 1;
|
||||
unsubscribeCacheChange(reloadTrigger, cacheKey, handleReload);
|
||||
};
|
||||
} else {
|
||||
return () => {
|
||||
openedCount -= 1;
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -5,19 +5,17 @@ const driverBase = require('../frontend/driver');
|
||||
const Analyser = require('./Analyser');
|
||||
const MongoClient = require('mongodb').MongoClient;
|
||||
const ObjectId = require('mongodb').ObjectId;
|
||||
const Cursor = require('mongodb').Cursor;
|
||||
const AbstractCursor = require('mongodb').AbstractCursor;
|
||||
const createBulkInsertStream = require('./createBulkInsertStream');
|
||||
|
||||
function transformMongoData(row) {
|
||||
return _.mapValues(row, (v) => (v && v.constructor == ObjectId ? { $oid: v.toString() } : v));
|
||||
}
|
||||
|
||||
function readCursor(cursor, options) {
|
||||
return new Promise((resolve) => {
|
||||
options.recordset({ __isDynamicStructure: true });
|
||||
|
||||
cursor.on('data', (data) => options.row(transformMongoData(data)));
|
||||
cursor.on('end', () => resolve());
|
||||
async function readCursor(cursor, options) {
|
||||
options.recordset({ __isDynamicStructure: true });
|
||||
await cursor.forEach((row) => {
|
||||
options.row(transformMongoData(row));
|
||||
});
|
||||
}
|
||||
|
||||
@ -118,7 +116,7 @@ const driver = {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exprValue instanceof Cursor) {
|
||||
if (exprValue instanceof AbstractCursor) {
|
||||
await readCursor(exprValue, options);
|
||||
} else if (isPromise(exprValue)) {
|
||||
try {
|
||||
|
@ -22,7 +22,7 @@ function extractTediousColumns(columns, addDriverNativeColumn = false) {
|
||||
return res;
|
||||
}
|
||||
|
||||
async function tediousConnect({ server, port, user, password, database, ssl }) {
|
||||
async function tediousConnect({ server, port, user, password, database, ssl, trustServerCertificate }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connectionOptions = {
|
||||
encrypt: !!ssl,
|
||||
@ -32,6 +32,7 @@ async function tediousConnect({ server, port, user, password, database, ssl }) {
|
||||
validateBulkLoadParameters: false,
|
||||
requestTimeout: 1000 * 3600,
|
||||
port: port ? parseInt(port) : undefined,
|
||||
trustServerCertificate: !!trustServerCertificate,
|
||||
};
|
||||
|
||||
if (database) {
|
||||
|
@ -126,7 +126,8 @@ const driver = {
|
||||
showConnectionField: (field, values) =>
|
||||
['authType', 'server', 'port', 'user', 'password', 'defaultDatabase', 'singleDatabase', 'isReadOnly'].includes(
|
||||
field
|
||||
),
|
||||
) ||
|
||||
(field == 'trustServerCertificate' && values.authType != 'sql' && values.authType != 'sspi'),
|
||||
getQuerySplitterOptions: () => mssqlSplitterOptions,
|
||||
|
||||
engine: 'mssql@dbgate-plugin-mssql',
|
||||
|
65
yarn.lock
65
yarn.lock
@ -1941,7 +1941,14 @@ asn1.js@^4.0.0:
|
||||
inherits "^2.0.1"
|
||||
minimalistic-assert "^1.0.0"
|
||||
|
||||
asn1@~0.2.0, asn1@~0.2.3:
|
||||
asn1@^0.2.4:
|
||||
version "0.2.6"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.6.tgz#0d3a7bb6e64e02a90c0303b31f292868ea09a08d"
|
||||
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
|
||||
dependencies:
|
||||
safer-buffer "~2.1.0"
|
||||
|
||||
asn1@~0.2.3:
|
||||
version "0.2.4"
|
||||
resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136"
|
||||
integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==
|
||||
@ -2471,6 +2478,11 @@ bufferutil@^4.0.1:
|
||||
dependencies:
|
||||
node-gyp-build "~3.7.0"
|
||||
|
||||
buildcheck@0.0.3:
|
||||
version "0.0.3"
|
||||
resolved "https://registry.yarnpkg.com/buildcheck/-/buildcheck-0.0.3.tgz#70451897a95d80f7807e68fc412eb2e7e35ff4d5"
|
||||
integrity sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==
|
||||
|
||||
builtin-modules@^3.1.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.2.0.tgz#45d5db99e7ee5e6bc4f362e008bf917ab5049887"
|
||||
@ -3090,6 +3102,14 @@ cors@^2.8.5:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
cpu-features@~0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/cpu-features/-/cpu-features-0.0.4.tgz#0023475bb4f4c525869c162e4108099e35bf19d8"
|
||||
integrity sha512-fKiZ/zp1mUwQbnzb9IghXtHtDoTMtNeb8oYGx6kX2SYfhnG0HNdBEBIzB9b5KlXu5DQPhfy3mInbBxFcgwAr3A==
|
||||
dependencies:
|
||||
buildcheck "0.0.3"
|
||||
nan "^2.15.0"
|
||||
|
||||
crc-32@~1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.0.tgz#cb2db6e29b88508e32d9dd0ec1693e7b41a18208"
|
||||
@ -3353,6 +3373,13 @@ debug@^4.3.1:
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
decamelize-keys@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9"
|
||||
@ -7794,6 +7821,11 @@ nan@^2.15.0:
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee"
|
||||
integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==
|
||||
|
||||
nan@^2.16.0:
|
||||
version "2.16.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.16.0.tgz#664f43e45460fb98faf00edca0bb0d7b8dce7916"
|
||||
integrity sha512-UdAqHyFngu7TfQKsCBgAA6pWDkT8MAO7d0jyOecVhN5354xbLqdn8mV9Tat9gepAupm0bt2DbeaSC8vS52MuFA==
|
||||
|
||||
nanomatch@^1.2.9:
|
||||
version "1.2.13"
|
||||
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
|
||||
@ -7978,14 +8010,6 @@ node-releases@^1.1.71:
|
||||
resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.72.tgz#14802ab6b1039a79a0c7d662b610a5bbd76eacbe"
|
||||
integrity sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==
|
||||
|
||||
node-ssh-forward@^0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/node-ssh-forward/-/node-ssh-forward-0.7.2.tgz#a5103ba9ae0e044156b0568a5084304e7d5b4a2a"
|
||||
integrity sha512-dQGhwT9emJJ0PymZGXdwHue18oc+bnAeqSGUCi4+YufGLaTSfA4Ft04T97WGkVxmBUySy/O6lvKIfpJM0a8lgA==
|
||||
dependencies:
|
||||
debug "^4.1.1"
|
||||
ssh2 "^0.8.9"
|
||||
|
||||
node-xml-stream-parser@^1.0.12:
|
||||
version "1.0.12"
|
||||
resolved "https://registry.yarnpkg.com/node-xml-stream-parser/-/node-xml-stream-parser-1.0.12.tgz#2d97cc91147bbcdffa0a89cf7dfcc129db2aa789"
|
||||
@ -10141,21 +10165,16 @@ ssf@~0.11.2:
|
||||
dependencies:
|
||||
frac "~1.1.2"
|
||||
|
||||
ssh2-streams@~0.4.10:
|
||||
version "0.4.10"
|
||||
resolved "https://registry.yarnpkg.com/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34"
|
||||
integrity sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==
|
||||
ssh2@^1.11.0:
|
||||
version "1.11.0"
|
||||
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-1.11.0.tgz#ce60186216971e12f6deb553dcf82322498fe2e4"
|
||||
integrity sha512-nfg0wZWGSsfUe/IBJkXVll3PEZ//YH2guww+mP88gTpuSU4FtZN7zu9JoeTGOyCNx2dTDtT9fOpWwlzyj4uOOw==
|
||||
dependencies:
|
||||
asn1 "~0.2.0"
|
||||
asn1 "^0.2.4"
|
||||
bcrypt-pbkdf "^1.0.2"
|
||||
streamsearch "~0.1.2"
|
||||
|
||||
ssh2@^0.8.9:
|
||||
version "0.8.9"
|
||||
resolved "https://registry.yarnpkg.com/ssh2/-/ssh2-0.8.9.tgz#54da3a6c4ba3daf0d8477a538a481326091815f3"
|
||||
integrity sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==
|
||||
dependencies:
|
||||
ssh2-streams "~0.4.10"
|
||||
optionalDependencies:
|
||||
cpu-features "~0.0.4"
|
||||
nan "^2.16.0"
|
||||
|
||||
sshpk@^1.7.0:
|
||||
version "1.16.1"
|
||||
@ -10265,7 +10284,7 @@ stream-transform@^2.1.0:
|
||||
dependencies:
|
||||
mixme "^0.5.0"
|
||||
|
||||
streamsearch@0.1.2, streamsearch@~0.1.2:
|
||||
streamsearch@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
||||
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
|
||||
|
Loading…
Reference in New Issue
Block a user