Compare commits
52 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 593e919e32 | |||
| 21c26067ef | |||
| fd12eef0fc | |||
| 6fb314c414 | |||
| c65806fd89 | |||
| 307aaa2801 | |||
| 19dadcd4ae | |||
| d234226750 | |||
| c57ec68916 | |||
| dfc8c75d76 | |||
| 399d194771 | |||
| 7b64e33e92 | |||
| 42ffd49f6e | |||
| 3982a28549 | |||
| f5e243a77f | |||
| 7888cf6714 | |||
| fd9fa0c95a | |||
| 0d2120e96b | |||
| 229f0ea9c1 | |||
| c9308255a7 | |||
| 8ff44e41b1 | |||
| ab2fb3bf97 | |||
| d5b8433c17 | |||
| cb0aee6476 | |||
| 4efa87c3c8 | |||
| 20180fe4c4 | |||
| 80e17eff39 | |||
| 9bed46fe01 | |||
| 44059f1215 | |||
| 4593ab7c46 | |||
| 68cf397473 | |||
| cc385c12ec | |||
| d243e8cee5 | |||
| 5f56aa2cf6 | |||
| ce38f7da4c | |||
| 3f14fec678 | |||
| b39af32426 | |||
| f81cefa8cb | |||
| 8a2b6f3f37 | |||
| 2ba0c2cc46 | |||
| 6e4a53a2ab | |||
| c80510c37b | |||
| 857f3fb4f7 | |||
| 22a263a598 | |||
| f9f2a501ab | |||
| 45d172d0b1 | |||
| 00453ae379 | |||
| abc007753a | |||
| b314e363cd | |||
| b439c7bb70 | |||
| 7704e9b305 | |||
| a7ed6bf62b |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.7.22",
|
||||
"version": "3.7.26",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"dependencies": {
|
||||
|
||||
@@ -18,7 +18,9 @@
|
||||
"exceljs": "^4.0.1",
|
||||
"express": "^4.17.1",
|
||||
"express-basic-auth": "^1.2.0",
|
||||
"express-fileupload": "^1.2.0",
|
||||
"find-free-port": "^2.0.0",
|
||||
"find-remove": "^2.0.3",
|
||||
"fs-extra": "^8.1.0",
|
||||
"http": "^0.0.0",
|
||||
"line-reader": "^0.4.0",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
const fs = require('fs-extra');
|
||||
const stream = require('stream');
|
||||
const readline = require('readline');
|
||||
const path = require('path');
|
||||
const { formatWithOptions } = require('util');
|
||||
const { archivedir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const JsonLinesDatastore = require('../utility/JsonLinesDatastore');
|
||||
|
||||
module.exports = {
|
||||
folders_meta: 'get',
|
||||
@@ -61,4 +65,37 @@ module.exports = {
|
||||
await fs.rmdir(path.join(archivedir(), folder), { recursive: true });
|
||||
socket.emitChanged(`archive-folders-changed`);
|
||||
},
|
||||
|
||||
saveFreeTable_meta: 'post',
|
||||
async saveFreeTable({ folder, file, data }) {
|
||||
const { structure, rows } = data;
|
||||
const fileStream = fs.createWriteStream(path.join(archivedir(), folder, `${file}.jsonl`));
|
||||
await fileStream.write(JSON.stringify(structure) + '\n');
|
||||
for (const row of rows) {
|
||||
await fileStream.write(JSON.stringify(row) + '\n');
|
||||
}
|
||||
await fileStream.close();
|
||||
return true;
|
||||
},
|
||||
|
||||
loadFreeTable_meta: 'post',
|
||||
async loadFreeTable({ folder, file }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileStream = fs.createReadStream(path.join(archivedir(), folder, `${file}.jsonl`));
|
||||
const liner = readline.createInterface({
|
||||
input: fileStream,
|
||||
});
|
||||
let structure = null;
|
||||
const rows = [];
|
||||
liner.on('line', (line) => {
|
||||
const data = JSON.parse(line);
|
||||
if (structure) rows.push(data);
|
||||
else structure = data;
|
||||
});
|
||||
liner.on('close', () => {
|
||||
resolve({ structure, rows });
|
||||
fileStream.close();
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,12 +16,24 @@ ${script}
|
||||
dbgateApi.runScript(run);
|
||||
`;
|
||||
|
||||
const loaderScriptTemplate = (functionName, props, runid) => `
|
||||
const dbgateApi = require(process.env.DBGATE_API || "@dbgate/api");
|
||||
require=null;
|
||||
async function run() {
|
||||
const reader=await dbgateApi.${functionName}(${JSON.stringify(props)});
|
||||
const writer=await dbgateApi.collectorWriter({runid: '${runid}'});
|
||||
await dbgateApi.copyStream(reader, writer);
|
||||
}
|
||||
dbgateApi.runScript(run);
|
||||
`;
|
||||
|
||||
module.exports = {
|
||||
/** @type {import('@dbgate/types').OpenedRunner[]} */
|
||||
opened: [],
|
||||
requests: {},
|
||||
|
||||
dispatchMessage(runid, message) {
|
||||
// console.log('DISPATCHING', message);
|
||||
if (message) console.log('...', message.message);
|
||||
if (_.isString(message)) {
|
||||
socket.emit(`runner-info-${runid}`, {
|
||||
message,
|
||||
@@ -40,12 +52,24 @@ module.exports = {
|
||||
|
||||
handle_ping() {},
|
||||
|
||||
start_meta: 'post',
|
||||
async start({ script }) {
|
||||
const runid = uuidv1();
|
||||
handle_freeData(runid, { freeData }) {
|
||||
const [resolve, reject] = this.requests[runid];
|
||||
resolve(freeData);
|
||||
delete this.requests[runid];
|
||||
},
|
||||
|
||||
rejectRequest(runid, error) {
|
||||
if (this.requests[runid]) {
|
||||
const [resolve, reject] = this.requests[runid];
|
||||
reject(error);
|
||||
delete this.requests[runid];
|
||||
}
|
||||
},
|
||||
|
||||
startCore(runid, scriptText) {
|
||||
const directory = path.join(rundir(), runid);
|
||||
const scriptFile = path.join(uploadsdir(), runid + '.js');
|
||||
fs.writeFileSync(`${scriptFile}`, scriptTemplate(script));
|
||||
fs.writeFileSync(`${scriptFile}`, scriptText);
|
||||
fs.mkdirSync(directory);
|
||||
console.log(`RUNNING SCRIPT ${scriptFile}`);
|
||||
const subprocess = fork(scriptFile, ['--checkParent'], {
|
||||
@@ -61,9 +85,13 @@ module.exports = {
|
||||
byline(subprocess.stdout).on('data', pipeDispatcher('info'));
|
||||
byline(subprocess.stderr).on('data', pipeDispatcher('error'));
|
||||
subprocess.on('exit', (code) => {
|
||||
this.rejectRequest(runid, { message: 'No data retured, maybe input data source is too big' });
|
||||
console.log('... EXIT process', code);
|
||||
socket.emit(`runner-done-${runid}`, code);
|
||||
});
|
||||
subprocess.on('error', (error) => {
|
||||
this.rejectRequest(runid, { message: error && (error.message || error.toString()) });
|
||||
console.error('... ERROR subprocess', error);
|
||||
this.dispatchMessage({
|
||||
severity: 'error',
|
||||
message: error.toString(),
|
||||
@@ -81,6 +109,12 @@ module.exports = {
|
||||
return newOpened;
|
||||
},
|
||||
|
||||
start_meta: 'post',
|
||||
async start({ script }) {
|
||||
const runid = uuidv1();
|
||||
return this.startCore(runid, scriptTemplate(script));
|
||||
},
|
||||
|
||||
cancel_meta: 'post',
|
||||
async cancel({ runid }) {
|
||||
const runner = this.opened.find((x) => x.runid == runid);
|
||||
@@ -106,4 +140,14 @@ module.exports = {
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
loadReader_meta: 'post',
|
||||
async loadReader({ functionName, props }) {
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
const runid = uuidv1();
|
||||
this.requests[runid] = [resolve, reject];
|
||||
this.startCore(runid, loaderScriptTemplate(functionName, props, runid));
|
||||
});
|
||||
return promise;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
const path = require('path');
|
||||
const { uploadsdir } = require('../utility/directories');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
|
||||
const extensions = [
|
||||
{
|
||||
ext: '.xlsx',
|
||||
type: 'excel',
|
||||
},
|
||||
{
|
||||
ext: '.jsonl',
|
||||
type: 'jsonl',
|
||||
},
|
||||
{
|
||||
ext: '.csv',
|
||||
type: 'csv',
|
||||
},
|
||||
];
|
||||
|
||||
module.exports = {
|
||||
upload_meta: {
|
||||
method: 'post',
|
||||
raw: true,
|
||||
},
|
||||
upload(req, res) {
|
||||
const { data } = req.files || {};
|
||||
if (!data) {
|
||||
res.json(null);
|
||||
return;
|
||||
}
|
||||
const uploadName = uuidv1();
|
||||
const filePath = path.join(uploadsdir(), uploadName);
|
||||
console.log(`Uploading file ${data.name}, size=${data.size}`);
|
||||
let storageType = null;
|
||||
let shortName = data.name;
|
||||
for (const { ext, type } of extensions) {
|
||||
if (data.name.endsWith(ext)) {
|
||||
storageType = type;
|
||||
shortName = data.name.slice(0, -ext.length);
|
||||
}
|
||||
}
|
||||
data.mv(filePath, () => {
|
||||
res.json({
|
||||
originalName: data.name,
|
||||
shortName,
|
||||
storageType,
|
||||
uploadName,
|
||||
filePath,
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
const express = require('express');
|
||||
const basicAuth = require('express-basic-auth');
|
||||
const bodyParser = require('body-parser');
|
||||
const fileUpload = require('express-fileupload');
|
||||
const http = require('http');
|
||||
const cors = require('cors');
|
||||
const io = require('socket.io');
|
||||
@@ -21,6 +22,7 @@ const jsldata = require('./controllers/jsldata');
|
||||
const config = require('./controllers/config');
|
||||
const files = require('./controllers/files');
|
||||
const archive = require('./controllers/archive');
|
||||
const uploads = require('./controllers/uploads');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
|
||||
@@ -47,6 +49,13 @@ function start(argument = null) {
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
|
||||
app.use(
|
||||
'/uploads',
|
||||
fileUpload({
|
||||
limits: { fileSize: 4 * 1024 * 1024 },
|
||||
})
|
||||
);
|
||||
|
||||
useController(app, '/connections', connections);
|
||||
useController(app, '/server-connections', serverConnections);
|
||||
useController(app, '/database-connections', databaseConnections);
|
||||
@@ -57,6 +66,7 @@ function start(argument = null) {
|
||||
useController(app, '/config', config);
|
||||
useController(app, '/files', files);
|
||||
useController(app, '/archive', archive);
|
||||
useController(app, '/uploads', uploads);
|
||||
|
||||
if (process.env.PAGES_DIRECTORY) {
|
||||
app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||
|
||||
@@ -19,7 +19,7 @@ class TableWriter {
|
||||
this.jslid = uuidv1();
|
||||
this.currentFile = path.join(jsldir(), `${this.jslid}.jsonl`);
|
||||
this.currentRowCount = 0;
|
||||
this.currentChangeIndex = 0;
|
||||
this.currentChangeIndex = 1;
|
||||
fs.writeFileSync(this.currentFile, JSON.stringify({ columns }) + '\n');
|
||||
this.currentStream = fs.createWriteStream(this.currentFile, { flags: 'a' });
|
||||
this.writeCurrentStats(false, false);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
const stream = require('stream');
|
||||
|
||||
class CollectorWriterStream extends stream.Writable {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
this.rows = [];
|
||||
this.structure = null;
|
||||
this.runid = options.runid;
|
||||
}
|
||||
_write(chunk, enc, next) {
|
||||
if (!this.structure) this.structure = chunk;
|
||||
else this.rows.push(chunk);
|
||||
next();
|
||||
}
|
||||
|
||||
_final(callback) {
|
||||
process.send({
|
||||
msgtype: 'freeData',
|
||||
runid: this.runid,
|
||||
freeData: { rows: this.rows, structure: this.structure },
|
||||
});
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
async function collectorWriter({ runid }) {
|
||||
return new CollectorWriterStream({
|
||||
objectMode: true,
|
||||
runid,
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = collectorWriter;
|
||||
@@ -13,6 +13,7 @@ const jsonLinesReader = require('./jsonLinesReader');
|
||||
const jslDataReader = require('./jslDataReader');
|
||||
const archiveWriter = require('./archiveWriter');
|
||||
const archiveReader = require('./archiveReader');
|
||||
const collectorWriter = require('./collectorWriter');
|
||||
|
||||
module.exports = {
|
||||
queryReader,
|
||||
@@ -30,4 +31,5 @@ module.exports = {
|
||||
jslDataReader,
|
||||
archiveWriter,
|
||||
archiveReader,
|
||||
collectorWriter,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
const findRemoveSync = require('find-remove');
|
||||
|
||||
function cleanDirectory(directory) {
|
||||
findRemoveSync(directory, {
|
||||
age: { seconds: 3600 },
|
||||
files: '*.*',
|
||||
dir: '*',
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = cleanDirectory;
|
||||
@@ -1,11 +1,15 @@
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const cleanDirectory = require('./cleanDirectory');
|
||||
|
||||
const createDirectories = {};
|
||||
|
||||
const ensureDirectory = (dir) => {
|
||||
const ensureDirectory = (dir, clean) => {
|
||||
if (!createDirectories[dir]) {
|
||||
if (clean && fs.existsSync(dir)) {
|
||||
console.log(`Cleaning directory ${dir}`);
|
||||
cleanDirectory(dir);
|
||||
}
|
||||
if (!fs.existsSync(dir)) {
|
||||
console.log(`Creating directory ${dir}`);
|
||||
fs.mkdirSync(dir);
|
||||
@@ -21,16 +25,16 @@ function datadir() {
|
||||
return dir;
|
||||
}
|
||||
|
||||
const dirFunc = (dirname) => () => {
|
||||
const dirFunc = (dirname, clean = false) => () => {
|
||||
const dir = path.join(datadir(), dirname);
|
||||
ensureDirectory(dir);
|
||||
ensureDirectory(dir, clean);
|
||||
|
||||
return dir;
|
||||
};
|
||||
|
||||
const jsldir = dirFunc('jsl');
|
||||
const rundir = dirFunc('run');
|
||||
const uploadsdir = dirFunc('uploads');
|
||||
const jsldir = dirFunc('jsl', true);
|
||||
const rundir = dirFunc('run', true);
|
||||
const uploadsdir = dirFunc('uploads', true);
|
||||
const archivedir = dirFunc('archive');
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import _ from 'lodash';
|
||||
import { EngineDriver, ViewInfo, ColumnInfo } from '@dbgate/types';
|
||||
import { GridDisplay, ChangeCacheFunc, ChangeConfigFunc } from './GridDisplay';
|
||||
import { GridConfig, GridCache } from './GridConfig';
|
||||
import { FreeTableModel } from './FreeTableModel';
|
||||
|
||||
export class FreeTableGridDisplay extends GridDisplay {
|
||||
constructor(
|
||||
public model: FreeTableModel,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc
|
||||
) {
|
||||
super(config, setConfig, cache, setCache);
|
||||
this.columns = this.getDisplayColumns(model);
|
||||
this.filterable = false;
|
||||
this.sortable = false;
|
||||
}
|
||||
|
||||
getDisplayColumns(model: FreeTableModel) {
|
||||
return (
|
||||
model?.structure?.columns
|
||||
?.map((col) => this.getDisplayColumn(col))
|
||||
?.map((col) => ({
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
})) || []
|
||||
);
|
||||
}
|
||||
|
||||
getDisplayColumn( col: ColumnInfo) {
|
||||
const uniquePath = [col.columnName];
|
||||
const uniqueName = uniquePath.join('.');
|
||||
return {
|
||||
...col,
|
||||
pureName: 'data',
|
||||
schemaName: '',
|
||||
headerText: col.columnName,
|
||||
uniqueName,
|
||||
uniquePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { TableInfo } from '@dbgate/types';
|
||||
|
||||
export interface FreeTableModel {
|
||||
structure: TableInfo;
|
||||
rows: any[];
|
||||
}
|
||||
|
||||
export function createFreeTableModel() {
|
||||
return {
|
||||
structure: {
|
||||
columns: [
|
||||
{
|
||||
columnName: 'col1',
|
||||
},
|
||||
],
|
||||
foreignKeys: [],
|
||||
},
|
||||
rows: [
|
||||
{
|
||||
col1: 'val1',
|
||||
},
|
||||
{
|
||||
col1: 'val2',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -76,6 +76,10 @@ export abstract class GridDisplay {
|
||||
}));
|
||||
}
|
||||
|
||||
get hasReferences() {
|
||||
return false;
|
||||
}
|
||||
|
||||
get focusedColumn() {
|
||||
return this.config.focusedColumn;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface MacroArgument {
|
||||
type: 'text' | 'select';
|
||||
label: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface MacroDefinition {
|
||||
title: string;
|
||||
name: string;
|
||||
group: string;
|
||||
description?: string;
|
||||
type: 'transformValue';
|
||||
code: string;
|
||||
args?: MacroArgument[];
|
||||
}
|
||||
|
||||
export interface MacroSelectedCell {
|
||||
column: string;
|
||||
row: number;
|
||||
}
|
||||
@@ -218,4 +218,11 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get hasReferences() {
|
||||
if (!this.table) return false;
|
||||
if (this.table.foreignKeys && this.table.foreignKeys.length > 0) return true;
|
||||
if (this.table.dependencies && this.table.dependencies.length > 0) return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class ViewGridDisplay extends GridDisplay {
|
||||
this.columns = this.getDisplayColumns(view);
|
||||
this.filterable = true;
|
||||
this.sortable = true;
|
||||
this.editable = true;
|
||||
this.editable = false;
|
||||
}
|
||||
|
||||
getDisplayColumns(view: ViewInfo) {
|
||||
|
||||
@@ -5,3 +5,7 @@ export * from "./ViewGridDisplay";
|
||||
export * from "./JslGridDisplay";
|
||||
export * from "./ChangeSet";
|
||||
export * from "./filterName";
|
||||
export * from "./FreeTableGridDisplay";
|
||||
export * from "./FreeTableModel";
|
||||
export * from "./MacroDefinition";
|
||||
export * from "./runMacro";
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { FreeTableModel } from './FreeTableModel';
|
||||
import _ from 'lodash';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import uuidv4 from 'uuid/v4';
|
||||
import moment from 'moment';
|
||||
import { MacroDefinition, MacroSelectedCell } from './MacroDefinition';
|
||||
|
||||
const getMacroFunction = {
|
||||
transformValue: (code) => `
|
||||
(value, args, modules, rowIndex, row, columnName) => {
|
||||
${code}
|
||||
}
|
||||
`,
|
||||
transformRows: (code) => `
|
||||
(rows, args, modules, selectedCells, cols, columns) => {
|
||||
${code}
|
||||
}
|
||||
`,
|
||||
transformData: (code) => `
|
||||
(rows, args, modules, selectedCells, cols, columns) => {
|
||||
${code}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
const modules = {
|
||||
lodash: _,
|
||||
uuidv1,
|
||||
uuidv4,
|
||||
moment,
|
||||
};
|
||||
|
||||
function runTramsformValue(
|
||||
func,
|
||||
macroArgs: {},
|
||||
data: FreeTableModel,
|
||||
preview: boolean,
|
||||
selectedCells: MacroSelectedCell[],
|
||||
errors: string[] = []
|
||||
) {
|
||||
const selectedRows = _.groupBy(selectedCells, 'row');
|
||||
const rows = data.rows.map((row, rowIndex) => {
|
||||
const selectedRow = selectedRows[rowIndex];
|
||||
if (selectedRow) {
|
||||
const modifiedFields = [];
|
||||
let res = null;
|
||||
for (const cell of selectedRow) {
|
||||
const { column } = cell;
|
||||
const oldValue = row[column];
|
||||
let newValue = oldValue;
|
||||
try {
|
||||
newValue = func(oldValue, macroArgs, modules, rowIndex, row, column);
|
||||
} catch (err) {
|
||||
errors.push(`Error processing column ${column} on row ${rowIndex}: ${err.message}`);
|
||||
}
|
||||
if (newValue != oldValue) {
|
||||
if (res == null) {
|
||||
res = { ...row };
|
||||
}
|
||||
res[column] = newValue;
|
||||
if (preview) modifiedFields.push(column);
|
||||
}
|
||||
}
|
||||
if (res) {
|
||||
if (modifiedFields.length > 0) {
|
||||
return {
|
||||
...res,
|
||||
__modifiedFields: new Set(modifiedFields),
|
||||
};
|
||||
}
|
||||
return res;
|
||||
}
|
||||
return row;
|
||||
} else {
|
||||
return row;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
structure: data.structure,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function removePreviewRowFlags(rows) {
|
||||
rows = rows.filter((row) => row.__rowStatus != 'deleted');
|
||||
rows = rows.map((row) => {
|
||||
if (row.__rowStatus || row.__modifiedFields || row.__insertedFields || row.__deletedFields)
|
||||
return _.omit(row, ['__rowStatus', '__modifiedFields', '__insertedFields', '__deletedFields']);
|
||||
return row;
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
|
||||
function runTramsformRows(
|
||||
func,
|
||||
macroArgs: {},
|
||||
data: FreeTableModel,
|
||||
preview: boolean,
|
||||
selectedCells: MacroSelectedCell[],
|
||||
errors: string[] = []
|
||||
) {
|
||||
let rows = data.rows;
|
||||
try {
|
||||
rows = func(
|
||||
data.rows,
|
||||
macroArgs,
|
||||
modules,
|
||||
selectedCells,
|
||||
data.structure.columns.map((x) => x.columnName),
|
||||
data.structure.columns
|
||||
);
|
||||
if (!preview) {
|
||||
rows = removePreviewRowFlags(rows);
|
||||
}
|
||||
} catch (err) {
|
||||
errors.push(`Error processing rows: ${err.message}`);
|
||||
}
|
||||
return {
|
||||
structure: data.structure,
|
||||
rows,
|
||||
};
|
||||
}
|
||||
|
||||
function runTramsformData(
|
||||
func,
|
||||
macroArgs: {},
|
||||
data: FreeTableModel,
|
||||
preview: boolean,
|
||||
selectedCells: MacroSelectedCell[],
|
||||
errors: string[] = []
|
||||
) {
|
||||
try {
|
||||
let { rows, columns, cols } = func(
|
||||
data.rows,
|
||||
macroArgs,
|
||||
modules,
|
||||
selectedCells,
|
||||
data.structure.columns.map((x) => x.columnName),
|
||||
data.structure.columns
|
||||
);
|
||||
if (cols && !columns) {
|
||||
columns = cols.map((columnName) => ({ columnName }));
|
||||
}
|
||||
columns = _.uniqBy(columns, 'columnName');
|
||||
if (!preview) {
|
||||
rows = removePreviewRowFlags(rows);
|
||||
}
|
||||
return {
|
||||
structure: { columns },
|
||||
rows,
|
||||
};
|
||||
} catch (err) {
|
||||
errors.push(`Error processing data: ${err.message}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function runMacro(
|
||||
macro: MacroDefinition,
|
||||
macroArgs: {},
|
||||
data: FreeTableModel,
|
||||
preview: boolean,
|
||||
selectedCells: MacroSelectedCell[],
|
||||
errors: string[] = []
|
||||
): FreeTableModel {
|
||||
let func;
|
||||
try {
|
||||
func = eval(getMacroFunction[macro.type](macro.code));
|
||||
} catch (err) {
|
||||
errors.push(`Error compiling macro ${macro.name}: ${err.message}`);
|
||||
return data;
|
||||
}
|
||||
if (macro.type == 'transformValue') {
|
||||
return runTramsformValue(func, macroArgs, data, preview, selectedCells, errors);
|
||||
}
|
||||
if (macro.type == 'transformRows') {
|
||||
return runTramsformRows(func, macroArgs, data, preview, selectedCells, errors);
|
||||
}
|
||||
if (macro.type == 'transformData') {
|
||||
// @ts-ignore
|
||||
return runTramsformData(func, macroArgs, data, preview, selectedCells, errors);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -19,6 +19,8 @@
|
||||
"react": "^16.12.0",
|
||||
"react-ace": "^8.0.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dropzone": "^11.2.3",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-scripts": "3.3.0",
|
||||
"react-select": "^3.1.0",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.5 KiB |
@@ -0,0 +1,10 @@
|
||||
<?xml version='1.0' encoding='iso-8859-1'?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||
<g transform="translate(-326-532.36)">
|
||||
<rect width="18" height="6" x="329" y="537.36" fill="#88ff88" stroke="#2d2d2d" stroke-linejoin="round" stroke-linecap="round" stroke-width=".837" rx=".646"/>
|
||||
|
||||
<rect width="18" height="5" x="329" y="547.86" fill="#88ff88" stroke="#2d2d2d" stroke-linejoin="round" stroke-linecap="round" stroke-width=".837" rx=".646"/>
|
||||
|
||||
<path d="m507.95 46.02c-1.367-16.368-14.588-30.13-31.21-30.13h-444.96c-16.622 0-29.844 13.762-31.24 30.13h-.54v414.82c0 17.544 14.239 31.782 31.782 31.782h444.95c17.544 0 31.782-14.239 31.782-31.782v-414.82h-.571m-349.04 414.82h-127.13v-95.35h127.13v95.35m0-124.49h-127.13v-97.99h127.13v97.99m0-129.77h-127.13v-95.35h127.13v95.35m158.91 254.26h-127.13v-95.35h127.13v95.35m0-124.49h-127.13v-97.99h127.13v97.99m0-129.77h-127.13v-95.35h127.13v95.35m158.91 254.26h-127.13v-95.35h127.13v95.35m0-124.49h-127.13v-97.99h127.13v97.99m0-129.77h-127.13v-95.35h127.13v95.35" transform="matrix(.03776 0 0 .03776 328.4 534.76)" fill="#2d2d2d"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 491.556 491.556" style="enable-background:new 0 0 491.556 491.556;" xml:space="preserve">
|
||||
<path id="XMLID_680_" d="M480.537,132.385c-6.372-2.813-13.814-1.418-18.729,3.502l-49.891,49.868l-91.012-16.093l-16.101-91.018
|
||||
l49.894-49.867c4.911-4.921,6.307-12.356,3.5-18.728c-2.805-6.364-9.258-10.36-16.197-10.029
|
||||
C308,1.608,274.485,15.334,248.51,41.316c-39.887,39.872-50.913,97.498-33.435,147.364L19.369,384.429
|
||||
c-24.516,24.5-24.516,64.24,0,88.749c11.756,11.764,27.712,18.378,44.363,18.378c16.635,0,32.605-6.615,44.361-18.37l196.679-196.68
|
||||
c49.196,16.044,105.395,4.654,144.471-34.447c25.976-25.968,39.708-59.475,41.314-93.468
|
||||
C490.864,141.644,486.909,135.206,480.537,132.385z M94.214,435.747c-10.328,10.329-27.094,10.329-37.422,0
|
||||
c-10.346-10.344-10.346-27.094,0-37.439c10.328-10.327,27.094-10.327,37.422,0C104.544,408.653,104.544,425.402,94.214,435.747z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -13,6 +13,7 @@ import {
|
||||
import { SocketProvider } from './utility/SocketProvider';
|
||||
import ConnectionsPinger from './utility/ConnectionsPinger';
|
||||
import { ModalLayerProvider } from './modals/showModal';
|
||||
import UploadsProvider from './utility/UploadsProvider';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -26,7 +27,9 @@ function App() {
|
||||
<ConnectionsPinger>
|
||||
<ModalLayerProvider>
|
||||
<CurrentArchiveProvider>
|
||||
<Screen />
|
||||
<UploadsProvider>
|
||||
<Screen />
|
||||
</UploadsProvider>
|
||||
</CurrentArchiveProvider>
|
||||
</ModalLayerProvider>
|
||||
</ConnectionsPinger>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const TargetStyled = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: #aaaaff;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
z-index: 1000;
|
||||
`;
|
||||
|
||||
const InfoBox = styled.div``;
|
||||
|
||||
const IconWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
font-size: 50px;
|
||||
margin-bottom: 20px;
|
||||
`;
|
||||
|
||||
const InfoWrapper = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 10px;
|
||||
`;
|
||||
|
||||
const TitleWrapper = styled.div`
|
||||
font-size: 30px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
|
||||
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
|
||||
return (
|
||||
!!isDragActive && (
|
||||
<TargetStyled>
|
||||
<InfoBox>
|
||||
<IconWrapper>
|
||||
<i className="fas fa-cloud-upload-alt" />
|
||||
</IconWrapper>
|
||||
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
|
||||
<InfoWrapper>Supported file types: csv, MS Excel, json-lines</InfoWrapper>
|
||||
</InfoBox>
|
||||
<input {...inputProps} />
|
||||
</TargetStyled>
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import ToolBar from './widgets/Toolbar';
|
||||
import StatusBar from './widgets/StatusBar';
|
||||
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
|
||||
import { ModalLayer } from './modals/showModal';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget';
|
||||
import { useUploadsZone } from './utility/UploadsProvider';
|
||||
|
||||
const BodyDiv = styled.div`
|
||||
position: fixed;
|
||||
@@ -105,8 +107,11 @@ export default function Screen() {
|
||||
: theme.widgetMenu.iconSize;
|
||||
const toolbarPortalRef = React.useRef();
|
||||
const onSplitDown = useSplitterDrag('clientX', (diff) => setLeftPanelWidth((v) => v + diff));
|
||||
|
||||
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div {...getRootProps()}>
|
||||
<ToolBarDiv>
|
||||
<ToolBar toolbarPortalRef={toolbarPortalRef} />
|
||||
</ToolBarDiv>
|
||||
@@ -134,6 +139,8 @@ export default function Screen() {
|
||||
<StatusBar />
|
||||
</StausBarContainer>
|
||||
<ModalLayer />
|
||||
</>
|
||||
|
||||
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName } from '@dbgate/datalib';
|
||||
import { MacroIcon, StartIcon } from '../icons';
|
||||
|
||||
const macroAppObject = () => ({ name, type, title, group }, { setOpenedTabs }) => {
|
||||
const key = name;
|
||||
// const Icon = (props) => <i className="fas fa-archive" />;
|
||||
const Icon = MacroIcon;
|
||||
const matcher = (filter) => filterName(filter, name, title);
|
||||
const groupTitle = group;
|
||||
|
||||
return { title, key, Icon, groupTitle, matcher };
|
||||
};
|
||||
|
||||
export default macroAppObject;
|
||||
@@ -7,13 +7,52 @@ import { openNewTab } from '../utility/common';
|
||||
import { filterName } from '@dbgate/datalib';
|
||||
import axios from '../utility/axios';
|
||||
|
||||
function openArchive(setOpenedTabs, fileName, folderName) {
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: fileName,
|
||||
icon: 'archtable.svg',
|
||||
tooltip: `${folderName}\n${fileName}`,
|
||||
tabComponent: 'ArchiveFileTab',
|
||||
props: {
|
||||
archiveFile: fileName,
|
||||
archiveFolder: folderName,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function Menu({ data, setOpenedTabs }) {
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
||||
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
||||
};
|
||||
const handleOpenRead = () => {
|
||||
openArchive(setOpenedTabs, data.fileName, data.folderName);
|
||||
};
|
||||
const handleOpenWrite = async () => {
|
||||
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
|
||||
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: data.fileName,
|
||||
icon: 'freetable.svg',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialData: {
|
||||
functionName: 'archiveReader',
|
||||
props: {
|
||||
fileName: data.fileName,
|
||||
folderName: data.folderName,
|
||||
},
|
||||
},
|
||||
archiveFile: data.fileName,
|
||||
archiveFolder: data.folderName,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
@@ -24,16 +63,7 @@ const archiveFileAppObject = () => ({ fileName, folderName }, { setOpenedTabs })
|
||||
// const Icon = (props) => <i className="fas fa-archive" />;
|
||||
const Icon = ArchiveTableIcon;
|
||||
const onClick = () => {
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: fileName,
|
||||
icon: 'archtable.svg',
|
||||
tooltip: `${folderName}\n${fileName}`,
|
||||
tabComponent: 'ArchiveFileTab',
|
||||
props: {
|
||||
archiveFile: fileName,
|
||||
archiveFolder: folderName,
|
||||
},
|
||||
});
|
||||
openArchive(setOpenedTabs, fileName, folderName);
|
||||
};
|
||||
const matcher = (filter) => filterName(filter, fileName);
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ const menus = {
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
],
|
||||
views: [
|
||||
{
|
||||
@@ -51,6 +55,10 @@ const menus = {
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open structure',
|
||||
tab: 'TableStructureTab',
|
||||
@@ -113,7 +121,7 @@ function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
|
||||
{menus[data.objectTypeField].map((menu) => (
|
||||
<DropDownMenuItem
|
||||
key={menu.label}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
if (menu.isExport) {
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
@@ -127,6 +135,26 @@ function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
|
||||
}}
|
||||
/>
|
||||
));
|
||||
} else if (menu.isOpenFreeTable) {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: data.pureName,
|
||||
icon: 'freetable.svg',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialData: {
|
||||
functionName: 'tableReader',
|
||||
props: {
|
||||
connection: {
|
||||
...coninfo,
|
||||
database: data.database,
|
||||
},
|
||||
schemaName: data.schemaName,
|
||||
pureName: data.pureName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openDatabaseObjectDetail(setOpenedTabs, menu.tab, menu.sqlTemplate, data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { SelectField } from '../utility/inputs';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import styled from 'styled-components';
|
||||
import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView';
|
||||
import JsonCellView from './JsonCellDataView';
|
||||
|
||||
const Toolbar = styled.div`
|
||||
display: flex;
|
||||
background: #ccc;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const MainWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const DataWrapper = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const formats = [
|
||||
{
|
||||
type: 'textWrap',
|
||||
title: 'Text (wrap)',
|
||||
Component: TextCellViewWrap,
|
||||
single: true,
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
title: 'Text (no wrap)',
|
||||
Component: TextCellViewNoWrap,
|
||||
single: true,
|
||||
},
|
||||
{
|
||||
type: 'json',
|
||||
title: 'Json',
|
||||
Component: JsonCellView,
|
||||
single: true,
|
||||
},
|
||||
];
|
||||
|
||||
function autodetect(selection, grider, value) {
|
||||
if (_.isString(value)) {
|
||||
if (value.startsWith('[') || value.startsWith('{')) return 'json';
|
||||
}
|
||||
return 'textWrap';
|
||||
}
|
||||
|
||||
export default function CellDataView({ selection, grider }) {
|
||||
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
||||
let value = null;
|
||||
if (grider && selection.length == 1) {
|
||||
const rowData = grider.getRowData(selection[0].row);
|
||||
const { column } = selection[0];
|
||||
if (rowData) value = rowData[column];
|
||||
}
|
||||
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
||||
const autodetectFormat = formats.find((x) => x.type == autodetectFormatType);
|
||||
|
||||
const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
|
||||
const usedFormat = formats.find((x) => x.type == usedFormatType);
|
||||
|
||||
const { Component } = usedFormat || {};
|
||||
|
||||
return (
|
||||
<MainWrapper>
|
||||
<Toolbar>
|
||||
Format:
|
||||
<SelectField value={selectedFormatType} onChange={(e) => setSelectedFormatType(e.target.value)}>
|
||||
<option value="autodetect">Autodetect - {autodetectFormat.title}</option>
|
||||
|
||||
{formats.map((fmt) => (
|
||||
<option value={fmt.type} key={fmt.type}>
|
||||
{fmt.title}
|
||||
</option>
|
||||
))}
|
||||
</SelectField>
|
||||
</Toolbar>
|
||||
|
||||
<DataWrapper>
|
||||
{usedFormat == null || (usedFormat.single && value == null) ? (
|
||||
<ErrorInfo message="Must be selected one cell" />
|
||||
) : (
|
||||
<Component value={value} grider={grider} selection={selection} />
|
||||
)}
|
||||
</DataWrapper>
|
||||
</MainWrapper>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import ReactJson from 'react-json-view';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
|
||||
const OuterWrapper = styled.div`
|
||||
flex: 1;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const InnerWrapper = styled.div`
|
||||
overflow: scroll;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
export default function JsonCellView({ value }) {
|
||||
try {
|
||||
const json = JSON.parse(value);
|
||||
return (
|
||||
<OuterWrapper>
|
||||
<InnerWrapper>
|
||||
<ReactJson src={json} />
|
||||
</InnerWrapper>
|
||||
</OuterWrapper>
|
||||
);
|
||||
} catch (err) {
|
||||
return <ErrorInfo message="Error parsing JSON" />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const StyledInput = styled.textarea`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export function TextCellViewWrap({ value, grider, selection }) {
|
||||
return <StyledInput value={value} wrap="hard" readOnly />;
|
||||
}
|
||||
|
||||
export function TextCellViewNoWrap({ value, grider, selection }) {
|
||||
return (
|
||||
<StyledInput
|
||||
value={value}
|
||||
wrap="off"
|
||||
readOnly
|
||||
// readOnly={grider ? !grider.editable : true}
|
||||
// onChange={(e) => grider.setCellValue(selection[0].row, selection[0].column, e.target.value)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import {
|
||||
ChangeSet,
|
||||
changeSetContainsChanges,
|
||||
changeSetInsertNewRow,
|
||||
createChangeSet,
|
||||
deleteChangeSetRows,
|
||||
findExistingChangeSetItem,
|
||||
getChangeSetInsertedRows,
|
||||
GridDisplay,
|
||||
revertChangeSetRowChanges,
|
||||
setChangeSetValue,
|
||||
} from '@dbgate/datalib';
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
|
||||
export default class ChangeSetGrider extends Grider {
|
||||
public insertedRows: any[];
|
||||
public changeSet: ChangeSet;
|
||||
public setChangeSet: Function;
|
||||
private rowCacheIndexes: Set<number>;
|
||||
private rowDataCache;
|
||||
private rowStatusCache;
|
||||
private rowDefinitionsCache;
|
||||
private batchChangeSet: ChangeSet;
|
||||
|
||||
constructor(public sourceRows: any[], public changeSetState, public dispatchChangeSet, public display: GridDisplay) {
|
||||
super();
|
||||
this.changeSet = changeSetState && changeSetState.value;
|
||||
this.insertedRows = getChangeSetInsertedRows(this.changeSet, display.baseTable);
|
||||
this.setChangeSet = (value) => dispatchChangeSet({ type: 'set', value });
|
||||
this.rowCacheIndexes = new Set();
|
||||
this.rowDataCache = {};
|
||||
this.rowStatusCache = {};
|
||||
this.rowDefinitionsCache = {};
|
||||
this.batchChangeSet = null;
|
||||
}
|
||||
|
||||
getRowSource(index: number) {
|
||||
if (index < this.sourceRows.length) return this.sourceRows[index];
|
||||
return null;
|
||||
}
|
||||
|
||||
getInsertedRowIndex(index) {
|
||||
return index >= this.sourceRows.length ? index - this.sourceRows.length : null;
|
||||
}
|
||||
|
||||
requireRowCache(index: number) {
|
||||
if (this.rowCacheIndexes.has(index)) return;
|
||||
const row = this.getRowSource(index);
|
||||
const insertedRowIndex = this.getInsertedRowIndex(index);
|
||||
const rowDefinition = this.display.getChangeSetRow(row, insertedRowIndex);
|
||||
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(this.changeSet, rowDefinition);
|
||||
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
|
||||
let status = 'regular';
|
||||
if (matchedChangeSetItem && matchedField == 'updates') status = 'updated';
|
||||
if (matchedField == 'deletes') status = 'deleted';
|
||||
if (insertedRowIndex != null) status = 'inserted';
|
||||
const rowStatus = {
|
||||
status,
|
||||
modifiedFields:
|
||||
matchedChangeSetItem && matchedChangeSetItem.fields ? new Set(Object.keys(matchedChangeSetItem.fields)) : null,
|
||||
};
|
||||
this.rowDataCache[index] = rowUpdated;
|
||||
this.rowStatusCache[index] = rowStatus;
|
||||
this.rowDefinitionsCache[index] = rowDefinition;
|
||||
this.rowCacheIndexes.add(index);
|
||||
}
|
||||
|
||||
get editable() {
|
||||
return this.display.editable;
|
||||
}
|
||||
|
||||
get canInsert() {
|
||||
return !!this.display.baseTable;
|
||||
}
|
||||
|
||||
getRowData(index: number) {
|
||||
this.requireRowCache(index);
|
||||
return this.rowDataCache[index];
|
||||
}
|
||||
|
||||
getRowStatus(index): GriderRowStatus {
|
||||
this.requireRowCache(index);
|
||||
return this.rowStatusCache[index];
|
||||
}
|
||||
|
||||
get rowCount() {
|
||||
return this.sourceRows.length + this.insertedRows.length;
|
||||
}
|
||||
|
||||
applyModification(changeSetReducer) {
|
||||
if (this.batchChangeSet) {
|
||||
this.batchChangeSet = changeSetReducer(this.batchChangeSet);
|
||||
} else {
|
||||
this.setChangeSet(changeSetReducer(this.changeSet));
|
||||
}
|
||||
}
|
||||
|
||||
setCellValue(index: number, uniqueName: string, value: any) {
|
||||
const row = this.getRowSource(index);
|
||||
const definition = this.display.getChangeSetField(row, uniqueName, this.getInsertedRowIndex(index));
|
||||
this.applyModification((chs) => setChangeSetValue(chs, definition, value));
|
||||
}
|
||||
|
||||
deleteRow(index: number) {
|
||||
this.requireRowCache(index);
|
||||
this.applyModification((chs) => deleteChangeSetRows(chs, this.rowDefinitionsCache[index]));
|
||||
}
|
||||
|
||||
get rowCountInUpdate() {
|
||||
if (this.batchChangeSet) {
|
||||
const newRows = getChangeSetInsertedRows(this.batchChangeSet, this.display.baseTable);
|
||||
return this.sourceRows.length + newRows.length;
|
||||
} else {
|
||||
return this.rowCount;
|
||||
}
|
||||
}
|
||||
|
||||
insertRow(): number {
|
||||
const res = this.rowCountInUpdate;
|
||||
this.applyModification((chs) => changeSetInsertNewRow(chs, this.display.baseTable));
|
||||
return res;
|
||||
}
|
||||
|
||||
beginUpdate() {
|
||||
this.batchChangeSet = this.changeSet;
|
||||
}
|
||||
endUpdate() {
|
||||
this.setChangeSet(this.batchChangeSet);
|
||||
this.batchChangeSet = null;
|
||||
}
|
||||
|
||||
revertRowChanges(index: number) {
|
||||
this.requireRowCache(index);
|
||||
this.applyModification((chs) => revertChangeSetRowChanges(chs, this.rowDefinitionsCache[index]));
|
||||
}
|
||||
revertAllChanges() {
|
||||
this.applyModification((chs) => createChangeSet());
|
||||
}
|
||||
undo() {
|
||||
this.dispatchChangeSet({ type: 'undo' });
|
||||
}
|
||||
redo() {
|
||||
this.dispatchChangeSet({ type: 'redo' });
|
||||
}
|
||||
get canUndo() {
|
||||
return this.changeSetState.canUndo;
|
||||
}
|
||||
get canRedo() {
|
||||
return this.changeSetState.canRedo;
|
||||
}
|
||||
get containsChanges() {
|
||||
return changeSetContainsChanges(this.changeSet);
|
||||
}
|
||||
get disableLoadNextPage() {
|
||||
return this.insertedRows.length > 0;
|
||||
}
|
||||
|
||||
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
|
||||
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
|
||||
}
|
||||
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
|
||||
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,6 @@ import { ExpandIcon } from '../icons';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import { ManagerInnerContainer } from './ManagerStyles';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
@@ -89,17 +88,14 @@ function ColumnManagerRow(props) {
|
||||
export default function ColumnManager(props) {
|
||||
const { display } = props;
|
||||
const [columnFilter, setColumnFilter] = React.useState('');
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>Columns</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput
|
||||
placeholder="Search columns"
|
||||
filter={columnFilter}
|
||||
setFilter={setColumnFilter}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
<InlineButton onClick={() => display.hideAllColumns()}>Hide</InlineButton>
|
||||
<InlineButton onClick={() => display.showAllColumns()}>Show</InlineButton>
|
||||
|
||||
@@ -1,34 +1,12 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import DataGridCore from './DataGridCore';
|
||||
import ColumnManager from './ColumnManager';
|
||||
|
||||
import {
|
||||
// SearchBoxWrapper,
|
||||
// WidgetsInnerContainer,
|
||||
// Input,
|
||||
ManagerMainContainer,
|
||||
ManagerOuterContainer1,
|
||||
ManagerOuterContainer2,
|
||||
ManagerOuterContainerFull,
|
||||
WidgetTitle,
|
||||
} from './ManagerStyles';
|
||||
import ReferenceManager from './ReferenceManager';
|
||||
import { HorizontalSplitter } from '../widgets/Splitter';
|
||||
|
||||
const MainContainer = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const ColumnManagerContainer = styled.div`
|
||||
background-color: white;
|
||||
overflow-y: scroll;
|
||||
`;
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
||||
import CellDataView from '../celldata/CellDataView';
|
||||
import { FreeTableGridDisplay } from '@dbgate/datalib';
|
||||
|
||||
const LeftContainer = styled.div`
|
||||
background-color: white;
|
||||
@@ -41,50 +19,32 @@ const DataGridContainer = styled.div`
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function DataGrid(props) {
|
||||
const Container1 = props.showReferences ? ManagerOuterContainer1 : ManagerOuterContainerFull;
|
||||
const { GridCore } = props;
|
||||
const [managerSize, setManagerSize] = React.useState(0);
|
||||
const [selection, setSelection] = React.useState([]);
|
||||
const [grider, setGrider] = React.useState(null);
|
||||
return (
|
||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
||||
<LeftContainer>
|
||||
<ManagerMainContainer>
|
||||
<Container1>
|
||||
<ColumnManager {...props} managerSize={managerSize}/>
|
||||
</Container1>
|
||||
{props.showReferences && (
|
||||
<ManagerOuterContainer2>
|
||||
<ReferenceManager {...props} managerSize={managerSize}/>
|
||||
</ManagerOuterContainer2>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height="50%">
|
||||
<ColumnManager {...props} managerSize={managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
{props.showReferences && props.display.hasReferences && (
|
||||
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
|
||||
<ReferenceManager {...props} managerSize={managerSize} />
|
||||
</WidgetColumnBarItem>
|
||||
)}
|
||||
</ManagerMainContainer>
|
||||
<WidgetColumnBarItem title="Cell data" name="cellData" collapsed={props.isDetailView}>
|
||||
<CellDataView selection={selection} grider={grider} />
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</LeftContainer>
|
||||
|
||||
<DataGridContainer>
|
||||
<DataGridCore {...props} />
|
||||
<GridCore {...props} onSelectionChanged={setSelection} onChangeGrider={setGrider} />
|
||||
</DataGridContainer>
|
||||
</HorizontalSplitter>
|
||||
|
||||
// <MainContainer>
|
||||
// <LeftContainer style={{ width: 300 }}>
|
||||
// <ManagerMainContainer>
|
||||
// <Container1>
|
||||
// <ColumnManager {...props} />
|
||||
// </Container1>
|
||||
// {props.showReferences && (
|
||||
// <ManagerOuterContainer2>
|
||||
// <ReferenceManager {...props} />
|
||||
// </ManagerOuterContainer2>
|
||||
// )}
|
||||
// </ManagerMainContainer>
|
||||
// </LeftContainer>
|
||||
|
||||
// {/* <ColumnManagerContainer>
|
||||
// <ColumnManager {...props} />
|
||||
// </ColumnManagerContainer> */}
|
||||
// <DataGridContainer>
|
||||
// <DataGridCore {...props} />
|
||||
// </DataGridContainer>
|
||||
// </MainContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export default function DataGridContextMenu({
|
||||
exportGrid,
|
||||
filterSelectedValue,
|
||||
openQuery,
|
||||
openFreeTable,
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
@@ -34,11 +35,12 @@ export default function DataGridContextMenu({
|
||||
<DropDownMenuItem onClick={setNull} keyText="Ctrl+0">
|
||||
Set NULL
|
||||
</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={exportGrid}>Export</DropDownMenuItem>
|
||||
{exportGrid && <DropDownMenuItem onClick={exportGrid}>Export</DropDownMenuItem>}
|
||||
<DropDownMenuItem onClick={filterSelectedValue} keyText="Ctrl+F">
|
||||
Filter selected value
|
||||
</DropDownMenuItem>
|
||||
{openQuery && <DropDownMenuItem onClick={openQuery}>Open query</DropDownMenuItem>}
|
||||
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import ReactDOM from 'react-dom';
|
||||
import styled from 'styled-components';
|
||||
import { HorizontalScrollBar, VerticalScrollBar } from './ScrollBars';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
import axios from '../utility/axios';
|
||||
import DataFilterControl from './DataFilterControl';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { getFilterType, getFilterValueExpression } from '@dbgate/filterparser';
|
||||
@@ -18,19 +17,6 @@ import {
|
||||
filterCellsForRow,
|
||||
cellIsSelected,
|
||||
} from './gridutil';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
|
||||
import {
|
||||
changeSetToSql,
|
||||
createChangeSet,
|
||||
revertChangeSetRowChanges,
|
||||
getChangeSetInsertedRows,
|
||||
changeSetInsertNewRow,
|
||||
deleteChangeSetRows,
|
||||
batchUpdateChangeSet,
|
||||
setChangeSetValue,
|
||||
} from '@dbgate/datalib';
|
||||
import { scriptToSql } from '@dbgate/sqltree';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import DataGridToolbar from './DataGridToolbar';
|
||||
// import usePropsCompare from '../utility/usePropsCompare';
|
||||
@@ -38,12 +24,8 @@ import ColumnHeaderControl from './ColumnHeaderControl';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import { showMenu } from '../modals/DropDownMenu';
|
||||
import DataGridContextMenu from './DataGridContextMenu';
|
||||
import useSocket from '../utility/SocketProvider';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
|
||||
@@ -110,114 +92,35 @@ const RowCountLabel = styled.div`
|
||||
bottom: 20px;
|
||||
`;
|
||||
|
||||
const LoadingInfoWrapper = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
const LoadingInfoBox = styled.div`
|
||||
background-color: #ccc;
|
||||
padding: 10px;
|
||||
border: 1px solid gray;
|
||||
`;
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { display, conid, database, jslid } = props;
|
||||
|
||||
if (jslid) {
|
||||
const response = await axios.request({
|
||||
url: 'jsldata/get-rows',
|
||||
method: 'get',
|
||||
params: {
|
||||
jslid,
|
||||
offset,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
const sql = display.getPageQuery(offset, limit);
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql },
|
||||
});
|
||||
|
||||
if (response.data.errorMessage) return response.data;
|
||||
return response.data.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
const { display, jslid } = props;
|
||||
if (jslid) return true;
|
||||
const sql = display.getPageQuery(0, 1);
|
||||
return !!sql;
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
async function loadRowCount(props) {
|
||||
const { display, conid, database, jslid } = props;
|
||||
|
||||
if (jslid) {
|
||||
const response = await axios.request({
|
||||
url: 'jsldata/get-stats',
|
||||
method: 'get',
|
||||
params: {
|
||||
jslid,
|
||||
},
|
||||
});
|
||||
return response.data.rowCount;
|
||||
}
|
||||
|
||||
const sql = display.getCountQuery();
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql },
|
||||
});
|
||||
|
||||
return parseInt(response.data.rows[0].count);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function DataGridCore(props) {
|
||||
const { conid, database, display, changeSetState, dispatchChangeSet, tabVisible, jslid } = props;
|
||||
const {
|
||||
display,
|
||||
conid,
|
||||
database,
|
||||
tabVisible,
|
||||
loadNextData,
|
||||
errorMessage,
|
||||
isLoadedAll,
|
||||
loadedTime,
|
||||
exportGrid,
|
||||
allRowCount,
|
||||
openQuery,
|
||||
onSave,
|
||||
isLoading,
|
||||
grider,
|
||||
onSelectionChanged,
|
||||
frameSelection,
|
||||
onKeyDown,
|
||||
} = props;
|
||||
// console.log('RENDER GRID', display.baseTable.pureName);
|
||||
const columns = React.useMemo(() => display.allColumns, [display]);
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
// usePropsCompare(props);
|
||||
|
||||
// console.log(`GRID, conid=${conid}, database=${database}, sql=${sql}`);
|
||||
const [loadProps, setLoadProps] = React.useState({
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
allRowCount: null,
|
||||
errorMessage: null,
|
||||
jslStatsCounter: 0,
|
||||
jslChangeIndex: 0,
|
||||
});
|
||||
const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps;
|
||||
|
||||
const loadedTimeRef = React.useRef(0);
|
||||
const focusFieldRef = React.useRef(null);
|
||||
|
||||
const [vScrollValueToSet, setvScrollValueToSet] = React.useState();
|
||||
@@ -234,19 +137,6 @@ export default function DataGridCore(props) {
|
||||
const [autofillSelectedCells, setAutofillSelectedCells] = React.useState(emptyCellArray);
|
||||
const [focusFilterInputs, setFocusFilterInputs] = React.useState({});
|
||||
|
||||
// const [inplaceEditorCell, setInplaceEditorCell] = React.useState(nullCell);
|
||||
// const [inplaceEditorInitText, setInplaceEditorInitText] = React.useState('');
|
||||
// const [inplaceEditorShouldSave, setInplaceEditorShouldSave] = React.useState(false);
|
||||
// const [inplaceEditorChangedOnCreate, setInplaceEditorChangedOnCreate] = React.useState(false);
|
||||
|
||||
const changeSet = changeSetState && changeSetState.value;
|
||||
const setChangeSet = React.useCallback((value) => dispatchChangeSet({ type: 'set', value }), [dispatchChangeSet]);
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
const changeSetRef = React.useRef(changeSet);
|
||||
|
||||
changeSetRef.current = changeSet;
|
||||
|
||||
const autofillMarkerCell = React.useMemo(
|
||||
() =>
|
||||
selectedCells && selectedCells.length > 0 && _.uniq(selectedCells.map((x) => x[0])).length == 1
|
||||
@@ -255,81 +145,17 @@ export default function DataGridCore(props) {
|
||||
[selectedCells]
|
||||
);
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
||||
const handleLoadRowCount = async () => {
|
||||
const rowCount = await loadRowCount(props);
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
allRowCount: rowCount,
|
||||
}));
|
||||
};
|
||||
|
||||
const loadNextData = async () => {
|
||||
if (isLoading) return;
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: true,
|
||||
}));
|
||||
const loadStart = new Date().getTime();
|
||||
loadedTimeRef.current = loadStart;
|
||||
|
||||
const nextRows = await loadDataPage(props, loadedRows.length, 100);
|
||||
if (loadedTimeRef.current !== loadStart) {
|
||||
// new load was dispatched
|
||||
return;
|
||||
}
|
||||
// if (!_.isArray(nextRows)) {
|
||||
// console.log('Error loading data from server', nextRows);
|
||||
// nextRows = [];
|
||||
// }
|
||||
// console.log('nextRows', nextRows);
|
||||
if (nextRows.errorMessage) {
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
errorMessage: nextRows.errorMessage,
|
||||
}));
|
||||
} else {
|
||||
if (allRowCount == null) handleLoadRowCount();
|
||||
const loadedInfo = {
|
||||
loadedRows: [...loadedRows, ...nextRows],
|
||||
loadedTime,
|
||||
};
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
isLoadedAll: oldLoadProps.jslStatsCounter == loadProps.jslStatsCounter && nextRows.length === 0,
|
||||
...loadedInfo,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
// const data = useFetch({
|
||||
// url: 'database-connections/query-data',
|
||||
// method: 'post',
|
||||
// params: {
|
||||
// conid,
|
||||
// database,
|
||||
// },
|
||||
// data: { sql },
|
||||
// });
|
||||
// const { rows, columns } = data || {};
|
||||
const [firstVisibleRowScrollIndex, setFirstVisibleRowScrollIndex] = React.useState(0);
|
||||
const [firstVisibleColumnScrollIndex, setFirstVisibleColumnScrollIndex] = React.useState(0);
|
||||
const socket = useSocket();
|
||||
|
||||
const [headerRowRef, { height: rowHeight }] = useDimensions();
|
||||
const [tableBodyRef] = useDimensions();
|
||||
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
|
||||
// const [tableRef, { height: tableHeight, width: tableWidth }] = useDimensions();
|
||||
const confirmSqlModalState = useModalState();
|
||||
const [confirmSql, setConfirmSql] = React.useState('');
|
||||
|
||||
const [inplaceEditorState, dispatchInsplaceEditor] = React.useReducer((state, action) => {
|
||||
switch (action.type) {
|
||||
case 'show':
|
||||
if (!display.editable) return {};
|
||||
if (!grider.editable) return {};
|
||||
return {
|
||||
cell: action.cell,
|
||||
text: action.text,
|
||||
@@ -355,8 +181,8 @@ export default function DataGridCore(props) {
|
||||
|
||||
// usePropsCompare({ loadedRows, columns, containerWidth, display });
|
||||
|
||||
const columnSizes = React.useMemo(() => countColumnSizes(loadedRows, columns, containerWidth, display), [
|
||||
loadedRows,
|
||||
const columnSizes = React.useMemo(() => countColumnSizes(grider, columns, containerWidth, display), [
|
||||
grider,
|
||||
columns,
|
||||
containerWidth,
|
||||
display,
|
||||
@@ -376,93 +202,28 @@ export default function DataGridCore(props) {
|
||||
// console.log('visibleRowCountUpperBound', visibleRowCountUpperBound);
|
||||
// console.log('rowHeight', rowHeight);
|
||||
|
||||
const reload = () => {
|
||||
setLoadProps({
|
||||
allRowCount: null,
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
errorMessage: null,
|
||||
jslStatsCounter: 0,
|
||||
jslChangeIndex: 0,
|
||||
});
|
||||
};
|
||||
|
||||
const insertedRows = getChangeSetInsertedRows(changeSet, display.baseTable);
|
||||
|
||||
const rowCountNewIncluded = loadedRows.length + insertedRows.length;
|
||||
|
||||
React.useEffect(() => {
|
||||
if (
|
||||
!isLoadedAll &&
|
||||
!errorMessage &&
|
||||
firstVisibleRowScrollIndex + visibleRowCountUpperBound >= loadedRows.length &&
|
||||
insertedRows.length == 0
|
||||
) {
|
||||
if (dataPageAvailable(props)) {
|
||||
// If not, callbacks to load missing metadata are dispatched
|
||||
loadNextData();
|
||||
}
|
||||
}
|
||||
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
|
||||
display.reload();
|
||||
}
|
||||
if (display.cache.refreshTime > loadedTime) {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (tabVisible) {
|
||||
if (focusFieldRef.current) focusFieldRef.current.focus();
|
||||
}
|
||||
}, [tabVisible, focusFieldRef.current]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onSelectionChanged) {
|
||||
onSelectionChanged(getSelectedCellsPublished());
|
||||
}
|
||||
}, [onSelectionChanged, selectedCells]);
|
||||
|
||||
const maxScrollColumn = React.useMemo(() => {
|
||||
let newColumn = columnSizes.scrollInView(0, columns.length - 1 - columnSizes.frozenCount, gridScrollAreaWidth);
|
||||
return newColumn;
|
||||
}, [columnSizes, gridScrollAreaWidth]);
|
||||
|
||||
const handleJslDataStats = React.useCallback((stats) => {
|
||||
if (stats.changeIndex < loadProps.jslChangeIndex) return;
|
||||
setLoadProps((oldProps) => ({
|
||||
...oldProps,
|
||||
allRowCount: stats.rowCount,
|
||||
isLoadedAll: false,
|
||||
jslStatsCounter: oldProps.jslStatsCounter + 1,
|
||||
jslChangeIndex: stats.changeIndex,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (jslid && socket) {
|
||||
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
return () => {
|
||||
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
};
|
||||
}
|
||||
}, [jslid]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.onReferenceSourceChanged && ((loadedRows && loadedRows.length > 0) || isLoadedAll)) {
|
||||
if (props.onReferenceSourceChanged && (grider.rowCount > 0 || isLoadedAll)) {
|
||||
props.onReferenceSourceChanged(getSelectedRowData(), loadedTime);
|
||||
}
|
||||
}, [selectedCells, props.refReloadToken, loadedRows && loadedRows[0]]);
|
||||
|
||||
// const handleCloseInplaceEditor = React.useCallback(
|
||||
// mode => {
|
||||
// const [row, col] = currentCell || [];
|
||||
// setInplaceEditorCell(null);
|
||||
// setInplaceEditorInitText(null);
|
||||
// setInplaceEditorShouldSave(false);
|
||||
// if (tableElement) tableElement.focus();
|
||||
// // @ts-ignore
|
||||
// if (mode == 'enter' && row) moveCurrentCell(row + 1, col);
|
||||
// if (mode == 'save') setTimeout(handleSave, 1);
|
||||
// },
|
||||
// [tableElement, currentCell]
|
||||
// );
|
||||
}, [selectedCells, props.refReloadToken, grider.getRowData(0)]);
|
||||
|
||||
// usePropsCompare({ columnSizes, firstVisibleColumnScrollIndex, gridScrollAreaWidth, columns });
|
||||
|
||||
@@ -487,6 +248,12 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
}, [display && display.focusedColumn]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (loadNextData && firstVisibleRowScrollIndex + visibleRowCountUpperBound >= grider.rowCount) {
|
||||
loadNextData();
|
||||
}
|
||||
});
|
||||
|
||||
React.useEffect(() => {
|
||||
if (display.groupColumns) {
|
||||
props.onReferenceClick({
|
||||
@@ -504,7 +271,7 @@ export default function DataGridCore(props) {
|
||||
const rowCountInfo = React.useMemo(() => {
|
||||
if (selectedCells.length > 1 && selectedCells.every((x) => _.isNumber(x[0]) && _.isNumber(x[1]))) {
|
||||
let sum = _.sumBy(selectedCells, (cell) => {
|
||||
const row = loadedRows[cell[0]];
|
||||
const row = grider.getRowData(cell[0]);
|
||||
if (row) {
|
||||
const colName = realColumnUniqueNames[cell[1]];
|
||||
if (colName) {
|
||||
@@ -523,24 +290,24 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
if (allRowCount == null) return 'Loading row count...';
|
||||
return `Rows: ${allRowCount.toLocaleString()}`;
|
||||
// if (this.isLoadingFirstPage) return "Loading first page...";
|
||||
// if (this.isFirstPageError) return "Error loading first page";
|
||||
// return `Rows: ${this.rowCount.toLocaleString()}`;
|
||||
}, [selectedCells, allRowCount, loadedRows, visibleRealColumns]);
|
||||
}, [selectedCells, allRowCount, grider, visibleRealColumns]);
|
||||
|
||||
if (!loadedRows || !columns || columns.length == 0)
|
||||
return (
|
||||
<LoadingInfoWrapper>
|
||||
<LoadingInfoBox>
|
||||
<LoadingInfo message="Waiting for structure" />
|
||||
</LoadingInfoBox>
|
||||
</LoadingInfoWrapper>
|
||||
);
|
||||
if (!columns || columns.length == 0) return <LoadingInfo wrapper message="Waiting for structure" />;
|
||||
|
||||
if (errorMessage) {
|
||||
return <ErrorInfo message={errorMessage} />;
|
||||
}
|
||||
|
||||
if (grider.errors && grider.errors.length > 0) {
|
||||
return (
|
||||
<div>
|
||||
{grider.errors.map((err, index) => (
|
||||
<ErrorInfo message={err} key={index} isSmall />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const handleRowScroll = (value) => {
|
||||
setFirstVisibleRowScrollIndex(value);
|
||||
};
|
||||
@@ -549,6 +316,24 @@ export default function DataGridCore(props) {
|
||||
setFirstVisibleColumnScrollIndex(value);
|
||||
};
|
||||
|
||||
const handleOpenFreeTable = () => {
|
||||
const columns = getSelectedColumns();
|
||||
const rows = getSelectedRowData().map((row) => _.pickBy(row, (v, col) => columns.find((x) => x.columnName == col)));
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: 'selection',
|
||||
icon: 'freetable.svg',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialData: {
|
||||
structure: {
|
||||
columns,
|
||||
},
|
||||
rows,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleContextMenu = (event) => {
|
||||
event.preventDefault();
|
||||
showMenu(
|
||||
@@ -563,7 +348,8 @@ export default function DataGridCore(props) {
|
||||
setNull={setNull}
|
||||
exportGrid={exportGrid}
|
||||
filterSelectedValue={filterSelectedValue}
|
||||
openQuery={display.baseTable ? openQuery : null}
|
||||
openQuery={openQuery}
|
||||
openFreeTable={handleOpenFreeTable}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -617,39 +403,8 @@ export default function DataGridCore(props) {
|
||||
copyToClipboard();
|
||||
}
|
||||
|
||||
function exportGrid() {
|
||||
const initialValues = {};
|
||||
if (jslid) {
|
||||
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
|
||||
if (archiveMatch) {
|
||||
initialValues.sourceStorageType = 'archive';
|
||||
initialValues.sourceArchiveFolder = archiveMatch[1];
|
||||
initialValues.sourceList = [archiveMatch[2]];
|
||||
} else {
|
||||
initialValues.sourceStorageType = 'jsldata';
|
||||
initialValues.sourceJslId = jslid;
|
||||
initialValues.sourceList = ['query-data'];
|
||||
}
|
||||
} else {
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceSql = display.getExportQuery();
|
||||
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
|
||||
}
|
||||
showModal((modalState) => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
||||
}
|
||||
|
||||
function setCellValue(chs, cell, value) {
|
||||
return setChangeSetValue(
|
||||
chs,
|
||||
display.getChangeSetField(
|
||||
loadedAndInsertedRows[cell[0]],
|
||||
realColumnUniqueNames[cell[1]],
|
||||
cell[0] >= loadedRows.length ? cell[0] - loadedRows.length : null
|
||||
),
|
||||
value
|
||||
);
|
||||
function setCellValue(cell, value) {
|
||||
grider.setCellValue(cell[0], realColumnUniqueNames[cell[1]], value);
|
||||
}
|
||||
|
||||
function handlePaste(event) {
|
||||
@@ -663,62 +418,49 @@ export default function DataGridCore(props) {
|
||||
pastedText = event.clipboardData.getData('text/plain');
|
||||
}
|
||||
event.preventDefault();
|
||||
grider.beginUpdate();
|
||||
const pasteRows = pastedText
|
||||
.replace(/\r/g, '')
|
||||
.split('\n')
|
||||
.map((row) => row.split('\t'));
|
||||
let chs = changeSet;
|
||||
let allRows = loadedAndInsertedRows;
|
||||
|
||||
if (selectedCells.length <= 1) {
|
||||
const startRow = isRegularCell(currentCell) ? currentCell[0] : loadedAndInsertedRows.length;
|
||||
const selectedRegular = cellsToRegularCells(selectedCells);
|
||||
if (selectedRegular.length <= 1) {
|
||||
const startRow = isRegularCell(currentCell) ? currentCell[0] : grider.rowCount;
|
||||
const startCol = isRegularCell(currentCell) ? currentCell[1] : 0;
|
||||
let rowIndex = startRow;
|
||||
for (const rowData of pasteRows) {
|
||||
if (rowIndex >= allRows.length) {
|
||||
chs = changeSetInsertNewRow(chs, display.baseTable);
|
||||
allRows = [...loadedRows, ...getChangeSetInsertedRows(chs, display.baseTable)];
|
||||
if (rowIndex >= grider.rowCountInUpdate) {
|
||||
grider.insertRow();
|
||||
}
|
||||
let colIndex = startCol;
|
||||
const row = allRows[rowIndex];
|
||||
for (const cell of rowData) {
|
||||
chs = setChangeSetValue(
|
||||
chs,
|
||||
display.getChangeSetField(
|
||||
row,
|
||||
realColumnUniqueNames[colIndex],
|
||||
rowIndex >= loadedRows.length ? rowIndex - loadedRows.length : null
|
||||
),
|
||||
cell == '(NULL)' ? null : cell
|
||||
);
|
||||
setCellValue([rowIndex, colIndex], cell == '(NULL)' ? null : cell);
|
||||
colIndex += 1;
|
||||
}
|
||||
rowIndex += 1;
|
||||
}
|
||||
}
|
||||
if (selectedCells.length > 1) {
|
||||
const regularSelected = selectedCells.filter(isRegularCell);
|
||||
const startRow = _.min(regularSelected.map((x) => x[0]));
|
||||
const startCol = _.min(regularSelected.map((x) => x[1]));
|
||||
for (const cell of regularSelected) {
|
||||
if (selectedRegular.length > 1) {
|
||||
const startRow = _.min(selectedRegular.map((x) => x[0]));
|
||||
const startCol = _.min(selectedRegular.map((x) => x[1]));
|
||||
for (const cell of selectedRegular) {
|
||||
const [rowIndex, colIndex] = cell;
|
||||
const selectionRow = rowIndex - startRow;
|
||||
const selectionCol = colIndex - startCol;
|
||||
const pasteRow = pasteRows[selectionRow % pasteRows.length];
|
||||
const pasteCell = pasteRow[selectionCol % pasteRow.length];
|
||||
chs = setCellValue(chs, cell, pasteCell);
|
||||
setCellValue(cell, pasteCell);
|
||||
}
|
||||
}
|
||||
|
||||
setChangeSet(chs);
|
||||
grider.endUpdate();
|
||||
}
|
||||
|
||||
function setNull() {
|
||||
let chs = changeSet;
|
||||
grider.beginUpdate();
|
||||
selectedCells.filter(isRegularCell).forEach((cell) => {
|
||||
chs = setCellValue(chs, cell, null);
|
||||
setCellValue(cell, null);
|
||||
});
|
||||
setChangeSet(chs);
|
||||
grider.endUpdate();
|
||||
}
|
||||
|
||||
function cellsToRegularCells(cells) {
|
||||
@@ -733,7 +475,7 @@ export default function DataGridCore(props) {
|
||||
cells = _.flatten(
|
||||
cells.map((cell) => {
|
||||
if (cell[0] == 'header') {
|
||||
return _.range(0, allRowCount).map((row) => [row, cell[1]]);
|
||||
return _.range(0, grider.rowCount).map((row) => [row, cell[1]]);
|
||||
}
|
||||
return [cell];
|
||||
})
|
||||
@@ -746,7 +488,7 @@ export default function DataGridCore(props) {
|
||||
const rowIndexes = _.sortBy(_.uniq(cells.map((x) => x[0])));
|
||||
const lines = rowIndexes.map((rowIndex) => {
|
||||
let colIndexes = _.sortBy(cells.filter((x) => x[0] == rowIndex).map((x) => x[1]));
|
||||
const rowData = loadedAndInsertedRows[rowIndex];
|
||||
const rowData = grider.getRowData(rowIndex);
|
||||
if (!rowData) return '';
|
||||
const line = colIndexes
|
||||
.map((col) => realColumnUniqueNames[col])
|
||||
@@ -784,17 +526,11 @@ export default function DataGridCore(props) {
|
||||
const currentRowNumber = currentCell[0];
|
||||
if (_.isNumber(currentRowNumber)) {
|
||||
const rowIndexes = _.uniq((autofillSelectedCells || []).map((x) => x[0])).filter((x) => x != currentRowNumber);
|
||||
// @ts-ignore
|
||||
const colNames = selectedCells.map((cell) => realColumnUniqueNames[cell[1]]);
|
||||
const changeObject = _.pick(loadedAndInsertedRows[currentRowNumber], colNames);
|
||||
setChangeSet(
|
||||
batchUpdateChangeSet(
|
||||
changeSet,
|
||||
getRowDefinitions(rowIndexes),
|
||||
// @ts-ignore
|
||||
rowIndexes.map(() => changeObject)
|
||||
)
|
||||
);
|
||||
const changeObject = _.pick(grider.getRowData(currentRowNumber), colNames);
|
||||
grider.beginUpdate();
|
||||
for (const index of rowIndexes) grider.updateRow(index, changeObject);
|
||||
grider.endUpdate();
|
||||
}
|
||||
|
||||
setAutofillDragStartCell(null);
|
||||
@@ -803,36 +539,60 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
}
|
||||
|
||||
function getRowDefinitions(rowIndexes) {
|
||||
const res = [];
|
||||
if (!loadedAndInsertedRows) return res;
|
||||
for (const index of rowIndexes) {
|
||||
if (loadedAndInsertedRows[index] && _.isNumber(index)) {
|
||||
const insertedRowIndex = index >= loadedRows.length ? index - loadedRows.length : null;
|
||||
res.push(display.getChangeSetRow(loadedAndInsertedRows[index], insertedRowIndex));
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
function getSelectedRowIndexes() {
|
||||
return _.uniq((selectedCells || []).map((x) => x[0]));
|
||||
if (selectedCells.find((x) => x[0] == 'header')) return _.range(0, grider.rowCount);
|
||||
return _.uniq((selectedCells || []).map((x) => x[0])).filter((x) => _.isNumber(x));
|
||||
}
|
||||
|
||||
function getSelectedRowDefinitions() {
|
||||
return getRowDefinitions(getSelectedRowIndexes());
|
||||
function getSelectedColumnIndexes() {
|
||||
if (selectedCells.find((x) => x[1] == 'header')) return _.range(0, realColumnUniqueNames.length);
|
||||
return _.uniq((selectedCells || []).map((x) => x[1])).filter((x) => _.isNumber(x));
|
||||
}
|
||||
|
||||
function getSelectedCellsPublished() {
|
||||
const regular = cellsToRegularCells(selectedCells);
|
||||
// @ts-ignore
|
||||
return regular
|
||||
.map((cell) => ({
|
||||
row: cell[0],
|
||||
column: realColumnUniqueNames[cell[1]],
|
||||
}))
|
||||
.filter((x) => x.column);
|
||||
|
||||
// return regular.map((cell) => {
|
||||
// const row = cell[0];
|
||||
// const column = realColumnUniqueNames[cell[1]];
|
||||
// let value = null;
|
||||
// if (grider && column) {
|
||||
// let rowData = grider.getRowData(row);
|
||||
// if (rowData) value = rowData[column];
|
||||
// }
|
||||
// return {
|
||||
// row,
|
||||
// column,
|
||||
// value,
|
||||
// };
|
||||
// });
|
||||
}
|
||||
|
||||
function getSelectedRowData() {
|
||||
return _.compact(getSelectedRowIndexes().map((index) => loadedRows && loadedRows[index]));
|
||||
return _.compact(getSelectedRowIndexes().map((index) => grider.getRowData(index)));
|
||||
}
|
||||
|
||||
function getSelectedColumns() {
|
||||
return _.compact(
|
||||
getSelectedColumnIndexes().map((index) => ({
|
||||
columnName: realColumnUniqueNames[index],
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function revertRowChanges() {
|
||||
const updatedChangeSet = getSelectedRowDefinitions().reduce(
|
||||
(chs, row) => revertChangeSetRowChanges(chs, row),
|
||||
changeSet
|
||||
);
|
||||
setChangeSet(updatedChangeSet);
|
||||
grider.beginUpdate();
|
||||
for (const index of getSelectedRowIndexes()) {
|
||||
if (_.isNumber(index)) grider.revertRowChanges(index);
|
||||
}
|
||||
grider.endUpdate();
|
||||
}
|
||||
|
||||
function filterSelectedValue() {
|
||||
@@ -841,7 +601,7 @@ export default function DataGridCore(props) {
|
||||
if (!isRegularCell(cell)) continue;
|
||||
const modelIndex = columnSizes.realToModel(cell[1]);
|
||||
const columnName = columns[modelIndex].uniqueName;
|
||||
let value = loadedRows[cell[0]][columnName];
|
||||
let value = grider.getRowData(cell[0])[columnName];
|
||||
let svalue = getFilterValueExpression(value, columns[modelIndex].dataType);
|
||||
if (_.has(flts, columnName)) flts[columnName] += ',' + svalue;
|
||||
else flts[columnName] = svalue;
|
||||
@@ -850,28 +610,12 @@ export default function DataGridCore(props) {
|
||||
display.setFilters(flts);
|
||||
}
|
||||
|
||||
function openQuery() {
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: 'Query',
|
||||
icon: 'sql.svg',
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
initialScript: display.getExportQuery(),
|
||||
schemaName: display.baseTable.schemaName,
|
||||
pureName: display.baseTable.pureName,
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function revertAllChanges() {
|
||||
setChangeSet(createChangeSet());
|
||||
}
|
||||
|
||||
function deleteSelectedRows() {
|
||||
const updatedChangeSet = getSelectedRowDefinitions().reduce((chs, row) => deleteChangeSetRows(chs, row), changeSet);
|
||||
setChangeSet(updatedChangeSet);
|
||||
grider.beginUpdate();
|
||||
for (const index of getSelectedRowIndexes()) {
|
||||
if (_.isNumber(index)) grider.deleteRow(index);
|
||||
}
|
||||
grider.endUpdate();
|
||||
}
|
||||
|
||||
function handleGridWheel(event) {
|
||||
@@ -882,7 +626,7 @@ export default function DataGridCore(props) {
|
||||
if (event.deltaY < 0) {
|
||||
newFirstVisibleRowScrollIndex -= wheelRowCount;
|
||||
}
|
||||
let rowCount = rowCountNewIncluded;
|
||||
let rowCount = grider.rowCount;
|
||||
if (newFirstVisibleRowScrollIndex + visibleRowCountLowerBound > rowCount) {
|
||||
newFirstVisibleRowScrollIndex = rowCount - visibleRowCountLowerBound + 1;
|
||||
}
|
||||
@@ -895,17 +639,11 @@ export default function DataGridCore(props) {
|
||||
setvScrollValueToSetDate(new Date());
|
||||
}
|
||||
|
||||
// async function blurEditorAndSave() {
|
||||
// setInplaceEditorCell(null);
|
||||
// setInplaceEditorInitText(null);
|
||||
// await sleep(1);
|
||||
// }
|
||||
|
||||
function undo() {
|
||||
dispatchChangeSet({ type: 'undo' });
|
||||
grider.undo();
|
||||
}
|
||||
function redo() {
|
||||
dispatchChangeSet({ type: 'redo' });
|
||||
grider.redo();
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
@@ -914,39 +652,13 @@ export default function DataGridCore(props) {
|
||||
dispatchInsplaceEditor({ type: 'shouldSave' });
|
||||
return;
|
||||
}
|
||||
const script = changeSetToSql(changeSetRef.current, display.dbinfo);
|
||||
const sql = scriptToSql(display.driver, script);
|
||||
setConfirmSql(sql);
|
||||
confirmSqlModalState.open();
|
||||
}
|
||||
|
||||
async function handleConfirmSql() {
|
||||
const resp = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql: confirmSql },
|
||||
});
|
||||
|
||||
const { errorMessage } = resp.data || {};
|
||||
if (errorMessage) {
|
||||
showModal((modalState) => (
|
||||
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
|
||||
));
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
setConfirmSql(null);
|
||||
display.reload();
|
||||
}
|
||||
if (onSave) onSave();
|
||||
}
|
||||
|
||||
const insertNewRow = () => {
|
||||
if (display.baseTable) {
|
||||
setChangeSet(changeSetInsertNewRow(changeSet, display.baseTable));
|
||||
const cell = [rowCountNewIncluded, (currentCell && currentCell[1]) || 0];
|
||||
if (grider.canInsert) {
|
||||
const rowIndex = grider.insertRow();
|
||||
const cell = [rowIndex, (currentCell && currentCell[1]) || 0];
|
||||
// @ts-ignore
|
||||
setCurrentCell(cell);
|
||||
// @ts-ignore
|
||||
@@ -967,6 +679,10 @@ export default function DataGridCore(props) {
|
||||
};
|
||||
|
||||
function handleGridKeyDown(event) {
|
||||
if (onKeyDown) {
|
||||
onKeyDown(event);
|
||||
}
|
||||
|
||||
if (event.keyCode == keycodes.f5) {
|
||||
event.preventDefault();
|
||||
display.reload();
|
||||
@@ -1060,7 +776,7 @@ export default function DataGridCore(props) {
|
||||
|
||||
function handleCursorMove(event) {
|
||||
if (!isRegularCell(currentCell)) return null;
|
||||
let rowCount = rowCountNewIncluded;
|
||||
let rowCount = grider.rowCount;
|
||||
if (event.ctrlKey) {
|
||||
switch (event.keyCode) {
|
||||
case keycodes.upArrow:
|
||||
@@ -1118,7 +834,7 @@ export default function DataGridCore(props) {
|
||||
}
|
||||
|
||||
function moveCurrentCell(row, col, event = null) {
|
||||
const rowCount = rowCountNewIncluded;
|
||||
const rowCount = grider.rowCount;
|
||||
|
||||
if (row < 0) row = 0;
|
||||
if (row >= rowCount) row = rowCount - 1;
|
||||
@@ -1140,7 +856,7 @@ export default function DataGridCore(props) {
|
||||
|
||||
if (row != null) {
|
||||
let newRow = null;
|
||||
const rowCount = rowCountNewIncluded;
|
||||
const rowCount = grider.rowCount;
|
||||
if (rowCount == 0) return;
|
||||
|
||||
if (row < firstVisibleRowScrollIndex) newRow = row;
|
||||
@@ -1202,7 +918,7 @@ export default function DataGridCore(props) {
|
||||
// columnSizes.getVisibleScrollSizeSum()
|
||||
// );
|
||||
|
||||
const loadedAndInsertedRows = [...loadedRows, ...insertedRows];
|
||||
// const loadedAndInsertedRows = [...loadedRows, ...insertedRows];
|
||||
|
||||
// console.log('focusFieldRef.current', focusFieldRef.current);
|
||||
|
||||
@@ -1282,31 +998,25 @@ export default function DataGridCore(props) {
|
||||
)}
|
||||
</TableHead>
|
||||
<TableBody ref={tableBodyRef}>
|
||||
{loadedAndInsertedRows
|
||||
.slice(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound)
|
||||
.map((row, index) => (
|
||||
{_.range(firstVisibleRowScrollIndex, firstVisibleRowScrollIndex + visibleRowCountUpperBound).map(
|
||||
(rowIndex) => (
|
||||
<DataGridRow
|
||||
key={firstVisibleRowScrollIndex + index}
|
||||
rowIndex={firstVisibleRowScrollIndex + index}
|
||||
key={rowIndex}
|
||||
grider={grider}
|
||||
rowIndex={rowIndex}
|
||||
rowHeight={rowHeight}
|
||||
visibleRealColumns={visibleRealColumns}
|
||||
inplaceEditorState={inplaceEditorState}
|
||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||
autofillSelectedCells={autofillSelectedCells}
|
||||
selectedCells={filterCellsForRow(selectedCells, firstVisibleRowScrollIndex + index)}
|
||||
insertedRowIndex={
|
||||
firstVisibleRowScrollIndex + index >= loadedRows.length
|
||||
? firstVisibleRowScrollIndex + index - loadedRows.length
|
||||
: null
|
||||
}
|
||||
autofillMarkerCell={filterCellForRow(autofillMarkerCell, firstVisibleRowScrollIndex + index)}
|
||||
changeSet={changeSet}
|
||||
setChangeSet={setChangeSet}
|
||||
selectedCells={filterCellsForRow(selectedCells, rowIndex)}
|
||||
autofillMarkerCell={filterCellForRow(autofillMarkerCell, rowIndex)}
|
||||
display={display}
|
||||
row={row}
|
||||
focusedColumn={display.focusedColumn}
|
||||
frameSelection={frameSelection}
|
||||
/>
|
||||
))}
|
||||
)
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
<HorizontalScrollBar
|
||||
@@ -1321,36 +1031,19 @@ export default function DataGridCore(props) {
|
||||
valueToSet={vScrollValueToSet}
|
||||
valueToSetDate={vScrollValueToSetDate}
|
||||
minimum={0}
|
||||
maximum={rowCountNewIncluded - visibleRowCountUpperBound + 2}
|
||||
maximum={grider.rowCount - visibleRowCountUpperBound + 2}
|
||||
onScroll={handleRowScroll}
|
||||
viewportRatio={visibleRowCountUpperBound / rowCountNewIncluded}
|
||||
/>
|
||||
<ConfirmSqlModal
|
||||
modalState={confirmSqlModalState}
|
||||
sql={confirmSql}
|
||||
engine={display.engine}
|
||||
onConfirm={handleConfirmSql}
|
||||
viewportRatio={visibleRowCountUpperBound / grider.rowCount}
|
||||
/>
|
||||
{allRowCount && <RowCountLabel>{rowCountInfo}</RowCountLabel>}
|
||||
{props.toolbarPortalRef &&
|
||||
props.toolbarPortalRef.current &&
|
||||
tabVisible &&
|
||||
ReactDOM.createPortal(
|
||||
<DataGridToolbar
|
||||
reload={() => display.reload()}
|
||||
save={handleSave}
|
||||
changeSetState={changeSetState}
|
||||
dispatchChangeSet={dispatchChangeSet}
|
||||
revert={revertAllChanges}
|
||||
/>,
|
||||
<DataGridToolbar reload={() => display.reload()} save={handleSave} grider={grider} />,
|
||||
props.toolbarPortalRef.current
|
||||
)}
|
||||
{isLoading && (
|
||||
<LoadingInfoWrapper>
|
||||
<LoadingInfoBox>
|
||||
<LoadingInfo message="Loading data" />
|
||||
</LoadingInfoBox>
|
||||
</LoadingInfoWrapper>
|
||||
)}
|
||||
{isLoading && <LoadingInfo wrapper message="Loading data" />}
|
||||
</GridContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,6 +25,12 @@ const TableBodyCell = styled.td`
|
||||
background-color: deepskyblue;
|
||||
color: white;`}
|
||||
|
||||
${(props) =>
|
||||
props.isFrameSelected &&
|
||||
`
|
||||
outline: 3px solid cyan;
|
||||
outline-offset: -3px;`}
|
||||
|
||||
${(props) =>
|
||||
props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
@@ -34,13 +40,13 @@ const TableBodyCell = styled.td`
|
||||
color: white;`}
|
||||
|
||||
${(props) =>
|
||||
props.isModifiedRow &&
|
||||
!props.isInsertedRow &&
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isModifiedCell &&
|
||||
!props.isFocusedColumn &&
|
||||
`
|
||||
props.isModifiedRow &&
|
||||
!props.isInsertedRow &&
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isModifiedCell &&
|
||||
!props.isFocusedColumn &&
|
||||
`
|
||||
background-color: #FFFFDB;`}
|
||||
${(props) =>
|
||||
!props.isSelected &&
|
||||
@@ -59,12 +65,12 @@ const TableBodyCell = styled.td`
|
||||
`
|
||||
background-color: #DBFFDB;`}
|
||||
|
||||
${(props) =>
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
${(props) =>
|
||||
!props.isSelected &&
|
||||
!props.isAutofillSelected &&
|
||||
!props.isFocusedColumn &&
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
background-color: #FFDBFF;
|
||||
`}
|
||||
|
||||
@@ -75,14 +81,13 @@ const TableBodyCell = styled.td`
|
||||
`}
|
||||
|
||||
${(props) =>
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
props.isDeletedRow &&
|
||||
`
|
||||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
|
||||
// from http://www.patternify.com/
|
||||
background-repeat: repeat-x;
|
||||
background-position: 50% 50%;`}
|
||||
|
||||
`;
|
||||
`;
|
||||
|
||||
const HintSpan = styled.span`
|
||||
color: gray;
|
||||
@@ -163,22 +168,21 @@ function CellFormattedValue({ value, dataType }) {
|
||||
return value.toString();
|
||||
}
|
||||
|
||||
function DataGridRow({
|
||||
rowHeight,
|
||||
rowIndex,
|
||||
visibleRealColumns,
|
||||
inplaceEditorState,
|
||||
dispatchInsplaceEditor,
|
||||
row,
|
||||
display,
|
||||
changeSet,
|
||||
setChangeSet,
|
||||
insertedRowIndex,
|
||||
autofillMarkerCell,
|
||||
selectedCells,
|
||||
autofillSelectedCells,
|
||||
focusedColumn,
|
||||
}) {
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
function DataGridRow(props) {
|
||||
const {
|
||||
rowHeight,
|
||||
rowIndex,
|
||||
visibleRealColumns,
|
||||
inplaceEditorState,
|
||||
dispatchInsplaceEditor,
|
||||
autofillMarkerCell,
|
||||
selectedCells,
|
||||
autofillSelectedCells,
|
||||
focusedColumn,
|
||||
grider,
|
||||
frameSelection,
|
||||
} = props;
|
||||
// usePropsCompare({
|
||||
// rowHeight,
|
||||
// rowIndex,
|
||||
@@ -197,18 +201,19 @@ function DataGridRow({
|
||||
|
||||
// console.log('RENDER ROW', rowIndex);
|
||||
|
||||
const rowDefinition = display.getChangeSetRow(row, insertedRowIndex);
|
||||
const [matchedField, matchedChangeSetItem] = findExistingChangeSetItem(changeSet, rowDefinition);
|
||||
const rowUpdated = matchedChangeSetItem ? { ...row, ...matchedChangeSetItem.fields } : row;
|
||||
const rowData = grider.getRowData(rowIndex);
|
||||
const rowStatus = grider.getRowStatus(rowIndex);
|
||||
|
||||
const hintFieldsAllowed = visibleRealColumns
|
||||
.filter((col) => {
|
||||
if (!col.hintColumnName) return false;
|
||||
if (matchedChangeSetItem && matchedField == 'updates' && col.uniqueName in matchedChangeSetItem.fields)
|
||||
return false;
|
||||
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
|
||||
return true;
|
||||
})
|
||||
.map((col) => col.uniqueName);
|
||||
|
||||
if (!rowData) return null;
|
||||
|
||||
return (
|
||||
<TableBodyRow style={{ height: `${rowHeight}px` }}>
|
||||
<TableHeaderCell data-row={rowIndex} data-col="header">
|
||||
@@ -224,15 +229,18 @@ function DataGridRow({
|
||||
}}
|
||||
data-row={rowIndex}
|
||||
data-col={col.colIndex}
|
||||
isSelected={cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
||||
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
|
||||
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
|
||||
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
|
||||
isModifiedRow={!!matchedChangeSetItem}
|
||||
isModifiedRow={rowStatus.status == 'updated'}
|
||||
isFocusedColumn={col.uniqueName == focusedColumn}
|
||||
isModifiedCell={
|
||||
matchedChangeSetItem && matchedField == 'updates' && col.uniqueName in matchedChangeSetItem.fields
|
||||
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
|
||||
isInsertedRow={
|
||||
rowStatus.status == 'inserted' || (rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))
|
||||
}
|
||||
isDeletedRow={
|
||||
rowStatus.status == 'deleted' || (rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))
|
||||
}
|
||||
isInsertedRow={insertedRowIndex != null}
|
||||
isDeletedRow={matchedField == 'deletes'}
|
||||
>
|
||||
{inplaceEditorState.cell &&
|
||||
rowIndex == inplaceEditorState.cell[0] &&
|
||||
@@ -241,16 +249,15 @@ function DataGridRow({
|
||||
widthPx={col.widthPx}
|
||||
inplaceEditorState={inplaceEditorState}
|
||||
dispatchInsplaceEditor={dispatchInsplaceEditor}
|
||||
cellValue={rowUpdated[col.uniqueName]}
|
||||
changeSet={changeSet}
|
||||
setChangeSet={setChangeSet}
|
||||
insertedRowIndex={insertedRowIndex}
|
||||
definition={display.getChangeSetField(row, col.uniqueName, insertedRowIndex)}
|
||||
cellValue={rowData[col.uniqueName]}
|
||||
grider={grider}
|
||||
rowIndex={rowIndex}
|
||||
uniqueName={col.uniqueName}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<CellFormattedValue value={rowUpdated[col.uniqueName]} dataType={col.dataType} />
|
||||
{hintFieldsAllowed.includes(col.uniqueName) && <HintSpan>{row[col.hintColumnName]}</HintSpan>}
|
||||
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} />
|
||||
{hintFieldsAllowed.includes(col.uniqueName) && <HintSpan>{rowData[col.hintColumnName]}</HintSpan>}
|
||||
</>
|
||||
)}
|
||||
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import React from 'react';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
import { changeSetContainsChanges } from '@dbgate/datalib';
|
||||
|
||||
export default function DataGridToolbar({ reload, changeSetState, dispatchChangeSet, save, revert }) {
|
||||
export default function DataGridToolbar({ reload, grider, save }) {
|
||||
return (
|
||||
<>
|
||||
<ToolbarButton onClick={reload} icon="fas fa-sync">
|
||||
Refresh
|
||||
</ToolbarButton>
|
||||
<ToolbarButton disabled={!changeSetState.canUndo} onClick={() => dispatchChangeSet({ type: 'undo' })} icon="fas fa-undo">
|
||||
<ToolbarButton disabled={!grider.canUndo} onClick={() => grider.undo()} icon="fas fa-undo">
|
||||
Undo
|
||||
</ToolbarButton>
|
||||
<ToolbarButton disabled={!changeSetState.canRedo} onClick={() => dispatchChangeSet({ type: 'redo' })} icon="fas fa-redo">
|
||||
<ToolbarButton disabled={!grider.canRedo} onClick={() => grider.redo()} icon="fas fa-redo">
|
||||
Redo
|
||||
</ToolbarButton>
|
||||
<ToolbarButton disabled={!changeSetContainsChanges(changeSetState.value)} onClick={save} icon="fas fa-save">
|
||||
<ToolbarButton disabled={!grider.allowSave} onClick={save} icon="fas fa-save">
|
||||
Save
|
||||
</ToolbarButton>
|
||||
<ToolbarButton disabled={!changeSetContainsChanges(changeSetState.value)} onClick={revert} icon="fas fa-times">
|
||||
<ToolbarButton disabled={!grider.containsChanges} onClick={() => grider.revertAllChanges()} icon="fas fa-times">
|
||||
Revert
|
||||
</ToolbarButton>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
export interface GriderRowStatus {
|
||||
status: 'regular' | 'updated' | 'deleted' | 'inserted';
|
||||
modifiedFields?: Set<string>;
|
||||
insertedFields?: Set<string>;
|
||||
deletedFields?: Set<string>;
|
||||
}
|
||||
|
||||
export default abstract class Grider {
|
||||
abstract getRowData(index): any;
|
||||
abstract get rowCount(): number;
|
||||
|
||||
getRowStatus(index): GriderRowStatus {
|
||||
const res: GriderRowStatus = {
|
||||
status: 'regular',
|
||||
};
|
||||
return res;
|
||||
}
|
||||
beginUpdate() {}
|
||||
endUpdate() {}
|
||||
setCellValue(index: number, uniqueName: string, value: any) {}
|
||||
deleteRow(index: number) {}
|
||||
insertRow(): number {
|
||||
return null;
|
||||
}
|
||||
revertRowChanges(index: number) {}
|
||||
revertAllChanges() {}
|
||||
undo() {}
|
||||
redo() {}
|
||||
get editable() {
|
||||
return false;
|
||||
}
|
||||
get canInsert() {
|
||||
return false;
|
||||
}
|
||||
get allowSave() {
|
||||
return this.containsChanges;
|
||||
}
|
||||
get rowCountInUpdate() {
|
||||
return this.rowCount;
|
||||
}
|
||||
get canUndo() {
|
||||
return false;
|
||||
}
|
||||
get canRedo() {
|
||||
return false;
|
||||
}
|
||||
get containsChanges() {
|
||||
return false;
|
||||
}
|
||||
get disableLoadNextPage() {
|
||||
return false;
|
||||
}
|
||||
get errors() {
|
||||
return null;
|
||||
}
|
||||
updateRow(index, changeObject) {
|
||||
for (const key of Object.keys(changeObject)) {
|
||||
this.setCellValue(index, key, changeObject[key]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import theme from '../theme';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import { setChangeSetValue } from '@dbgate/datalib';
|
||||
|
||||
const StyledInput = styled.input`
|
||||
border: 0px solid;
|
||||
@@ -16,13 +15,12 @@ const StyledInput = styled.input`
|
||||
|
||||
export default function InplaceEditor({
|
||||
widthPx,
|
||||
definition,
|
||||
changeSet,
|
||||
setChangeSet,
|
||||
rowIndex,
|
||||
uniqueName,
|
||||
grider,
|
||||
cellValue,
|
||||
inplaceEditorState,
|
||||
dispatchInsplaceEditor,
|
||||
isInsertedRow,
|
||||
}) {
|
||||
const editorRef = React.useRef();
|
||||
const isChangedRef = React.useRef(!!inplaceEditorState.text);
|
||||
@@ -37,7 +35,7 @@ export default function InplaceEditor({
|
||||
function handleBlur() {
|
||||
if (isChangedRef.current) {
|
||||
const editor = editorRef.current;
|
||||
setChangeSet(setChangeSetValue(changeSet, definition, editor.value));
|
||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
dispatchInsplaceEditor({ type: 'close' });
|
||||
@@ -45,7 +43,7 @@ export default function InplaceEditor({
|
||||
if (inplaceEditorState.shouldSave) {
|
||||
const editor = editorRef.current;
|
||||
if (isChangedRef.current) {
|
||||
setChangeSet(setChangeSetValue(changeSet, definition, editor.value));
|
||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
editor.blur();
|
||||
@@ -60,7 +58,7 @@ export default function InplaceEditor({
|
||||
break;
|
||||
case keycodes.enter:
|
||||
if (isChangedRef.current) {
|
||||
setChangeSet(setChangeSetValue(changeSet, definition, editor.value));
|
||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
editor.blur();
|
||||
@@ -69,7 +67,7 @@ export default function InplaceEditor({
|
||||
case keycodes.s:
|
||||
if (event.ctrlKey) {
|
||||
if (isChangedRef.current) {
|
||||
setChangeSet(setChangeSetValue(changeSet, definition, editor.value));
|
||||
grider.setCellValue(rowIndex, uniqueName, editor.value);
|
||||
isChangedRef.current = false;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import useSocket from '../utility/SocketProvider';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import LoadingDataGridCore from './LoadingDataGridCore';
|
||||
import RowsArrayGrider from './RowsArrayGrider';
|
||||
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { jslid } = props;
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'jsldata/get-rows',
|
||||
method: 'get',
|
||||
params: {
|
||||
jslid,
|
||||
offset,
|
||||
limit,
|
||||
},
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
return true;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { jslid } = props;
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'jsldata/get-stats',
|
||||
method: 'get',
|
||||
params: {
|
||||
jslid,
|
||||
},
|
||||
});
|
||||
return response.data.rowCount;
|
||||
}
|
||||
|
||||
export default function JslDataGridCore(props) {
|
||||
const { jslid } = props;
|
||||
const [changeIndex, setChangeIndex] = React.useState(0);
|
||||
|
||||
const showModal = useShowModal();
|
||||
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const socket = useSocket();
|
||||
|
||||
function exportGrid() {
|
||||
const initialValues = {};
|
||||
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
|
||||
if (archiveMatch) {
|
||||
initialValues.sourceStorageType = 'archive';
|
||||
initialValues.sourceArchiveFolder = archiveMatch[1];
|
||||
initialValues.sourceList = [archiveMatch[2]];
|
||||
} else {
|
||||
initialValues.sourceStorageType = 'jsldata';
|
||||
initialValues.sourceJslId = jslid;
|
||||
initialValues.sourceList = ['query-data'];
|
||||
}
|
||||
showModal((modalState) => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
||||
}
|
||||
|
||||
const handleJslDataStats = React.useCallback((stats) => {
|
||||
if (stats.changeIndex < changeIndex) return;
|
||||
setChangeIndex(stats.changeIndex);
|
||||
}, [changeIndex]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (jslid && socket) {
|
||||
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
return () => {
|
||||
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
|
||||
};
|
||||
}
|
||||
}, [jslid]);
|
||||
|
||||
return (
|
||||
<LoadingDataGridCore
|
||||
{...props}
|
||||
exportGrid={exportGrid}
|
||||
loadDataPage={loadDataPage}
|
||||
dataPageAvailable={dataPageAvailable}
|
||||
loadRowCount={loadRowCount}
|
||||
loadNextDataToken={changeIndex}
|
||||
onReload={() => setChangeIndex(0)}
|
||||
griderFactory={RowsArrayGrider.factory}
|
||||
griderFactoryDeps={RowsArrayGrider.factoryDeps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import React from 'react';
|
||||
import DataGridCore from './DataGridCore';
|
||||
|
||||
export default function LoadingDataGridCore(props) {
|
||||
const {
|
||||
display,
|
||||
loadDataPage,
|
||||
dataPageAvailable,
|
||||
loadRowCount,
|
||||
loadNextDataToken,
|
||||
onReload,
|
||||
exportGrid,
|
||||
openQuery,
|
||||
griderFactory,
|
||||
griderFactoryDeps,
|
||||
onChangeGrider,
|
||||
} = props;
|
||||
|
||||
const [loadProps, setLoadProps] = React.useState({
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
allRowCount: null,
|
||||
errorMessage: null,
|
||||
loadNextDataToken: 0,
|
||||
});
|
||||
const { isLoading, loadedRows, isLoadedAll, loadedTime, allRowCount, errorMessage } = loadProps;
|
||||
|
||||
const loadedTimeRef = React.useRef(0);
|
||||
|
||||
const handleLoadRowCount = async () => {
|
||||
const rowCount = await loadRowCount(props);
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
allRowCount: rowCount,
|
||||
}));
|
||||
};
|
||||
|
||||
const reload = () => {
|
||||
setLoadProps({
|
||||
allRowCount: null,
|
||||
isLoading: false,
|
||||
loadedRows: [],
|
||||
isLoadedAll: false,
|
||||
loadedTime: new Date().getTime(),
|
||||
errorMessage: null,
|
||||
loadNextDataToken: 0,
|
||||
});
|
||||
if (onReload) onReload();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (props.masterLoadedTime && props.masterLoadedTime > loadedTime) {
|
||||
display.reload();
|
||||
}
|
||||
if (display.cache.refreshTime > loadedTime) {
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
const loadNextData = async () => {
|
||||
if (isLoading) return;
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: true,
|
||||
}));
|
||||
const loadStart = new Date().getTime();
|
||||
loadedTimeRef.current = loadStart;
|
||||
|
||||
const nextRows = await loadDataPage(props, loadedRows.length, 100);
|
||||
if (loadedTimeRef.current !== loadStart) {
|
||||
// new load was dispatched
|
||||
return;
|
||||
}
|
||||
// if (!_.isArray(nextRows)) {
|
||||
// console.log('Error loading data from server', nextRows);
|
||||
// nextRows = [];
|
||||
// }
|
||||
// console.log('nextRows', nextRows);
|
||||
if (nextRows.errorMessage) {
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
errorMessage: nextRows.errorMessage,
|
||||
}));
|
||||
} else {
|
||||
if (allRowCount == null) handleLoadRowCount();
|
||||
const loadedInfo = {
|
||||
loadedRows: [...loadedRows, ...nextRows],
|
||||
loadedTime,
|
||||
};
|
||||
setLoadProps((oldLoadProps) => ({
|
||||
...oldLoadProps,
|
||||
isLoading: false,
|
||||
isLoadedAll: oldLoadProps.loadNextDataToken == loadNextDataToken && nextRows.length === 0,
|
||||
loadNextDataToken,
|
||||
...loadedInfo,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
setLoadProps((oldProps) => ({
|
||||
...oldProps,
|
||||
isLoadedAll: false,
|
||||
}));
|
||||
}, [loadNextDataToken]);
|
||||
|
||||
const griderProps = { ...props, sourceRows: loadedRows };
|
||||
const grider = React.useMemo(() => griderFactory(griderProps), griderFactoryDeps(griderProps));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (onChangeGrider) onChangeGrider(grider);
|
||||
}, [grider]);
|
||||
|
||||
const handleLoadNextData = () => {
|
||||
if (!isLoadedAll && !errorMessage && !grider.disableLoadNextPage) {
|
||||
if (dataPageAvailable(props)) {
|
||||
// If not, callbacks to load missing metadata are dispatched
|
||||
loadNextData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<DataGridCore
|
||||
{...props}
|
||||
loadNextData={handleLoadNextData}
|
||||
errorMessage={errorMessage}
|
||||
isLoadedAll={isLoadedAll}
|
||||
loadedTime={loadedTime}
|
||||
exportGrid={exportGrid}
|
||||
allRowCount={allRowCount}
|
||||
openQuery={openQuery}
|
||||
isLoading={isLoading}
|
||||
grider={grider}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,55 +1,9 @@
|
||||
import styled from 'styled-components';
|
||||
import theme from '../theme';
|
||||
|
||||
// export const SearchBoxWrapper = styled.div`
|
||||
// display: flex;
|
||||
// margin-bottom: 5px;
|
||||
// `;
|
||||
|
||||
export const ManagerMainContainer = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const ManagerOuterContainer = styled.div`
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const ManagerOuterContainer1 = styled(ManagerOuterContainer)`
|
||||
flex: 0 0 60%;
|
||||
`;
|
||||
|
||||
export const ManagerOuterContainer2 = styled(ManagerOuterContainer)`
|
||||
flex: 0 0 40%;
|
||||
`;
|
||||
|
||||
export const ManagerOuterContainerFull = styled(ManagerOuterContainer)`
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export const ManagerInnerContainer = styled.div`
|
||||
flex: 1 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
// export const Input = styled.input`
|
||||
// flex: 1;
|
||||
// min-width: 90px;
|
||||
// `;
|
||||
|
||||
export const WidgetTitle = styled.div`
|
||||
padding: 5px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
background-color: gray;
|
||||
// background-color: #CEC;
|
||||
`;
|
||||
|
||||
@@ -4,7 +4,6 @@ import { ManagerInnerContainer } from './ManagerStyles';
|
||||
import { LinkIcon, ReferenceIcon } from '../icons';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import { filterName } from '@dbgate/datalib';
|
||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
||||
|
||||
const SearchBoxWrapper = styled.div`
|
||||
display: flex;
|
||||
@@ -50,13 +49,11 @@ export default function ReferenceManager(props) {
|
||||
const { baseTable } = display || {};
|
||||
const { foreignKeys } = baseTable || {};
|
||||
const { dependencies } = baseTable || {};
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>References</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search references" filter={filter} setFilter={setFilter} inputRef={inputRef} />
|
||||
<SearchInput placeholder="Search references" filter={filter} setFilter={setFilter} />
|
||||
</SearchBoxWrapper>
|
||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
||||
{foreignKeys && foreignKeys.length > 0 && (
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import Grider, { GriderRowStatus } from './Grider';
|
||||
|
||||
export default class RowsArrayGrider extends Grider {
|
||||
constructor(private rows: any[]) {
|
||||
super();
|
||||
}
|
||||
getRowData(index: any) {
|
||||
return this.rows[index];
|
||||
}
|
||||
get rowCount() {
|
||||
return this.rows.length;
|
||||
}
|
||||
|
||||
static factory({ sourceRows }): RowsArrayGrider {
|
||||
return new RowsArrayGrider(sourceRows);
|
||||
}
|
||||
static factoryDeps({ sourceRows }) {
|
||||
return [sourceRows];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import DataGridCore from './DataGridCore';
|
||||
import useSocket from '../utility/SocketProvider';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import { changeSetToSql, createChangeSet, getChangeSetInsertedRows } from '@dbgate/datalib';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import LoadingDataGridCore from './LoadingDataGridCore';
|
||||
import ChangeSetGrider from './ChangeSetGrider';
|
||||
import { scriptToSql } from '@dbgate/sqltree';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import ConfirmSqlModal from '../modals/ConfirmSqlModal';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal';
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
async function loadDataPage(props, offset, limit) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const sql = display.getPageQuery(offset, limit);
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql },
|
||||
});
|
||||
|
||||
if (response.data.errorMessage) return response.data;
|
||||
return response.data.rows;
|
||||
}
|
||||
|
||||
function dataPageAvailable(props) {
|
||||
const { display } = props;
|
||||
const sql = display.getPageQuery(0, 1);
|
||||
return !!sql;
|
||||
}
|
||||
|
||||
async function loadRowCount(props) {
|
||||
const { display, conid, database } = props;
|
||||
|
||||
const sql = display.getCountQuery();
|
||||
|
||||
const response = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql },
|
||||
});
|
||||
|
||||
return parseInt(response.data.rows[0].count);
|
||||
}
|
||||
|
||||
/** @param props {import('./types').DataGridProps} */
|
||||
export default function SqlDataGridCore(props) {
|
||||
const { conid, database, display, changeSetState, dispatchChangeSet } = props;
|
||||
const showModal = useShowModal();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
const confirmSqlModalState = useModalState();
|
||||
const [confirmSql, setConfirmSql] = React.useState('');
|
||||
|
||||
const changeSet = changeSetState && changeSetState.value;
|
||||
const changeSetRef = React.useRef(changeSet);
|
||||
changeSetRef.current = changeSet;
|
||||
|
||||
function exportGrid() {
|
||||
const initialValues = {};
|
||||
initialValues.sourceStorageType = 'query';
|
||||
initialValues.sourceConnectionId = conid;
|
||||
initialValues.sourceDatabaseName = database;
|
||||
initialValues.sourceSql = display.getExportQuery();
|
||||
initialValues.sourceList = display.baseTable ? [display.baseTable.pureName] : [];
|
||||
showModal((modalState) => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
|
||||
}
|
||||
function openQuery() {
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: 'Query',
|
||||
icon: 'sql.svg',
|
||||
tabComponent: 'QueryTab',
|
||||
props: {
|
||||
initialScript: display.getExportQuery(),
|
||||
schemaName: display.baseTable.schemaName,
|
||||
pureName: display.baseTable.pureName,
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
const script = changeSetToSql(changeSetRef.current, display.dbinfo);
|
||||
const sql = scriptToSql(display.driver, script);
|
||||
setConfirmSql(sql);
|
||||
confirmSqlModalState.open();
|
||||
}
|
||||
|
||||
async function handleConfirmSql() {
|
||||
const resp = await axios.request({
|
||||
url: 'database-connections/query-data',
|
||||
method: 'post',
|
||||
params: {
|
||||
conid,
|
||||
database,
|
||||
},
|
||||
data: { sql: confirmSql },
|
||||
});
|
||||
const { errorMessage } = resp.data || {};
|
||||
if (errorMessage) {
|
||||
showModal((modalState) => (
|
||||
<ErrorMessageModal modalState={modalState} message={errorMessage} title="Error when saving" />
|
||||
));
|
||||
} else {
|
||||
dispatchChangeSet({ type: 'reset', value: createChangeSet() });
|
||||
setConfirmSql(null);
|
||||
display.reload();
|
||||
}
|
||||
}
|
||||
|
||||
// const grider = React.useMemo(()=>new ChangeSetGrider())
|
||||
|
||||
return (
|
||||
<>
|
||||
<LoadingDataGridCore
|
||||
{...props}
|
||||
exportGrid={exportGrid}
|
||||
openQuery={openQuery}
|
||||
loadDataPage={loadDataPage}
|
||||
dataPageAvailable={dataPageAvailable}
|
||||
loadRowCount={loadRowCount}
|
||||
griderFactory={ChangeSetGrider.factory}
|
||||
griderFactoryDeps={ChangeSetGrider.factoryDeps}
|
||||
// changeSet={changeSetState && changeSetState.value}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
<ConfirmSqlModal
|
||||
modalState={confirmSqlModalState}
|
||||
sql={confirmSql}
|
||||
engine={display.engine}
|
||||
onConfirm={handleConfirmSql}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import useSocket from '../utility/SocketProvider';
|
||||
import { VerticalSplitter } from '../widgets/Splitter';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import ReferenceHeader from './ReferenceHeader';
|
||||
import SqlDataGridCore from './SqlDataGridCore';
|
||||
|
||||
const ReferenceContainer = styled.div`
|
||||
position: absolute;
|
||||
@@ -41,6 +42,7 @@ export default function TableDataGrid({
|
||||
cache = undefined,
|
||||
setCache = undefined,
|
||||
masterLoadedTime = undefined,
|
||||
isDetailView = false,
|
||||
}) {
|
||||
// const [childConfig, setChildConfig] = React.useState(createGridConfig());
|
||||
const [myCache, setMyCache] = React.useState(createGridCache());
|
||||
@@ -162,6 +164,8 @@ export default function TableDataGrid({
|
||||
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
|
||||
refReloadToken={refReloadToken.toString()}
|
||||
masterLoadedTime={masterLoadedTime}
|
||||
GridCore={SqlDataGridCore}
|
||||
isDetailView={isDetailView}
|
||||
/>
|
||||
{reference && (
|
||||
<ReferenceContainer>
|
||||
@@ -182,6 +186,7 @@ export default function TableDataGrid({
|
||||
cache={childCache}
|
||||
setCache={setChildCache}
|
||||
masterLoadedTime={myLoadedTime}
|
||||
isDetailView
|
||||
/>
|
||||
</ReferenceGridWrapper>
|
||||
</ReferenceContainer>
|
||||
|
||||
@@ -2,10 +2,11 @@ import _ from 'lodash';
|
||||
import { SeriesSizes } from './SeriesSizes';
|
||||
import { CellAddress } from './selection';
|
||||
import { GridDisplay } from '@dbgate/datalib';
|
||||
import Grider from './Grider';
|
||||
|
||||
export function countColumnSizes(loadedRows, columns, containerWidth, display: GridDisplay) {
|
||||
export function countColumnSizes(grider: Grider, columns, containerWidth, display: GridDisplay) {
|
||||
const columnSizes = new SeriesSizes();
|
||||
if (!loadedRows || !columns) return columnSizes;
|
||||
if (!grider || !columns) return columnSizes;
|
||||
|
||||
let canvas = document.createElement('canvas');
|
||||
let context = canvas.getContext('2d');
|
||||
@@ -51,7 +52,8 @@ export function countColumnSizes(loadedRows, columns, containerWidth, display: G
|
||||
// if (headerWidth > this.rowHeaderWidth) this.rowHeaderWidth = headerWidth;
|
||||
|
||||
context.font = '14px Helvetica';
|
||||
for (let row of loadedRows.slice(0, 20)) {
|
||||
for (let rowIndex = 0; rowIndex < Math.min(grider.rowCount, 20); rowIndex += 1) {
|
||||
const row = grider.getRowData(rowIndex);
|
||||
for (let colIndex = 0; colIndex < columns.length; colIndex++) {
|
||||
const uqName = columns[colIndex].uniqueName;
|
||||
|
||||
|
||||
@@ -1,18 +1,46 @@
|
||||
import { GridDisplay, ChangeSet, GridReferenceDefinition } from '@dbgate/datalib';
|
||||
import Grider from './Grider';
|
||||
|
||||
export interface DataGridProps {
|
||||
conid?: string;
|
||||
database?: string;
|
||||
display: GridDisplay;
|
||||
tabVisible?: boolean;
|
||||
changeSetState?: { value: ChangeSet };
|
||||
dispatchChangeSet?: Function;
|
||||
toolbarPortalRef?: any;
|
||||
jslid?: string;
|
||||
showReferences?: boolean;
|
||||
onReferenceClick?: (def: GridReferenceDefinition) => void;
|
||||
onReferenceSourceChanged?: Function;
|
||||
refReloadToken?: string;
|
||||
masterLoadedTime?: number;
|
||||
managerSize?: number;
|
||||
grider?: Grider;
|
||||
conid?: string;
|
||||
database?: string;
|
||||
jslid?: string;
|
||||
|
||||
[field: string]: any;
|
||||
}
|
||||
|
||||
// export interface DataGridCoreProps extends DataGridProps {
|
||||
// rows: any[];
|
||||
// loadNextData?: Function;
|
||||
// exportGrid?: Function;
|
||||
// openQuery?: Function;
|
||||
// undo?: Function;
|
||||
// redo?: Function;
|
||||
|
||||
// errorMessage?: string;
|
||||
// isLoadedAll?: boolean;
|
||||
// loadedTime?: any;
|
||||
// allRowCount?: number;
|
||||
// conid?: string;
|
||||
// database?: string;
|
||||
// insertedRowCount?: number;
|
||||
// isLoading?: boolean;
|
||||
// }
|
||||
|
||||
// export interface LoadingDataGridProps extends DataGridProps {
|
||||
// conid?: string;
|
||||
// database?: string;
|
||||
// jslid?: string;
|
||||
// }
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { filterName } from '@dbgate/datalib';
|
||||
import { ExpandIcon, FontIcon } from '../icons';
|
||||
import InlineButton from '../widgets/InlineButton';
|
||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
||||
import keycodes from '../utility/keycodes';
|
||||
|
||||
const Row = styled.div`
|
||||
// margin-left: 5px;
|
||||
// margin-right: 5px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
// padding: 5px;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: lightblue;
|
||||
}
|
||||
`;
|
||||
const Name = styled.div`
|
||||
white-space: nowrap;
|
||||
margin: 5px;
|
||||
`;
|
||||
const Buttons = styled.div`
|
||||
white-space: nowrap;
|
||||
`;
|
||||
const Icon = styled(FontIcon)`
|
||||
// margin-left: 5px;
|
||||
&:hover {
|
||||
background-color: gray;
|
||||
}
|
||||
padding: 5px;
|
||||
`;
|
||||
const EditorInput = styled.input`
|
||||
width: calc(100% - 10px);
|
||||
background-color: ${(props) =>
|
||||
// @ts-ignore
|
||||
props.isError ? '#FFCCCC' : 'white'};
|
||||
`;
|
||||
|
||||
function ColumnNameEditor({
|
||||
onEnter,
|
||||
onBlur = undefined,
|
||||
focusOnCreate = false,
|
||||
blurOnEnter = false,
|
||||
existingNames,
|
||||
defaultValue = '',
|
||||
...other
|
||||
}) {
|
||||
const [value, setValue] = React.useState(defaultValue || '');
|
||||
const editorRef = React.useRef(null);
|
||||
const isError = value && existingNames && existingNames.includes(value);
|
||||
const handleKeyDown = (event) => {
|
||||
if (value && event.keyCode == keycodes.enter && !isError) {
|
||||
onEnter(value);
|
||||
setValue('');
|
||||
if (blurOnEnter) editorRef.current.blur();
|
||||
}
|
||||
if (event.keyCode == keycodes.escape) {
|
||||
setValue('');
|
||||
editorRef.current.blur();
|
||||
}
|
||||
};
|
||||
const handleBlur = () => {
|
||||
if (value && !isError) {
|
||||
onEnter(value);
|
||||
setValue('');
|
||||
}
|
||||
if (onBlur) onBlur();
|
||||
};
|
||||
React.useEffect(() => {
|
||||
if (focusOnCreate) editorRef.current.focus();
|
||||
}, [focusOnCreate]);
|
||||
return (
|
||||
<EditorInput
|
||||
ref={editorRef}
|
||||
type="text"
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
value={value}
|
||||
onChange={(ev) => setValue(ev.target.value)}
|
||||
// @ts-ignore
|
||||
isError={isError}
|
||||
{...other}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function exchange(array, i1, i2) {
|
||||
const i1r = (i1 + array.length) % array.length;
|
||||
const i2r = (i2 + array.length) % array.length;
|
||||
const res = [...array];
|
||||
[res[i1r], res[i2r]] = [res[i2r], res[i1r]];
|
||||
return res;
|
||||
}
|
||||
|
||||
function ColumnManagerRow({ column, onEdit, onRemove, onUp, onDown }) {
|
||||
const [isHover, setIsHover] = React.useState(false);
|
||||
return (
|
||||
<Row onMouseEnter={() => setIsHover(true)} onMouseLeave={() => setIsHover(false)}>
|
||||
<Name>{column.columnName}</Name>
|
||||
<Buttons>
|
||||
<Icon icon="fas fa-edit" onClick={onEdit} />
|
||||
<Icon icon="fas fa-trash" onClick={onRemove} />
|
||||
<Icon icon="fas fa-arrow-up" onClick={onUp} />
|
||||
<Icon icon="fas fa-arrow-down" onClick={onDown} />
|
||||
</Buttons>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
function dispatchChangeColumns(props, func, rowFunc = null) {
|
||||
const { modelState, dispatchModel } = props;
|
||||
const model = modelState.value;
|
||||
|
||||
dispatchModel({
|
||||
type: 'set',
|
||||
value: {
|
||||
rows: rowFunc ? model.rows.map(rowFunc) : model.rows,
|
||||
structure: {
|
||||
...model.structure,
|
||||
columns: func(model.structure.columns),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export default function FreeTableColumnEditor(props) {
|
||||
const { modelState, dispatchModel } = props;
|
||||
const [editingColumn, setEditingColumn] = React.useState(null);
|
||||
const model = modelState.value;
|
||||
return (
|
||||
<>
|
||||
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
|
||||
{model.structure.columns.map((column, index) =>
|
||||
index == editingColumn ? (
|
||||
<ColumnNameEditor
|
||||
defaultValue={column.columnName}
|
||||
onEnter={(columnName) => {
|
||||
dispatchChangeColumns(
|
||||
props,
|
||||
(cols) => cols.map((col, i) => (index == i ? { columnName } : col)),
|
||||
(row) => _.mapKeys(row, (v, k) => (k == column.columnName ? columnName : k))
|
||||
);
|
||||
}}
|
||||
onBlur={() => setEditingColumn(null)}
|
||||
focusOnCreate
|
||||
blurOnEnter
|
||||
existingNames={model.structure.columns.map((x) => x.columnName)}
|
||||
/>
|
||||
) : (
|
||||
<ColumnManagerRow
|
||||
key={column.uniqueName}
|
||||
column={column}
|
||||
onEdit={() => setEditingColumn(index)}
|
||||
onRemove={() => {
|
||||
dispatchChangeColumns(props, (cols) => cols.filter((c, i) => i != index));
|
||||
}}
|
||||
onUp={() => {
|
||||
dispatchChangeColumns(props, (cols) => exchange(cols, index, index - 1));
|
||||
}}
|
||||
onDown={() => {
|
||||
dispatchChangeColumns(props, (cols) => exchange(cols, index, index + 1));
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<ColumnNameEditor
|
||||
onEnter={(columnName) => {
|
||||
dispatchChangeColumns(props, (cols) => [...cols, { columnName }]);
|
||||
}}
|
||||
placeholder="New column"
|
||||
existingNames={model.structure.columns.map((x) => x.columnName)}
|
||||
/>
|
||||
</ManagerInnerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import { runMacro } from '@dbgate/datalib';
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { HorizontalSplitter, VerticalSplitter } from '../widgets/Splitter';
|
||||
import FreeTableColumnEditor from './FreeTableColumnEditor';
|
||||
import FreeTableGridCore from './FreeTableGridCore';
|
||||
import MacroDetail from './MacroDetail';
|
||||
import MacroManager from './MacroManager';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
||||
|
||||
const LeftContainer = styled.div`
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const DataGridContainer = styled.div`
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
`;
|
||||
|
||||
function extractMacroValuesForMacro(macroValues, macro) {
|
||||
if (!macro) return {};
|
||||
return {
|
||||
..._.fromPairs((macro.args || []).filter((x) => x.default != null).map((x) => [x.name, x.default])),
|
||||
..._.mapKeys(macroValues, (v, k) => k.replace(/^.*#/, '')),
|
||||
};
|
||||
}
|
||||
|
||||
export default function FreeTableGrid(props) {
|
||||
const { modelState, dispatchModel } = props;
|
||||
const [managerSize, setManagerSize] = React.useState(0);
|
||||
const [selectedMacro, setSelectedMacro] = React.useState(null);
|
||||
const [macroValues, setMacroValues] = React.useState({});
|
||||
const [selectedCells, setSelectedCells] = React.useState([]);
|
||||
const handleExecuteMacro = () => {
|
||||
const newModel = runMacro(
|
||||
selectedMacro,
|
||||
extractMacroValuesForMacro(macroValues, selectedMacro),
|
||||
modelState.value,
|
||||
false,
|
||||
selectedCells
|
||||
);
|
||||
dispatchModel({ type: 'set', value: newModel });
|
||||
setSelectedMacro(null);
|
||||
};
|
||||
// console.log('macroValues', macroValues);
|
||||
return (
|
||||
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
|
||||
<LeftContainer>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Columns" name="columns" height="40%">
|
||||
<FreeTableColumnEditor {...props} />
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Macros" name="macros">
|
||||
<MacroManager
|
||||
{...props}
|
||||
managerSize={managerSize}
|
||||
selectedMacro={selectedMacro}
|
||||
setSelectedMacro={setSelectedMacro}
|
||||
/>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</LeftContainer>
|
||||
|
||||
<DataGridContainer>
|
||||
<VerticalSplitter initialValue="70%">
|
||||
<FreeTableGridCore
|
||||
{...props}
|
||||
macroPreview={selectedMacro}
|
||||
macroValues={extractMacroValuesForMacro(macroValues, selectedMacro)}
|
||||
onSelectionChanged={setSelectedCells}
|
||||
setSelectedMacro={setSelectedMacro}
|
||||
/>
|
||||
{!!selectedMacro && (
|
||||
<MacroDetail
|
||||
selectedMacro={selectedMacro}
|
||||
setSelectedMacro={setSelectedMacro}
|
||||
onChangeValues={setMacroValues}
|
||||
macroValues={macroValues}
|
||||
onExecute={handleExecuteMacro}
|
||||
/>
|
||||
)}
|
||||
</VerticalSplitter>
|
||||
</DataGridContainer>
|
||||
</HorizontalSplitter>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createGridCache, FreeTableGridDisplay } from '@dbgate/datalib';
|
||||
import React from 'react';
|
||||
import DataGridCore from '../datagrid/DataGridCore';
|
||||
import keycodes from '../utility/keycodes';
|
||||
import FreeTableGrider from './FreeTableGrider';
|
||||
import MacroPreviewGrider from './MacroPreviewGrider';
|
||||
|
||||
export default function FreeTableGridCore(props) {
|
||||
const {
|
||||
modelState,
|
||||
dispatchModel,
|
||||
config,
|
||||
setConfig,
|
||||
macroPreview,
|
||||
macroValues,
|
||||
onSelectionChanged,
|
||||
setSelectedMacro,
|
||||
} = props;
|
||||
const [cache, setCache] = React.useState(createGridCache());
|
||||
const [selectedCells, setSelectedCells] = React.useState([]);
|
||||
const grider = React.useMemo(
|
||||
() =>
|
||||
macroPreview
|
||||
? new MacroPreviewGrider(modelState.value, macroPreview, macroValues, selectedCells)
|
||||
: FreeTableGrider.factory(props),
|
||||
[
|
||||
...FreeTableGrider.factoryDeps(props),
|
||||
macroPreview,
|
||||
macroPreview ? macroValues : null,
|
||||
macroPreview ? selectedCells : null,
|
||||
]
|
||||
);
|
||||
const display = React.useMemo(() => new FreeTableGridDisplay(grider.model || modelState.value, config, setConfig, cache, setCache), [
|
||||
modelState.value,
|
||||
config,
|
||||
cache,
|
||||
grider,
|
||||
]);
|
||||
|
||||
const handleSelectionChanged = React.useCallback(
|
||||
(cells) => {
|
||||
if (onSelectionChanged) onSelectionChanged(cells);
|
||||
setSelectedCells(cells);
|
||||
},
|
||||
[setSelectedCells]
|
||||
);
|
||||
|
||||
const handleKeyDown = React.useCallback((event) => {
|
||||
if (event.keyCode == keycodes.escape) {
|
||||
setSelectedMacro(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<DataGridCore
|
||||
{...props}
|
||||
grider={grider}
|
||||
display={display}
|
||||
onSelectionChanged={macroPreview ? handleSelectionChanged : null}
|
||||
frameSelection={!!macroPreview}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { FreeTableModel } from '@dbgate/datalib';
|
||||
import Grider, { GriderRowStatus } from '../datagrid/Grider';
|
||||
|
||||
export default class FreeTableGrider extends Grider {
|
||||
public model: FreeTableModel;
|
||||
private batchModel: FreeTableModel;
|
||||
|
||||
constructor(public modelState, public dispatchModel) {
|
||||
super();
|
||||
this.model = modelState && modelState.value;
|
||||
}
|
||||
getRowData(index: any) {
|
||||
return this.model.rows[index];
|
||||
}
|
||||
get rowCount() {
|
||||
return this.model.rows.length;
|
||||
}
|
||||
get currentModel(): FreeTableModel {
|
||||
return this.batchModel || this.model;
|
||||
}
|
||||
set currentModel(value) {
|
||||
if (this.batchModel) this.batchModel = value;
|
||||
else this.dispatchModel({ type: 'set', value });
|
||||
}
|
||||
setCellValue(index: number, uniqueName: string, value: any) {
|
||||
const model = this.currentModel;
|
||||
if (model.rows[index])
|
||||
this.currentModel = {
|
||||
...model,
|
||||
rows: model.rows.map((row, i) => (index == i ? { ...row, [uniqueName]: value } : row)),
|
||||
};
|
||||
}
|
||||
get editable() {
|
||||
return true;
|
||||
}
|
||||
get canInsert() {
|
||||
return true;
|
||||
}
|
||||
get allowSave() {
|
||||
return true;
|
||||
}
|
||||
insertRow(): number {
|
||||
const model = this.currentModel;
|
||||
this.currentModel = {
|
||||
...model,
|
||||
rows: [...model.rows, {}],
|
||||
};
|
||||
return this.currentModel.rows.length - 1;
|
||||
}
|
||||
deleteRow(index: number) {
|
||||
const model = this.currentModel;
|
||||
this.currentModel = {
|
||||
...model,
|
||||
rows: model.rows.filter((row, i) => index != i),
|
||||
};
|
||||
}
|
||||
beginUpdate() {
|
||||
this.batchModel = this.model;
|
||||
}
|
||||
endUpdate() {
|
||||
if (this.model != this.batchModel) {
|
||||
this.dispatchModel({ type: 'set', value: this.batchModel });
|
||||
this.batchModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
static factory({ modelState, dispatchModel }): FreeTableGrider {
|
||||
return new FreeTableGrider(modelState, dispatchModel);
|
||||
}
|
||||
static factoryDeps({ modelState, dispatchModel }) {
|
||||
return [modelState, dispatchModel];
|
||||
}
|
||||
undo() {
|
||||
this.dispatchModel({ type: 'undo' });
|
||||
}
|
||||
redo() {
|
||||
this.dispatchModel({ type: 'redo' });
|
||||
}
|
||||
get canUndo() {
|
||||
return this.modelState.canUndo;
|
||||
}
|
||||
get canRedo() {
|
||||
return this.modelState.canRedo;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import ToolbarButton from '../widgets/ToolbarButton';
|
||||
import styled from 'styled-components';
|
||||
import { MacroIcon } from '../icons';
|
||||
import { TabPage, TabControl } from '../widgets/TabControl';
|
||||
import theme from '../theme';
|
||||
import JavaScriptEditor from '../sqleditor/JavaScriptEditor';
|
||||
import MacroParameters from './MacroParameters';
|
||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
||||
import { FormButton } from '../utility/forms';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #ddeeee;
|
||||
height: ${theme.toolBar.height}px;
|
||||
min-height: ${theme.toolBar.height}px;
|
||||
overflow: hidden;
|
||||
border-top: 1px solid #ccc;
|
||||
border-bottom: 1px solid #ccc;
|
||||
`;
|
||||
|
||||
const Header = styled.div`
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const HeaderText = styled.div`
|
||||
margin-left: 10px;
|
||||
`;
|
||||
|
||||
const MacroDetailContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
const MacroDetailTabWrapper = styled.div`
|
||||
display: flex;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const MacroSection = styled.div`
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
const TextWrapper = styled.div`
|
||||
margin: 5px;
|
||||
`;
|
||||
|
||||
const Buttons = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
function MacroHeader({ selectedMacro, setSelectedMacro, onExecute }) {
|
||||
return (
|
||||
<Container>
|
||||
<Header>
|
||||
<MacroIcon />
|
||||
<HeaderText>{selectedMacro.title}</HeaderText>
|
||||
</Header>
|
||||
<Buttons>
|
||||
<ToolbarButton icon="fas fa-check" onClick={onExecute} patchY={6}>
|
||||
Execute
|
||||
</ToolbarButton>
|
||||
<ToolbarButton icon="fas fa-times" onClick={() => setSelectedMacro(null)} patchY={6}>
|
||||
Close
|
||||
</ToolbarButton>
|
||||
</Buttons>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MacroDetail({ selectedMacro, setSelectedMacro, onChangeValues, macroValues, onExecute }) {
|
||||
return (
|
||||
<MacroDetailContainer>
|
||||
<MacroHeader selectedMacro={selectedMacro} setSelectedMacro={setSelectedMacro} onExecute={onExecute} />
|
||||
<TabControl>
|
||||
<TabPage label="Macro detail" key="detail">
|
||||
<MacroDetailTabWrapper>
|
||||
<MacroSection>
|
||||
<WidgetTitle>Execute</WidgetTitle>
|
||||
<FormStyledButton value="Execute" onClick={onExecute} />
|
||||
</MacroSection>
|
||||
|
||||
<MacroSection>
|
||||
<WidgetTitle>Parameters</WidgetTitle>
|
||||
{selectedMacro.args && selectedMacro.args.length > 0 ? (
|
||||
<MacroParameters
|
||||
key={selectedMacro.name}
|
||||
args={selectedMacro.args}
|
||||
onChangeValues={onChangeValues}
|
||||
macroValues={macroValues}
|
||||
namePrefix={`${selectedMacro.name}#`}
|
||||
/>
|
||||
) : (
|
||||
<TextWrapper>This macro has no parameters</TextWrapper>
|
||||
)}
|
||||
</MacroSection>
|
||||
<MacroSection>
|
||||
<WidgetTitle>Description</WidgetTitle>
|
||||
<TextWrapper>{selectedMacro.description}</TextWrapper>
|
||||
</MacroSection>
|
||||
</MacroDetailTabWrapper>
|
||||
</TabPage>
|
||||
<TabPage label="JavaScript" key="javascript">
|
||||
<JavaScriptEditor readOnly value={selectedMacro.code} />
|
||||
</TabPage>
|
||||
</TabControl>
|
||||
</MacroDetailContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import styled from 'styled-components';
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
|
||||
import SearchInput from '../widgets/SearchInput';
|
||||
import { WidgetTitle } from '../widgets/WidgetStyles';
|
||||
import macros from './macros';
|
||||
import { AppObjectList } from '../appobj/AppObjectList';
|
||||
import macroAppObject from '../appobj/MacroAppObject';
|
||||
|
||||
const SearchBoxWrapper = styled.div`
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
`;
|
||||
|
||||
// const MacroItemStyled = styled.div`
|
||||
// white-space: nowrap;
|
||||
// padding: 5px;
|
||||
// &:hover {
|
||||
// background-color: lightblue;
|
||||
// }
|
||||
// `;
|
||||
|
||||
// function MacroListItem({ macro }) {
|
||||
// return <MacroItemStyled>{macro.title}</MacroItemStyled>;
|
||||
// }
|
||||
|
||||
export default function MacroManager({ managerSize, selectedMacro, setSelectedMacro }) {
|
||||
const [filter, setFilter] = React.useState('');
|
||||
|
||||
return (
|
||||
<>
|
||||
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search macros" filter={filter} setFilter={setFilter} />
|
||||
</SearchBoxWrapper>
|
||||
<AppObjectList
|
||||
list={_.sortBy(macros, 'title')}
|
||||
makeAppObj={macroAppObject()}
|
||||
onObjectClick={(macro) => setSelectedMacro(macro)}
|
||||
filter={filter}
|
||||
groupFunc={(appobj) => appobj.groupTitle}
|
||||
/>
|
||||
{/* {macros.map((macro) => (
|
||||
<MacroListItem key={`${macro.group}/${macro.name}`} macro={macro} />
|
||||
))} */}
|
||||
</ManagerInnerContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import {
|
||||
FormTextField,
|
||||
FormSubmit,
|
||||
FormArchiveFolderSelect,
|
||||
FormRow,
|
||||
FormLabel,
|
||||
FormSelectField,
|
||||
FormCheckboxField,
|
||||
} from '../utility/forms';
|
||||
import { Formik, Form, useFormikContext } from 'formik';
|
||||
|
||||
const MacroArgumentsWrapper = styled.div`
|
||||
`;
|
||||
|
||||
function MacroArgument({ arg, namePrefix }) {
|
||||
const name = `${namePrefix}${arg.name}`;
|
||||
if (arg.type == 'text') {
|
||||
return <FormTextField label={arg.label} name={name} />;
|
||||
}
|
||||
if (arg.type == 'checkbox') {
|
||||
return <FormCheckboxField label={arg.label} name={name} />;
|
||||
}
|
||||
if (arg.type == 'select') {
|
||||
return (
|
||||
<FormSelectField label={arg.label} name={name}>
|
||||
{arg.options.map((opt) =>
|
||||
_.isString(opt) ? <option value={opt}>{opt}</option> : <option value={opt.value}>{opt.name}</option>
|
||||
)}
|
||||
</FormSelectField>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function MacroArgumentList({ args, onChangeValues, namePrefix }) {
|
||||
const { values } = useFormikContext();
|
||||
React.useEffect(() => {
|
||||
if (onChangeValues) onChangeValues(values);
|
||||
}, [values]);
|
||||
return (
|
||||
<MacroArgumentsWrapper>
|
||||
{' '}
|
||||
{args.map((arg) => (
|
||||
<MacroArgument arg={arg} key={arg.name} namePrefix={namePrefix} />
|
||||
))}
|
||||
</MacroArgumentsWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default function MacroParameters({ args, onChangeValues, macroValues, namePrefix }) {
|
||||
if (!args || args.length == 0) return null;
|
||||
const initialValues = {
|
||||
..._.fromPairs(args.filter((x) => x.default != null).map((x) => [`${namePrefix}${x.name}`, x.default])),
|
||||
...macroValues,
|
||||
};
|
||||
return (
|
||||
<Formik initialValues={initialValues} onSubmit={() => {}}>
|
||||
<Form>
|
||||
<MacroArgumentList args={args} onChangeValues={onChangeValues} namePrefix={namePrefix} />
|
||||
</Form>
|
||||
</Formik>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { FreeTableModel, MacroDefinition, MacroSelectedCell, runMacro } from '@dbgate/datalib';
|
||||
import Grider, { GriderRowStatus } from '../datagrid/Grider';
|
||||
import _ from 'lodash';
|
||||
|
||||
function convertToSet(row, field) {
|
||||
if (!row) return null;
|
||||
if (!row[field]) return null;
|
||||
if (_.isSet(row[field])) return row[field];
|
||||
return new Set(row[field]);
|
||||
}
|
||||
|
||||
export default class MacroPreviewGrider extends Grider {
|
||||
model: FreeTableModel;
|
||||
_errors: string[] = [];
|
||||
constructor(model: FreeTableModel, macro: MacroDefinition, macroArgs: {}, selectedCells: MacroSelectedCell[]) {
|
||||
super();
|
||||
this.model = runMacro(macro, macroArgs, model, true, selectedCells, this._errors);
|
||||
}
|
||||
|
||||
get errors() {
|
||||
return this._errors;
|
||||
}
|
||||
|
||||
getRowStatus(index): GriderRowStatus {
|
||||
const row = this.model.rows[index];
|
||||
return {
|
||||
status: (row && row.__rowStatus) || 'regular',
|
||||
modifiedFields: convertToSet(row, '__modifiedFields'),
|
||||
insertedFields: convertToSet(row, '__insertedFields'),
|
||||
deletedFields: convertToSet(row, '__deletedFields'),
|
||||
};
|
||||
}
|
||||
|
||||
getRowData(index: any) {
|
||||
return this.model.rows[index];
|
||||
}
|
||||
get rowCount() {
|
||||
return this.model.rows.length;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
const macros = [
|
||||
{
|
||||
title: 'Remove diacritics',
|
||||
name: 'removeDiacritics',
|
||||
group: 'Text',
|
||||
description: 'Removes diacritics from selected cells',
|
||||
type: 'transformValue',
|
||||
code: `return modules.lodash.deburr(value)`,
|
||||
},
|
||||
{
|
||||
title: 'Search & replace text',
|
||||
name: 'stringReplace',
|
||||
group: 'Text',
|
||||
description: 'Search & replace text or regular expression',
|
||||
type: 'transformValue',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Find',
|
||||
name: 'find',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Replace with',
|
||||
name: 'replace',
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: 'Case sensitive',
|
||||
name: 'caseSensitive',
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: 'Regular expression',
|
||||
name: 'isRegex',
|
||||
},
|
||||
],
|
||||
code: `
|
||||
const rtext = args.isRegex ? args.find : modules.lodash.escapeRegExp(args.find);
|
||||
const rflags = args.caseSensitive ? 'g' : 'ig';
|
||||
return value ? value.toString().replace(new RegExp(rtext, rflags), args.replace || '') : value
|
||||
`,
|
||||
},
|
||||
{
|
||||
title: 'Change text case',
|
||||
name: 'changeTextCase',
|
||||
group: 'Text',
|
||||
description: 'Uppercase, lowercase and other case functions',
|
||||
type: 'transformValue',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
options: ['toUpper', 'toLower', 'lowerCase', 'upperCase', 'kebabCase', 'snakeCase', 'camelCase', 'startCase'],
|
||||
label: 'Type',
|
||||
name: 'type',
|
||||
default: 'toUpper',
|
||||
},
|
||||
],
|
||||
code: `return modules.lodash[args.type](value)`,
|
||||
},
|
||||
{
|
||||
title: 'Row index',
|
||||
name: 'rowIndex',
|
||||
group: 'Tools',
|
||||
description: 'Index of row from 1 (autoincrement)',
|
||||
type: 'transformValue',
|
||||
code: `return rowIndex + 1`,
|
||||
},
|
||||
{
|
||||
title: 'Generate UUID',
|
||||
name: 'uuidv1',
|
||||
group: 'Tools',
|
||||
description: 'Generate unique identifier',
|
||||
type: 'transformValue',
|
||||
args: [
|
||||
{
|
||||
type: 'select',
|
||||
options: [
|
||||
{ value: 'uuidv1', name: 'V1 - from timestamp' },
|
||||
{ value: 'uuidv4', name: 'V4 - random generated' },
|
||||
],
|
||||
label: 'Version',
|
||||
name: 'version',
|
||||
default: 'uuidv1',
|
||||
},
|
||||
],
|
||||
code: `return modules[args.version]()`,
|
||||
},
|
||||
{
|
||||
title: 'Current date',
|
||||
name: 'currentDate',
|
||||
group: 'Tools',
|
||||
description: 'Gets current date',
|
||||
type: 'transformValue',
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Format',
|
||||
name: 'format',
|
||||
default: 'YYYY-MM-DD HH:mm:ss',
|
||||
},
|
||||
],
|
||||
code: `return modules.moment().format(args.format)`,
|
||||
},
|
||||
{
|
||||
title: 'Duplicate rows',
|
||||
name: 'duplicateRows',
|
||||
group: 'Tools',
|
||||
description: 'Duplicate selected rows',
|
||||
type: 'transformRows',
|
||||
code: `
|
||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
||||
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
|
||||
const maxIndex = modules.lodash.max(selectedRowIndexes);
|
||||
return [
|
||||
...rows.slice(0, maxIndex + 1),
|
||||
...selectedRowIndexes.map(index => ({
|
||||
...modules.lodash.pick(rows[index], selectedRows[index].map(x => x.column)),
|
||||
__rowStatus: 'inserted',
|
||||
})),
|
||||
...rows.slice(maxIndex + 1),
|
||||
]
|
||||
`,
|
||||
},
|
||||
{
|
||||
title: 'Delete empty rows',
|
||||
name: 'deleteEmptyRows',
|
||||
group: 'Tools',
|
||||
description: 'Delete empty rows - rows with all values null or empty string',
|
||||
type: 'transformRows',
|
||||
code: `
|
||||
return rows.map(row => {
|
||||
if (cols.find(col => row[col])) return row;
|
||||
return {
|
||||
...row,
|
||||
__rowStatus: 'deleted',
|
||||
};
|
||||
})
|
||||
`,
|
||||
},
|
||||
{
|
||||
title: 'Duplicate columns',
|
||||
name: 'duplicateColumns',
|
||||
group: 'Tools',
|
||||
description: 'Duplicate selected columns',
|
||||
type: 'transformData',
|
||||
code: `
|
||||
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
|
||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
||||
const addedColumnNames = selectedColumnNames.map(col => (args.prefix || '') + col + (args.postfix || ''));
|
||||
const resultRows = rows.map((row, rowIndex) => ({
|
||||
...row,
|
||||
...(selectedRowIndexes.includes(rowIndex) ? modules.lodash.fromPairs(selectedColumnNames.map(col => [(args.prefix || '') + col + (args.postfix || ''), row[col]])) : {}),
|
||||
__insertedFields: addedColumnNames,
|
||||
}));
|
||||
const resultCols = [
|
||||
...cols,
|
||||
...addedColumnNames,
|
||||
];
|
||||
return {
|
||||
rows: resultRows,
|
||||
cols: resultCols,
|
||||
}
|
||||
`,
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Prefix',
|
||||
name: 'prefix',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Postfix',
|
||||
name: 'postfix',
|
||||
default: '_copy',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: 'Extract date fields',
|
||||
name: 'extractDateFields',
|
||||
group: 'Tools',
|
||||
description: 'Extract yaear, month, day and other date/time fields from selection and adds it as new columns',
|
||||
type: 'transformData',
|
||||
code: `
|
||||
const selectedColumnNames = modules.lodash.uniq(selectedCells.map(x => x.column));
|
||||
const selectedRowIndexes = modules.lodash.uniq(selectedCells.map(x => x.row));
|
||||
const addedColumnNames = modules.lodash.compact([args.year, args.month, args.day, args.hour, args.minute, args.second]);
|
||||
const selectedRows = modules.lodash.groupBy(selectedCells, 'row');
|
||||
const resultRows = rows.map((row, rowIndex) => {
|
||||
if (!selectedRowIndexes.includes(rowIndex)) return {
|
||||
...row,
|
||||
__insertedFields: addedColumnNames,
|
||||
};
|
||||
let mom = null;
|
||||
for(const cell of selectedRows[rowIndex]) {
|
||||
const m = modules.moment(row[cell.column]);
|
||||
if (m.isValid()) {
|
||||
mom = m;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!mom) return {
|
||||
...row,
|
||||
__insertedFields: addedColumnNames,
|
||||
};
|
||||
|
||||
const fields = {
|
||||
[args.year]: mom.year(),
|
||||
[args.month]: mom.month() + 1,
|
||||
[args.day]: mom.day(),
|
||||
[args.hour]: mom.hour(),
|
||||
[args.minute]: mom.minute(),
|
||||
[args.second]: mom.second(),
|
||||
};
|
||||
|
||||
return {
|
||||
...row,
|
||||
...modules.lodash.pick(fields, addedColumnNames),
|
||||
__insertedFields: addedColumnNames,
|
||||
}
|
||||
});
|
||||
const resultCols = [
|
||||
...cols,
|
||||
...addedColumnNames,
|
||||
];
|
||||
return {
|
||||
rows: resultRows,
|
||||
cols: resultCols,
|
||||
}
|
||||
`,
|
||||
args: [
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Year name',
|
||||
name: 'year',
|
||||
default: 'year',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Month name',
|
||||
name: 'month',
|
||||
default: 'month',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Day name',
|
||||
name: 'day',
|
||||
default: 'day',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Hour name',
|
||||
name: 'hour',
|
||||
default: 'hour',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Minute name',
|
||||
name: 'minute',
|
||||
default: 'minute',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
label: 'Second name',
|
||||
name: 'second',
|
||||
default: 'second',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default macros;
|
||||
@@ -0,0 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { openNewTab } from '../utility/common';
|
||||
|
||||
export default function useNewFreeTable() {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
return ({ title = undefined, ...props } = {}) =>
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: title || 'Table',
|
||||
icon: 'freetable.svg',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props,
|
||||
});
|
||||
}
|
||||
@@ -57,10 +57,12 @@ export function ExpandIcon({
|
||||
}
|
||||
|
||||
export const TableIcon = (props) => getIconImage('table2.svg', props);
|
||||
export const FreeTableIcon = (props) => getIconImage('freetable.svg', props);
|
||||
export const ViewIcon = (props) => getIconImage('view2.svg', props);
|
||||
export const ArchiveTableIcon = (props) => getIconImage('archtable.svg', props);
|
||||
export const DatabaseIcon = (props) => getIconImage('database.svg', props);
|
||||
export const ServerIcon = (props) => getIconImage('server.svg', props);
|
||||
export const MacroIcon = (props) => getIconImage('double-wrench.svg', props);
|
||||
|
||||
export const MicrosoftIcon = (props) => getIconImage('microsoft.svg', props);
|
||||
export const MySqlIcon = (props) => getIconImage('mysql.svg', props);
|
||||
|
||||
@@ -22,10 +22,12 @@ import getAsArray from '../utility/getAsArray';
|
||||
import axios from '../utility/axios';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import SqlEditor from '../sqleditor/SqlEditor';
|
||||
import { useUploadsProvider } from '../utility/UploadsProvider';
|
||||
|
||||
const Container = styled.div`
|
||||
max-height: 50vh;
|
||||
overflow-y: scroll;
|
||||
// max-height: 50vh;
|
||||
// overflow-y: scroll;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
@@ -62,6 +64,17 @@ const SqlWrapper = styled.div`
|
||||
width: 20vw;
|
||||
`;
|
||||
|
||||
const DragWrapper = styled.div`
|
||||
padding: 10px;
|
||||
background: #ddd;
|
||||
`;
|
||||
|
||||
const ArrowWrapper = styled.div`
|
||||
font-size: 30px;
|
||||
color: blue;
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
function getFileFilters(storageType) {
|
||||
const res = [];
|
||||
if (storageType == 'csv') res.push({ name: 'CSV files', extensions: ['csv'] });
|
||||
@@ -141,7 +154,7 @@ function FilesInput() {
|
||||
if (electron) {
|
||||
return <ElectronFilesInput />;
|
||||
}
|
||||
return <ErrorInfo message="Import files is currently implemented only for electron client" />;
|
||||
return <DragWrapper>Drag & drop imported files here</DragWrapper>;
|
||||
}
|
||||
|
||||
function SourceTargetConfig({
|
||||
@@ -287,12 +300,43 @@ function SourceName({ name }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ImportExportConfigurator() {
|
||||
export default function ImportExportConfigurator({ uploadedFile = undefined }) {
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const targetDbinfo = useDatabaseInfo({ conid: values.targetConnectionId, database: values.targetDatabaseName });
|
||||
const sourceConnectionInfo = useConnectionInfo({ conid: values.sourceConnectionId });
|
||||
const { engine: sourceEngine } = sourceConnectionInfo || {};
|
||||
const { sourceList } = values;
|
||||
const { uploadListener, setUploadListener } = useUploadsProvider();
|
||||
|
||||
const handleUpload = React.useCallback(
|
||||
(file) => {
|
||||
addFilesToSourceList(
|
||||
[
|
||||
{
|
||||
full: file.filePath,
|
||||
name: file.shortName,
|
||||
},
|
||||
],
|
||||
values,
|
||||
setFieldValue
|
||||
);
|
||||
// setFieldValue('sourceList', [...(sourceList || []), file.originalName]);
|
||||
},
|
||||
[setFieldValue, sourceList]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setUploadListener(() => handleUpload);
|
||||
return () => {
|
||||
setUploadListener(null);
|
||||
};
|
||||
}, [handleUpload]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (uploadedFile) {
|
||||
handleUpload(uploadedFile);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
@@ -307,6 +351,9 @@ export default function ImportExportConfigurator() {
|
||||
tablesField="sourceList"
|
||||
engine={sourceEngine}
|
||||
/>
|
||||
<ArrowWrapper>
|
||||
<i className="fas fa-arrow-right" />
|
||||
</ArrowWrapper>
|
||||
<SourceTargetConfig
|
||||
direction="target"
|
||||
storageTypeField="targetStorageType"
|
||||
|
||||
@@ -12,12 +12,76 @@ import { openNewTab } from '../utility/common';
|
||||
import { useCurrentArchive, useSetOpenedTabs } from '../utility/globalState';
|
||||
import RunnerOutputPane from '../query/RunnerOutputPane';
|
||||
import axios from '../utility/axios';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
|
||||
import SocketMessagesView from '../query/SocketMessagesView';
|
||||
import RunnerOutputFiles from '../query/RunnerOuputFiles';
|
||||
|
||||
const headerHeight = '60px';
|
||||
const footerHeight = '60px';
|
||||
|
||||
const OutputContainer = styled.div`
|
||||
position: relative;
|
||||
height: 150px;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
display: flex;
|
||||
// flex: 1;
|
||||
|
||||
position: fixed;
|
||||
top: ${headerHeight};
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: ${footerHeight};
|
||||
`;
|
||||
|
||||
const WidgetColumnWrapper = styled.div`
|
||||
max-width: 40%;
|
||||
// flex-basis: 50%;
|
||||
// flow-grow: 0;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
border-left: 1px solid #ccc;
|
||||
`;
|
||||
|
||||
const StyledForm = styled(Form)`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const ContentWrapper = styled.div`
|
||||
// border-bottom: 1px solid #ccc;
|
||||
border-top: 1px solid #ccc;
|
||||
// padding: 15px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const Footer = styled.div`
|
||||
position: fixed;
|
||||
height: ${footerHeight};
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0px;
|
||||
background-color: #eeffff;
|
||||
|
||||
border-top: 1px solid #ccc;
|
||||
// padding: 15px;
|
||||
`;
|
||||
|
||||
const FooterButtons = styled.div`
|
||||
margin: 15px;
|
||||
`;
|
||||
|
||||
function GenerateSctriptButton({ modalState }) {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const { values } = useFormikContext();
|
||||
@@ -38,7 +102,7 @@ function GenerateSctriptButton({ modalState }) {
|
||||
return <FormStyledButton type="button" value="Generate script" onClick={handleGenerateScript} />;
|
||||
}
|
||||
|
||||
export default function ImportExportModal({ modalState, initialValues }) {
|
||||
export default function ImportExportModal({ modalState, initialValues, uploadedFile = undefined }) {
|
||||
const [executeNumber, setExecuteNumber] = React.useState(0);
|
||||
const [runnerId, setRunnerId] = React.useState(null);
|
||||
const archive = useCurrentArchive();
|
||||
@@ -55,7 +119,7 @@ export default function ImportExportModal({ modalState, initialValues }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalBase modalState={modalState} fullScreen isFlex>
|
||||
<Formik
|
||||
onSubmit={handleExecute}
|
||||
initialValues={{
|
||||
@@ -66,22 +130,37 @@ export default function ImportExportModal({ modalState, initialValues }) {
|
||||
...initialValues,
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<StyledForm>
|
||||
<ModalHeader modalState={modalState}>Import/Export</ModalHeader>
|
||||
<ModalContent>
|
||||
<ImportExportConfigurator />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<FormStyledButton type="submit" value="Run" />
|
||||
<GenerateSctriptButton modalState={modalState} />
|
||||
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||
</ModalFooter>
|
||||
{runnerId && (
|
||||
<OutputContainer>
|
||||
<RunnerOutputPane runnerId={runnerId} executeNumber={executeNumber} />
|
||||
</OutputContainer>
|
||||
)}
|
||||
</Form>
|
||||
<Wrapper>
|
||||
<ContentWrapper>
|
||||
<ImportExportConfigurator uploadedFile={uploadedFile} />
|
||||
</ContentWrapper>
|
||||
<WidgetColumnWrapper>
|
||||
<WidgetColumnBar>
|
||||
{/* <WidgetColumnBarItem title="Preview" name="preview">
|
||||
Preview
|
||||
</WidgetColumnBarItem> */}
|
||||
<WidgetColumnBarItem title="Output files" name="output" height="20%">
|
||||
<RunnerOutputFiles runnerId={runnerId} executeNumber={executeNumber} />
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Messages" name="messages">
|
||||
<SocketMessagesView
|
||||
eventName={runnerId ? `runner-info-${runnerId}` : null}
|
||||
executeNumber={executeNumber}
|
||||
/>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
</WidgetColumnWrapper>
|
||||
</Wrapper>
|
||||
<Footer>
|
||||
<FooterButtons>
|
||||
<FormStyledButton type="submit" value="Run" />
|
||||
<GenerateSctriptButton modalState={modalState} />
|
||||
<FormStyledButton type="button" value="Close" onClick={modalState.close} />
|
||||
</FooterButtons>
|
||||
</Footer>
|
||||
</StyledForm>
|
||||
</Formik>
|
||||
</ModalBase>
|
||||
);
|
||||
|
||||
@@ -22,27 +22,51 @@ const StyledModal = styled(Modal)`
|
||||
background: #fff;
|
||||
overflow: auto;
|
||||
webkitoverflowscrolling: touch;
|
||||
borderradius: 4px;
|
||||
outline: none;
|
||||
|
||||
width: 50%;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
margin-top: 15vh;
|
||||
${(props) =>
|
||||
props.fullScreen &&
|
||||
`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
// width: 100%;
|
||||
// height: 100%;
|
||||
`}
|
||||
|
||||
// z-index:1200;
|
||||
${(props) =>
|
||||
!props.fullScreen &&
|
||||
`
|
||||
border-radius: 10px;
|
||||
width: 50%;
|
||||
max-width: 900px;
|
||||
margin: auto;
|
||||
margin-top: 15vh;
|
||||
`}
|
||||
|
||||
// z-index:1200;
|
||||
|
||||
${(props) =>
|
||||
props.isFlex &&
|
||||
`
|
||||
display: flex;
|
||||
`}
|
||||
`;
|
||||
|
||||
const ModalContent = styled.div`
|
||||
padding: 20px;
|
||||
`;
|
||||
|
||||
export default function ModalBase({ modalState, children }) {
|
||||
export default function ModalBase({ modalState, children, isFlex = false, fullScreen = false }) {
|
||||
return (
|
||||
<StyledModal
|
||||
isOpen={modalState.isOpen}
|
||||
onRequestClose={modalState.close}
|
||||
overlayClassName="RactModalOverlay"
|
||||
fullScreen={fullScreen}
|
||||
isFlex={isFlex}
|
||||
// style={{
|
||||
// overlay: {
|
||||
// backgroundColor: '#000',
|
||||
|
||||
@@ -4,6 +4,7 @@ import styled from 'styled-components';
|
||||
const Wrapper = styled.div`
|
||||
border-bottom: 1px solid #ccc;
|
||||
padding: 15px;
|
||||
background-color: #eeffff;
|
||||
`;
|
||||
|
||||
export default function ModalFooter({ children }) {
|
||||
|
||||
@@ -6,13 +6,16 @@ const Wrapper = styled.div`
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background-color: #eeffff;
|
||||
`;
|
||||
|
||||
const CloseWrapper = styled.div`
|
||||
font-size: 12pt;
|
||||
&:hover {
|
||||
background-color: #blue;
|
||||
background-color: #ccccff;
|
||||
}
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px;
|
||||
`;
|
||||
|
||||
export default function ModalHeader({ children, modalState }) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import ModalBase from './ModalBase';
|
||||
import { FormTextField, FormSubmit, FormArchiveFolderSelect, FormRow, FormLabel } from '../utility/forms';
|
||||
import { Formik, Form } from 'formik';
|
||||
import styled from 'styled-components';
|
||||
import ModalHeader from './ModalHeader';
|
||||
import ModalContent from './ModalContent';
|
||||
import ModalFooter from './ModalFooter';
|
||||
|
||||
const SelectWrapper = styled.div`
|
||||
width: 150px;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
export default function SaveArchiveModal({ file = 'new-table', folder = 'default', modalState, onSave }) {
|
||||
const handleSubmit = async (values) => {
|
||||
const { file, folder } = values;
|
||||
modalState.close();
|
||||
if (onSave) onSave(folder, file);
|
||||
};
|
||||
return (
|
||||
<ModalBase modalState={modalState}>
|
||||
<ModalHeader modalState={modalState}>Save to archive</ModalHeader>
|
||||
<Formik onSubmit={handleSubmit} initialValues={{ file, folder }}>
|
||||
<Form>
|
||||
<ModalContent>
|
||||
{/* <Label>Archive folder</Label> */}
|
||||
<FormRow>
|
||||
<FormLabel>Folder</FormLabel>
|
||||
<SelectWrapper>
|
||||
<FormArchiveFolderSelect name="folder" />
|
||||
</SelectWrapper>
|
||||
</FormRow>
|
||||
<FormTextField label="File name" name="file" />
|
||||
</ModalContent>
|
||||
<ModalFooter>
|
||||
<FormSubmit text="Save" />
|
||||
</ModalFooter>
|
||||
</Form>
|
||||
</Formik>
|
||||
</ModalBase>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import DataGrid from '../datagrid/DataGrid';
|
||||
import { JslGridDisplay, createGridConfig, createGridCache } from '@dbgate/datalib';
|
||||
import useFetch from '../utility/useFetch';
|
||||
import JslDataGridCore from '../datagrid/JslDataGridCore';
|
||||
|
||||
export default function JslDataGrid({ jslid }) {
|
||||
const info = useFetch({
|
||||
@@ -19,5 +20,5 @@ export default function JslDataGrid({ jslid }) {
|
||||
cache,
|
||||
]);
|
||||
|
||||
return <DataGrid display={display} jslid={jslid} />;
|
||||
return <DataGrid display={display} jslid={jslid} GridCore={JslDataGridCore} />;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { createGridCache, createChangeSet, createGridConfig, createFreeTableModel } from '@dbgate/datalib';
|
||||
import useUndoReducer from '../utility/useUndoReducer';
|
||||
import usePropsCompare from '../utility/usePropsCompare';
|
||||
import { useSetOpenedTabs, useUpdateDatabaseForTab } from '../utility/globalState';
|
||||
import TableDataGrid from '../datagrid/TableDataGrid';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import FreeTableGrid from '../freetable/FreeTableGrid';
|
||||
import SaveArchiveModal from '../modals/SaveArchiveModal';
|
||||
import useModalState from '../modals/useModalState';
|
||||
import axios from '../utility/axios';
|
||||
import LoadingInfo from '../widgets/LoadingInfo';
|
||||
import { changeTab } from '../utility/common';
|
||||
import ErrorInfo from '../widgets/ErrorInfo';
|
||||
|
||||
export default function FreeDataTab({ archiveFolder, archiveFile, tabVisible, toolbarPortalRef, tabid, initialData }) {
|
||||
const [config, setConfig] = useGridConfig(tabid);
|
||||
const [modelState, dispatchModel] = useUndoReducer(createFreeTableModel());
|
||||
const storageKey = `tabdata_freetable_${tabid}`;
|
||||
const saveSqlFileModalState = useModalState();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
const [errorMessage, setErrorMessage] = React.useState(null);
|
||||
|
||||
const handleLoadInitialData = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const resp = await axios.post('runners/load-reader', initialData);
|
||||
// @ts-ignore
|
||||
dispatchModel({ type: 'reset', value: resp.data });
|
||||
setIsLoading(false);
|
||||
} catch (err) {
|
||||
setIsLoading(false);
|
||||
const errorMessage = (err && err.response && err.response.data && err.response.data.error) || 'Loading failed';
|
||||
setErrorMessage(errorMessage);
|
||||
console.error(err.response);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const existingData = localStorage.getItem(storageKey);
|
||||
if (existingData) {
|
||||
const value = JSON.parse(existingData);
|
||||
// @ts-ignore
|
||||
dispatchModel({ type: 'reset', value });
|
||||
} else if (initialData) {
|
||||
if (initialData.functionName) handleLoadInitialData();
|
||||
// @ts-ignore
|
||||
else dispatchModel({ type: 'reset', value: initialData });
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
localStorage.setItem(storageKey, JSON.stringify(modelState.value));
|
||||
}, [modelState]);
|
||||
|
||||
const handleSave = async (folder, file) => {
|
||||
await axios.post('archive/save-free-table', { folder, file, data: modelState.value });
|
||||
changeTab(tabid, setOpenedTabs, (tab) => ({
|
||||
...tab,
|
||||
title: file,
|
||||
props: { archiveFIle: file, archiveFolder: folder },
|
||||
}));
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingInfo wrapper message="Loading data" />;
|
||||
}
|
||||
if (errorMessage) {
|
||||
return <ErrorInfo message={errorMessage} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<FreeTableGrid
|
||||
config={config}
|
||||
setConfig={setConfig}
|
||||
modelState={modelState}
|
||||
dispatchModel={dispatchModel}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
onSave={() => saveSqlFileModalState.open()}
|
||||
/>
|
||||
<SaveArchiveModal
|
||||
modalState={saveSqlFileModalState}
|
||||
folder={archiveFolder}
|
||||
file={archiveFile}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import useUndoReducer from '../utility/useUndoReducer';
|
||||
import usePropsCompare from '../utility/usePropsCompare';
|
||||
import { useUpdateDatabaseForTab } from '../utility/globalState';
|
||||
import useGridConfig from '../utility/useGridConfig';
|
||||
import SqlDataGridCore from '../datagrid/SqlDataGridCore';
|
||||
|
||||
export default function ViewDataTab({ conid, database, schemaName, pureName, tabVisible, toolbarPortalRef, tabid }) {
|
||||
const viewInfo = useViewInfo({ conid, database, schemaName, pureName });
|
||||
@@ -50,6 +51,7 @@ export default function ViewDataTab({ conid, database, schemaName, pureName, tab
|
||||
changeSetState={changeSetState}
|
||||
dispatchChangeSet={dispatchChangeSet}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
/>
|
||||
GridCore={SqlDataGridCore}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import QueryTab from './QueryTab';
|
||||
import ShellTab from './ShellTab';
|
||||
import InfoPageTab from './InfoPageTab';
|
||||
import ArchiveFileTab from './ArchiveFileTab';
|
||||
import FreeTableTab from './FreeTableTab';
|
||||
|
||||
export default {
|
||||
TableDataTab,
|
||||
@@ -14,4 +15,5 @@ export default {
|
||||
InfoPageTab,
|
||||
ShellTab,
|
||||
ArchiveFileTab,
|
||||
FreeTableTab,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import resolveApi from './resolveApi';
|
||||
|
||||
const UploadsContext = React.createContext(null);
|
||||
|
||||
export default function UploadsProvider({ children }) {
|
||||
const [uploadListener, setUploadListener] = React.useState(null);
|
||||
return <UploadsContext.Provider value={{ uploadListener, setUploadListener }}>{children}</UploadsContext.Provider>;
|
||||
}
|
||||
|
||||
export function useUploadsProvider() {
|
||||
return React.useContext(UploadsContext);
|
||||
}
|
||||
|
||||
export function useUploadsZone() {
|
||||
const { uploadListener } = useUploadsProvider();
|
||||
const showModal = useShowModal();
|
||||
|
||||
const onDrop = React.useCallback(
|
||||
(files) => {
|
||||
files.forEach(async (file) => {
|
||||
if (parseInt(file.size, 10) >= 4 * 1024 * 1024) {
|
||||
// to big file
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('data', file);
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
};
|
||||
|
||||
const apiBase = resolveApi();
|
||||
const resp = await fetch(`${apiBase}/uploads/upload`, fetchOptions);
|
||||
const fileData = await resp.json();
|
||||
|
||||
if (uploadListener) {
|
||||
uploadListener(fileData);
|
||||
} else {
|
||||
if (['csv', 'excel', 'jsonl'].includes(fileData.storageType)) {
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
uploadedFile={fileData}
|
||||
modalState={modalState}
|
||||
initialValues={{
|
||||
sourceStorageType: fileData.storageType,
|
||||
// sourceConnectionId: data.conid,
|
||||
// sourceDatabaseName: data.database,
|
||||
// sourceSchemaName: data.schemaName,
|
||||
// sourceList: [data.pureName],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// const reader = new FileReader();
|
||||
|
||||
// reader.onabort = () => console.log('file reading was aborted');
|
||||
// reader.onerror = () => console.log('file reading has failed');
|
||||
// reader.onload = () => {
|
||||
// // Do whatever you want with the file contents
|
||||
// const binaryStr = reader.result;
|
||||
// console.log(binaryStr);
|
||||
// };
|
||||
// reader.readAsArrayBuffer(file);
|
||||
});
|
||||
},
|
||||
[uploadListener]
|
||||
);
|
||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||
|
||||
return { getRootProps, getInputProps, isDragActive };
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import Select from 'react-select';
|
||||
import Creatable from 'react-select/creatable';
|
||||
import { TextField, SelectField } from './inputs';
|
||||
import { TextField, SelectField, CheckboxField } from './inputs';
|
||||
import { Field, useFormikContext } from 'formik';
|
||||
import FormStyledButton from '../widgets/FormStyledButton';
|
||||
import {
|
||||
@@ -49,6 +49,26 @@ export function FormTextField({ label, ...other }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function FormCheckboxFieldRaw({ name = undefined, ...other }) {
|
||||
const { values, setFieldValue } = useFormikContext();
|
||||
const handleChange = (event) => {
|
||||
setFieldValue(name, event.target.checked);
|
||||
};
|
||||
return <CheckboxField name={name} checked={!!values[name]} onChange={handleChange} {...other} />;
|
||||
// return <Field {...other} as={CheckboxField} />;
|
||||
}
|
||||
|
||||
export function FormCheckboxField({ label, ...other }) {
|
||||
return (
|
||||
<FormRow>
|
||||
<FormLabel>{label}</FormLabel>
|
||||
<FormValue>
|
||||
<FormCheckboxFieldRaw {...other} />
|
||||
</FormValue>
|
||||
</FormRow>
|
||||
);
|
||||
}
|
||||
|
||||
export function FormSelectFieldRaw({ children, ...other }) {
|
||||
return (
|
||||
<Field {...other} as={SelectField}>
|
||||
@@ -196,7 +216,7 @@ export function FormArchiveFilesSelect({ folderName, name }) {
|
||||
return <FormReactSelect options={filesOptions} name={name} isMulti />;
|
||||
}
|
||||
|
||||
export function FormArchiveFolderSelect({ name }) {
|
||||
export function FormArchiveFolderSelect({ name, ...other }) {
|
||||
const { setFieldValue } = useFormikContext();
|
||||
const folders = useArchiveFolders();
|
||||
const folderOptions = React.useMemo(
|
||||
@@ -214,6 +234,12 @@ export function FormArchiveFolderSelect({ name }) {
|
||||
};
|
||||
|
||||
return (
|
||||
<FormReactSelect options={folderOptions} name={name} Component={Creatable} onCreateOption={handleCreateOption} />
|
||||
<FormReactSelect
|
||||
{...other}
|
||||
options={folderOptions}
|
||||
name={name}
|
||||
Component={Creatable}
|
||||
onCreateOption={handleCreateOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -16,3 +16,7 @@ export function SelectField({ children = null, options = [], ...other }) {
|
||||
</select>
|
||||
);
|
||||
}
|
||||
|
||||
export function CheckboxField({ editorRef = undefined, ...other }) {
|
||||
return <input type="checkbox" {...other} ref={editorRef}></input>;
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
WidgetsOuterContainer,
|
||||
WidgetTitle,
|
||||
} from './WidgetStyles';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
||||
import { useArchiveFiles, useArchiveFolders } from '../utility/metadataLoaders';
|
||||
import archiveFolderAppObject from '../appobj/archiveFolderAppObject';
|
||||
@@ -22,7 +23,6 @@ import axios from '../utility/axios';
|
||||
|
||||
function ArchiveFolderList() {
|
||||
const folders = useArchiveFolders();
|
||||
const inputRef = React.useRef(null);
|
||||
const [filter, setFilter] = React.useState('');
|
||||
|
||||
const setArchive = useSetCurrentArchive();
|
||||
@@ -33,9 +33,8 @@ function ArchiveFolderList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>Archive folder</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput inputRef={inputRef} placeholder="Search archive folders" filter={filter} setFilter={setFilter} />
|
||||
<SearchInput placeholder="Search archive folders" filter={filter} setFilter={setFilter} />
|
||||
<InlineButton onClick={handleRefreshFolders}>Refresh</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
<WidgetsInnerContainer>
|
||||
@@ -53,7 +52,6 @@ function ArchiveFolderList() {
|
||||
function ArchiveFilesList() {
|
||||
const folder = useCurrentArchive();
|
||||
const files = useArchiveFiles({ folder });
|
||||
const inputRef = React.useRef(null);
|
||||
const [filter, setFilter] = React.useState('');
|
||||
const handleRefreshFiles = () => {
|
||||
axios.post('archive/refresh-files', { folder });
|
||||
@@ -61,9 +59,8 @@ function ArchiveFilesList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>Archive files</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput inputRef={inputRef} placeholder="Search archive files" filter={filter} setFilter={setFilter} />
|
||||
<SearchInput placeholder="Search archive files" filter={filter} setFilter={setFilter} />
|
||||
<InlineButton onClick={handleRefreshFiles}>Refresh</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
<WidgetsInnerContainer>
|
||||
@@ -82,13 +79,13 @@ function ArchiveFilesList() {
|
||||
|
||||
export default function ArchiveWidget() {
|
||||
return (
|
||||
<WidgetsMainContainer>
|
||||
<WidgetsOuterContainer>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Archive folders" name="folders" height="50%">
|
||||
<ArchiveFolderList />
|
||||
</WidgetsOuterContainer>
|
||||
<WidgetsOuterContainer>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Archive files" name="files">
|
||||
<ArchiveFilesList />
|
||||
</WidgetsOuterContainer>
|
||||
</WidgetsMainContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import axios from '../utility/axios';
|
||||
import LoadingInfo from './LoadingInfo';
|
||||
import SearchInput from './SearchInput';
|
||||
import ErrorInfo from './ErrorInfo';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||
|
||||
function SubDatabaseList({ data }) {
|
||||
const setDb = useSetCurrentDatabase();
|
||||
@@ -60,14 +61,12 @@ function ConnectionList() {
|
||||
axios.post('server-connections/refresh', { conid });
|
||||
}
|
||||
};
|
||||
const inputRef = React.useRef(null);
|
||||
|
||||
const [filter, setFilter] = React.useState('');
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>Connections</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} inputRef={inputRef} />
|
||||
<SearchInput placeholder="Search connection" filter={filter} setFilter={setFilter} />
|
||||
<InlineButton onClick={handleRefreshConnections}>Refresh</InlineButton>
|
||||
</SearchBoxWrapper>
|
||||
|
||||
@@ -103,7 +102,6 @@ function SqlObjectList({ conid, database }) {
|
||||
const inputRef = React.useRef(null);
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle inputRef={inputRef}>Tables, views, functions</WidgetTitle>
|
||||
<SearchBoxWrapper>
|
||||
<SearchInput inputRef={inputRef} placeholder="Search tables or objects" filter={filter} setFilter={setFilter} />
|
||||
<InlineButton onClick={handleRefreshDatabase}>Refresh</InlineButton>
|
||||
@@ -128,12 +126,7 @@ function SqlObjectListWrapper() {
|
||||
const db = useCurrentDatabase();
|
||||
|
||||
if (!db) {
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle>Tables, views, functions</WidgetTitle>
|
||||
<ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />
|
||||
</>
|
||||
);
|
||||
return <ErrorInfo message="Database not selected" icon="fas fa-exclamation-circle blue" />;
|
||||
}
|
||||
const { name, connection } = db;
|
||||
|
||||
@@ -144,13 +137,13 @@ function SqlObjectListWrapper() {
|
||||
|
||||
export default function DatabaseWidget() {
|
||||
return (
|
||||
<WidgetsMainContainer>
|
||||
<WidgetsOuterContainer>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Connections" name="connections" height="50%">
|
||||
<ConnectionList />
|
||||
</WidgetsOuterContainer>
|
||||
<WidgetsOuterContainer>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Tables, views, functions" name="dbObjects">
|
||||
<SqlObjectListWrapper />
|
||||
</WidgetsOuterContainer>
|
||||
</WidgetsMainContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,20 @@ const Icon = styled.div`
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
export default function ErrorInfo({ message, icon = 'fas fa-times-circle red' }) {
|
||||
const ContainerSmall = styled.div`
|
||||
display: flex;
|
||||
margin-right: 10px;
|
||||
`;
|
||||
|
||||
export default function ErrorInfo({ message, icon = 'fas fa-times-circle red', isSmall = false }) {
|
||||
if (isSmall) {
|
||||
return (
|
||||
<ContainerSmall>
|
||||
<FontIcon icon={icon} />
|
||||
{message}
|
||||
</ContainerSmall>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Container>
|
||||
<Icon>
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
WidgetTitle,
|
||||
} from './WidgetStyles';
|
||||
import savedSqlFileAppObject from '../appobj/savedSqlFileAppObject';
|
||||
import WidgetColumnBar, { WidgetColumnBarItem } from './WidgetColumnBar';
|
||||
|
||||
function ClosedTabsList() {
|
||||
const tabs = useOpenedTabs();
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle>Recently closed tabs</WidgetTitle>
|
||||
<WidgetsInnerContainer>
|
||||
<AppObjectList
|
||||
list={_.sortBy(
|
||||
@@ -38,7 +38,6 @@ function SavedSqlFilesList() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetTitle>Saved SQL files</WidgetTitle>
|
||||
<WidgetsInnerContainer>
|
||||
<AppObjectList list={files} makeAppObj={savedSqlFileAppObject()} />
|
||||
</WidgetsInnerContainer>
|
||||
@@ -48,13 +47,13 @@ function SavedSqlFilesList() {
|
||||
|
||||
export default function FilesWidget() {
|
||||
return (
|
||||
<WidgetsMainContainer>
|
||||
<WidgetsOuterContainer>
|
||||
<WidgetColumnBar>
|
||||
<WidgetColumnBarItem title="Recently closed tabs" name="closedTabs" height="50%">
|
||||
<ClosedTabsList />
|
||||
</WidgetsOuterContainer>
|
||||
<WidgetsOuterContainer>
|
||||
</WidgetColumnBarItem>
|
||||
<WidgetColumnBarItem title="Saved SQL files" name="sqlFiles">
|
||||
<SavedSqlFilesList />
|
||||
</WidgetsOuterContainer>
|
||||
</WidgetsMainContainer>
|
||||
</WidgetColumnBarItem>
|
||||
</WidgetColumnBar>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,24 @@ const Spinner = styled.div`
|
||||
margin: 10px;
|
||||
`;
|
||||
|
||||
export default function LoadingInfo({ message }) {
|
||||
return (
|
||||
const LoadingInfoWrapper = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
`;
|
||||
const LoadingInfoBox = styled.div`
|
||||
background-color: #ccc;
|
||||
padding: 10px;
|
||||
border: 1px solid gray;
|
||||
`;
|
||||
|
||||
export default function LoadingInfo({ message, wrapper = false }) {
|
||||
const core = (
|
||||
<Container>
|
||||
<Spinner>
|
||||
<i className="fas fa-spinner fa-spin" />
|
||||
@@ -22,4 +38,13 @@ export default function LoadingInfo({ message }) {
|
||||
{message}
|
||||
</Container>
|
||||
);
|
||||
if (wrapper) {
|
||||
return (
|
||||
<LoadingInfoWrapper>
|
||||
<LoadingInfoBox>{core}</LoadingInfoBox>
|
||||
</LoadingInfoWrapper>
|
||||
);
|
||||
} else {
|
||||
return core;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ const SplitterMainBase = styled.div`
|
||||
bottom: 0;
|
||||
`;
|
||||
|
||||
// @ts-ignore
|
||||
const VerticalMainContainer = styled(SplitterMainBase)`
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -7,6 +7,9 @@ import useNewQuery from '../query/useNewQuery';
|
||||
import { useConfig } from '../utility/metadataLoaders';
|
||||
import { useSetOpenedTabs, useOpenedTabs } from '../utility/globalState';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import useNewFreeTable from '../freetable/useNewFreeTable';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
|
||||
const ToolbarContainer = styled.div`
|
||||
display: flex;
|
||||
@@ -16,10 +19,12 @@ const ToolbarContainer = styled.div`
|
||||
export default function ToolBar({ toolbarPortalRef }) {
|
||||
const modalState = useModalState();
|
||||
const newQuery = useNewQuery();
|
||||
const newFreeTable = useNewFreeTable();
|
||||
const config = useConfig();
|
||||
const toolbar = config.toolbar || [];
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const openedTabs = useOpenedTabs();
|
||||
const showModal = useShowModal();
|
||||
|
||||
React.useEffect(() => {
|
||||
window['dbgate_createNewConnection'] = modalState.open;
|
||||
@@ -27,6 +32,21 @@ export default function ToolBar({ toolbarPortalRef }) {
|
||||
window['dbgate_closeAll'] = () => setOpenedTabs([]);
|
||||
});
|
||||
|
||||
const showImport = () => {
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
modalState={modalState}
|
||||
initialValues={{
|
||||
sourceStorageType: 'csv',
|
||||
// sourceConnectionId: data.conid,
|
||||
// sourceDatabaseName: data.database,
|
||||
// sourceSchemaName: data.schemaName,
|
||||
// sourceList: [data.pureName],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
|
||||
function openTabFromButton(button) {
|
||||
if (openedTabs.find((x) => x.tabComponent == 'InfoPageTab' && x.props && x.props.page == button.page)) {
|
||||
setOpenedTabs((tabs) =>
|
||||
@@ -74,6 +94,12 @@ export default function ToolBar({ toolbarPortalRef }) {
|
||||
<ToolbarButton onClick={newQuery} icon="fas fa-file-alt">
|
||||
New Query
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={newFreeTable} icon="fas fa-table">
|
||||
Free table editor
|
||||
</ToolbarButton>
|
||||
<ToolbarButton onClick={showImport} icon="fas fa-file-upload">
|
||||
Import data
|
||||
</ToolbarButton>
|
||||
<ToolbarContainer ref={toolbarPortalRef}></ToolbarContainer>
|
||||
</ToolbarContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
SearchBoxWrapper,
|
||||
WidgetsInnerContainer,
|
||||
WidgetsMainContainer,
|
||||
WidgetsOuterContainer,
|
||||
WidgetTitle,
|
||||
} from './WidgetStyles';
|
||||
import { VerticalSplitHandle, useSplitterDrag } from './Splitter';
|
||||
import useDimensions from '../utility/useDimensions';
|
||||
|
||||
export function WidgetColumnBarItem({ title, children, name, height = undefined, collapsed = false }) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
function WidgetContainer({ widget, visible, splitterVisible, parentHeight, initialSize = undefined }) {
|
||||
const [size, setSize] = React.useState(null);
|
||||
|
||||
const handleResizeDown = useSplitterDrag('clientY', (diff) => setSize((v) => v + diff));
|
||||
|
||||
React.useEffect(() => {
|
||||
if (_.isString(initialSize) && initialSize.endsWith('px')) setSize(parseInt(initialSize.slice(0, -2)));
|
||||
else if (_.isString(initialSize) && initialSize.endsWith('%'))
|
||||
setSize((parentHeight * parseFloat(initialSize.slice(0, -1))) / 100);
|
||||
else setSize(parentHeight / 3);
|
||||
}, [parentHeight]);
|
||||
|
||||
if (!visible) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<WidgetsOuterContainer style={splitterVisible ? { height: size } : null}>
|
||||
{widget.props.children}
|
||||
</WidgetsOuterContainer>
|
||||
{splitterVisible && <VerticalSplitHandle onMouseDown={handleResizeDown} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default function WidgetColumnBar({ children }) {
|
||||
const childArray = _.isArray(children) ? children : [children];
|
||||
const [refNode, dimensions] = useDimensions();
|
||||
const [collapsedWidgets, setCollapsedWidgets] = React.useState(() =>
|
||||
childArray.filter((x) => x && x.props.collapsed).map((x) => x.props.name)
|
||||
);
|
||||
const toggleCollapsed = (name) => {
|
||||
if (collapsedWidgets.includes(name)) setCollapsedWidgets(collapsedWidgets.filter((x) => x != name));
|
||||
else setCollapsedWidgets([...collapsedWidgets, name]);
|
||||
};
|
||||
|
||||
return (
|
||||
<WidgetsMainContainer ref={refNode}>
|
||||
{childArray.map((widget, index) => {
|
||||
if (!widget) return null;
|
||||
return (
|
||||
<React.Fragment key={widget.props.name}>
|
||||
<WidgetTitle onClick={() => toggleCollapsed(widget.props.name)}>{widget.props.title}</WidgetTitle>
|
||||
<WidgetContainer
|
||||
parentHeight={dimensions && dimensions.height}
|
||||
visible={!collapsedWidgets.includes(widget.props.name)}
|
||||
widget={widget}
|
||||
initialSize={widget.props.height}
|
||||
splitterVisible={!!childArray.slice(index + 1).find((x) => x && !collapsedWidgets.includes(x.props.name))}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</WidgetsMainContainer>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @ts-nocheck
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import theme from '../theme';
|
||||
// import theme from '../theme';
|
||||
import { useLeftPanelWidth } from '../utility/globalState';
|
||||
|
||||
export const SearchBoxWrapper = styled.div`
|
||||
@@ -19,17 +19,27 @@ export const WidgetsMainContainer = styled.div`
|
||||
`;
|
||||
|
||||
const StyledWidgetsOuterContainer = styled.div`
|
||||
flex: 1 1 0;
|
||||
overflow: hidden;
|
||||
width: ${(props) => props.leftPanelWidth}px;
|
||||
// width: ${(props) => props.leftPanelWidth}px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export function WidgetsOuterContainer({ children }) {
|
||||
const leftPanelWidth = useLeftPanelWidth();
|
||||
return <StyledWidgetsOuterContainer leftPanelWidth={leftPanelWidth}>{children}</StyledWidgetsOuterContainer>;
|
||||
export function WidgetsOuterContainer({ children, style = undefined, refNode = undefined }) {
|
||||
// const leftPanelWidth = useLeftPanelWidth();
|
||||
return (
|
||||
<StyledWidgetsOuterContainer
|
||||
ref={refNode}
|
||||
// leftPanelWidth={leftPanelWidth}
|
||||
style={{
|
||||
...style,
|
||||
flex: style && (style.height != null || style.width != null) ? undefined : '1 1 0',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</StyledWidgetsOuterContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export const StyledWidgetsInnerContainer = styled.div`
|
||||
@@ -53,14 +63,16 @@ const StyledWidgetTitle = styled.div`
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
background-color: gray;
|
||||
border: 1px solid #aaa;
|
||||
// background-color: #CEC;
|
||||
`;
|
||||
|
||||
export function WidgetTitle({ children, inputRef = undefined }) {
|
||||
export function WidgetTitle({ children, inputRef = undefined, onClick = undefined }) {
|
||||
return (
|
||||
<StyledWidgetTitle
|
||||
onClick={() => {
|
||||
if (inputRef && inputRef.current) inputRef.current.focus();
|
||||
if (onClick) onClick();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -2199,7 +2199,7 @@ arrify@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
|
||||
|
||||
asap@~2.0.6:
|
||||
asap@~2.0.3, asap@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
||||
@@ -2297,6 +2297,11 @@ atob@^2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
|
||||
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
|
||||
|
||||
attr-accept@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
|
||||
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==
|
||||
|
||||
autoprefixer@^9.6.1:
|
||||
version "9.7.4"
|
||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.4.tgz#f8bf3e06707d047f0641d87aee8cfb174b2a5378"
|
||||
@@ -2523,6 +2528,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
|
||||
integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c=
|
||||
|
||||
base16@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/base16/-/base16-1.0.0.tgz#e297f60d7ec1014a7a971a39ebc8a98c0b681e70"
|
||||
integrity sha1-4pf2DX7BAUp6lxo568ipjAtoHnA=
|
||||
|
||||
base64-arraybuffer@0.1.5:
|
||||
version "0.1.5"
|
||||
resolved "https://registry.yarnpkg.com/base64-arraybuffer/-/base64-arraybuffer-0.1.5.tgz#73926771923b5a19747ad666aa5cd4bf9c6e9ce8"
|
||||
@@ -2905,6 +2915,13 @@ builtin-status-codes@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
integrity sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=
|
||||
|
||||
busboy@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/busboy/-/busboy-0.3.1.tgz#170899274c5bf38aae27d5c62b71268cd585fd1b"
|
||||
integrity sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==
|
||||
dependencies:
|
||||
dicer "0.3.0"
|
||||
|
||||
byline@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
|
||||
@@ -3569,6 +3586,11 @@ core-js-pure@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
|
||||
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
|
||||
|
||||
core-js@^1.0.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
|
||||
|
||||
core-js@^2.4.0:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
|
||||
@@ -4206,6 +4228,13 @@ detect-port-alt@1.1.6:
|
||||
address "^1.0.1"
|
||||
debug "^2.6.0"
|
||||
|
||||
dicer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/dicer/-/dicer-0.3.0.tgz#eacd98b3bfbf92e8ab5c2fdb71aaac44bb06b872"
|
||||
integrity sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==
|
||||
dependencies:
|
||||
streamsearch "0.1.2"
|
||||
|
||||
diff-match-patch@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/diff-match-patch/-/diff-match-patch-1.0.4.tgz#6ac4b55237463761c4daf0dc603eb869124744b1"
|
||||
@@ -4450,6 +4479,13 @@ encodeurl@~1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
|
||||
|
||||
encoding@^0.1.11:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
|
||||
integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
|
||||
dependencies:
|
||||
iconv-lite "^0.6.2"
|
||||
|
||||
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
@@ -4969,6 +5005,13 @@ express-basic-auth@^1.2.0:
|
||||
dependencies:
|
||||
basic-auth "^2.0.1"
|
||||
|
||||
express-fileupload@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/express-fileupload/-/express-fileupload-1.2.0.tgz#356c4dfd645be71ab9fb2f4e6d84eeb00d247979"
|
||||
integrity sha512-oe4WpKcSppXnl5peornawWUa6tKmIc1/kJxMNRGJR1A0v4zyLL6VsFR6wZ8P2a4Iq3aGx8xae3Vlr+MOMQhFPw==
|
||||
dependencies:
|
||||
busboy "^0.3.1"
|
||||
|
||||
express@^4.17.1:
|
||||
version "4.17.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
|
||||
@@ -5122,6 +5165,26 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "2.1.1"
|
||||
|
||||
fbemitter@^2.0.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/fbemitter/-/fbemitter-2.1.1.tgz#523e14fdaf5248805bb02f62efc33be703f51865"
|
||||
integrity sha1-Uj4U/a9SSIBbsC9i78M75wP1GGU=
|
||||
dependencies:
|
||||
fbjs "^0.8.4"
|
||||
|
||||
fbjs@^0.8.0, fbjs@^0.8.4:
|
||||
version "0.8.17"
|
||||
resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
|
||||
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
|
||||
dependencies:
|
||||
core-js "^1.0.0"
|
||||
isomorphic-fetch "^2.1.1"
|
||||
loose-envify "^1.0.0"
|
||||
object-assign "^4.1.0"
|
||||
promise "^7.1.1"
|
||||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.18"
|
||||
|
||||
figgy-pudding@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
|
||||
@@ -5149,6 +5212,13 @@ file-loader@4.3.0:
|
||||
loader-utils "^1.2.3"
|
||||
schema-utils "^2.5.0"
|
||||
|
||||
file-selector@^0.2.2:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.3.tgz#e2958cdd4366f95e59dc618b95c700abe72ed7a6"
|
||||
integrity sha512-d+hc9ctodLSVG55V2V5I4/eJBEr2p2na/kDN46Ty7PBhdp/Q5NmeQTXKa1Hx3AcIL1lgSFKZI0ve/v5ZXGCDkQ==
|
||||
dependencies:
|
||||
tslib "^2.0.3"
|
||||
|
||||
file-uri-to-path@1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
|
||||
@@ -5221,6 +5291,14 @@ find-free-port@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-2.0.0.tgz#4b22e5f6579eb1a38c41ac6bcb3efed1b6da9b1b"
|
||||
integrity sha1-SyLl9leesaOMQaxryz7+0bbamxs=
|
||||
|
||||
find-remove@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/find-remove/-/find-remove-2.0.3.tgz#46e1cb12ca247963bc594c3af4c4878ba2244a79"
|
||||
integrity sha512-T7v/mSYhz2mv3ZM37BJH6Ny9tudA1qjJtH9N8MAZQKskJ546ciyMjPxsT7N/9OcbpN9+vfxMNHefzLbXOdMrhQ==
|
||||
dependencies:
|
||||
fmerge "1.2.0"
|
||||
rimraf "3.0.2"
|
||||
|
||||
find-root@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/find-root/-/find-root-1.1.0.tgz#abcfc8ba76f708c42a97b3d685b7e9450bfb9ce4"
|
||||
@@ -5301,6 +5379,19 @@ flush-write-stream@^1.0.0:
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^2.3.6"
|
||||
|
||||
flux@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/flux/-/flux-3.1.3.tgz#d23bed515a79a22d933ab53ab4ada19d05b2f08a"
|
||||
integrity sha1-0jvtUVp5oi2TOrU6tK2hnQWy8Io=
|
||||
dependencies:
|
||||
fbemitter "^2.0.0"
|
||||
fbjs "^0.8.0"
|
||||
|
||||
fmerge@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/fmerge/-/fmerge-1.2.0.tgz#36e99d2ae255e3ee1af666b4df780553671cf692"
|
||||
integrity sha1-NumdKuJV4+4a9ma033gFU2cc9pI=
|
||||
|
||||
follow-redirects@1.5.10:
|
||||
version "1.5.10"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
|
||||
@@ -6007,6 +6098,13 @@ iconv-lite@^0.5.0:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@^0.6.2:
|
||||
version "0.6.2"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01"
|
||||
integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
icss-utils@^4.0.0, icss-utils@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
|
||||
@@ -6514,7 +6612,7 @@ is-root@2.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-root/-/is-root-2.1.0.tgz#809e18129cf1129644302a4f8544035d51984a9c"
|
||||
integrity sha512-AGOriNp96vNBd3HtU+RzFEc75FfR5ymiYv8E553I71SCeXBiMsVDUtdio1OEFvrPyLIQ9tVR5RxXIFe5PUFjMg==
|
||||
|
||||
is-stream@^1.0.0, is-stream@^1.1.0:
|
||||
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
@@ -6590,6 +6688,14 @@ isobject@^3.0.0, isobject@^3.0.1:
|
||||
resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df"
|
||||
integrity sha1-TkMekrEalzFjaqH5yNHMvP2reN8=
|
||||
|
||||
isomorphic-fetch@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
|
||||
dependencies:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -7464,6 +7570,11 @@ lodash._reinterpolate@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
|
||||
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
|
||||
|
||||
lodash.curry@^4.0.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash.curry/-/lodash.curry-4.1.1.tgz#248e36072ede906501d75966200a86dab8b23170"
|
||||
integrity sha1-JI42By7ekGUB11lmIAqG2riyMXA=
|
||||
|
||||
lodash.defaults@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c"
|
||||
@@ -7484,6 +7595,11 @@ lodash.flatten@^4.4.0:
|
||||
resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f"
|
||||
integrity sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=
|
||||
|
||||
lodash.flow@^3.3.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.flow/-/lodash.flow-3.5.0.tgz#87bf40292b8cf83e4e8ce1a3ae4209e20071675a"
|
||||
integrity sha1-h79AKSuM+D5OjOGjrkIJ4gBxZ1o=
|
||||
|
||||
lodash.get@^4.4.2:
|
||||
version "4.4.2"
|
||||
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
|
||||
@@ -8075,6 +8191,14 @@ no-case@^2.2.0:
|
||||
dependencies:
|
||||
lower-case "^1.1.1"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
|
||||
dependencies:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-forge@0.9.0:
|
||||
version "0.9.0"
|
||||
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.9.0.tgz#d624050edbb44874adca12bb9a52ec63cb782579"
|
||||
@@ -9687,6 +9811,13 @@ promise-inflight@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
|
||||
integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM=
|
||||
|
||||
promise@^7.1.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
promise@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.yarnpkg.com/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6"
|
||||
@@ -9791,6 +9922,11 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
pure-color@^1.2.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/pure-color/-/pure-color-1.3.0.tgz#1fe064fb0ac851f0de61320a8bf796836422f33e"
|
||||
integrity sha1-H+Bk+wrIUfDeYTIKi/eWg2Qi8z4=
|
||||
|
||||
q@^1.1.2:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
|
||||
@@ -9899,6 +10035,16 @@ react-app-polyfill@^1.0.5:
|
||||
regenerator-runtime "^0.13.3"
|
||||
whatwg-fetch "^3.0.0"
|
||||
|
||||
react-base16-styling@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-base16-styling/-/react-base16-styling-0.6.0.tgz#ef2156d66cf4139695c8a167886cb69ea660792c"
|
||||
integrity sha1-7yFW1mz0E5aVyKFniGy2nqZgeSw=
|
||||
dependencies:
|
||||
base16 "^1.0.0"
|
||||
lodash.curry "^4.0.1"
|
||||
lodash.flow "^3.3.0"
|
||||
pure-color "^1.2.0"
|
||||
|
||||
react-dev-utils@^10.0.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-10.1.0.tgz#ccf82135f6dc2fc91969bc729ce57a69d8e86025"
|
||||
@@ -9939,6 +10085,15 @@ react-dom@^16.12.0:
|
||||
prop-types "^15.6.2"
|
||||
scheduler "^0.18.0"
|
||||
|
||||
react-dropzone@^11.2.3:
|
||||
version "11.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.2.3.tgz#8a49e9fc7ab75eaccf748c382a790092c035cef1"
|
||||
integrity sha512-D99BhHm7H1h7ztUH/FLDo4wDy7VzXMbHoS/tUZHtoaY37Y55uadq0kUKqoaJ8PXl+niqdb5t6GankuEaAL6Vwg==
|
||||
dependencies:
|
||||
attr-accept "^2.2.1"
|
||||
file-selector "^0.2.2"
|
||||
prop-types "^15.7.2"
|
||||
|
||||
react-error-overlay@^6.0.5:
|
||||
version "6.0.5"
|
||||
resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.5.tgz#55d59c2a3810e8b41922e0b4e5f85dcf239bd533"
|
||||
@@ -9966,7 +10121,17 @@ react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4:
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||
|
||||
react-lifecycles-compat@^3.0.0:
|
||||
react-json-view@^1.19.1:
|
||||
version "1.19.1"
|
||||
resolved "https://registry.yarnpkg.com/react-json-view/-/react-json-view-1.19.1.tgz#95d8e59e024f08a25e5dc8f076ae304eed97cf5c"
|
||||
integrity sha512-u5e0XDLIs9Rj43vWkKvwL8G3JzvXSl6etuS5G42a8klMohZuYFQzSN6ri+/GiBptDqlrXPTdExJVU7x9rrlXhg==
|
||||
dependencies:
|
||||
flux "^3.1.3"
|
||||
react-base16-styling "^0.6.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
react-textarea-autosize "^6.1.0"
|
||||
|
||||
react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
@@ -10055,6 +10220,13 @@ react-select@^3.1.0:
|
||||
react-input-autosize "^2.2.2"
|
||||
react-transition-group "^4.3.0"
|
||||
|
||||
react-textarea-autosize@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/react-textarea-autosize/-/react-textarea-autosize-6.1.0.tgz#df91387f8a8f22020b77e3833c09829d706a09a5"
|
||||
integrity sha512-F6bI1dgib6fSvG8so1HuArPUv+iVEfPliuLWusLF+gAKz0FbB4jLrWUrTAeq1afnPT2c9toEZYUdz/y1uKMy4A==
|
||||
dependencies:
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-transition-group@^4.3.0:
|
||||
version "4.4.1"
|
||||
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9"
|
||||
@@ -10525,7 +10697,7 @@ rimraf@2.6.3:
|
||||
dependencies:
|
||||
glob "^7.1.3"
|
||||
|
||||
rimraf@^3.0.0:
|
||||
rimraf@3.0.2, rimraf@^3.0.0:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a"
|
||||
integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==
|
||||
@@ -10583,7 +10755,7 @@ safe-regex@^1.1.0:
|
||||
dependencies:
|
||||
ret "~0.1.10"
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -10769,7 +10941,7 @@ set-value@^2.0.0, set-value@^2.0.1:
|
||||
is-plain-object "^2.0.3"
|
||||
split-string "^3.0.1"
|
||||
|
||||
setimmediate@^1.0.4, setimmediate@~1.0.4:
|
||||
setimmediate@^1.0.4, setimmediate@^1.0.5, setimmediate@~1.0.4:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
||||
@@ -11230,6 +11402,11 @@ stream-transform@^2.0.1:
|
||||
dependencies:
|
||||
mixme "^0.3.1"
|
||||
|
||||
streamsearch@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-0.1.2.tgz#808b9d0e56fc273d809ba57338e929919a1a9f1a"
|
||||
integrity sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=
|
||||
|
||||
strict-uri-encode@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
|
||||
@@ -11796,6 +11973,11 @@ tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.2:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a"
|
||||
integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
|
||||
|
||||
tslib@^2.0.3:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c"
|
||||
integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ==
|
||||
|
||||
tsutils@^3.17.1:
|
||||
version "3.17.1"
|
||||
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759"
|
||||
@@ -11865,6 +12047,11 @@ typescript@^3.7.4, typescript@^3.7.5:
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
|
||||
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.22"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.22.tgz#960df60a5f911ea8f1c818f3747b99c6e177eae3"
|
||||
integrity sha512-YUxzMjJ5T71w6a8WWVcMGM6YWOTX27rCoIQgLXiWaxqXSx9D7DNjiGWn1aJIRSQ5qr0xuhra77bSIh6voR/46Q==
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.10"
|
||||
resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
|
||||
@@ -12400,6 +12587,11 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-fetch@>=0.10.0:
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz#e5f871572d6879663fa5674c8f833f15a8425ab3"
|
||||
integrity sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==
|
||||
|
||||
whatwg-fetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
|
||||
Reference in New Issue
Block a user