mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-23 09:16:01 +00:00
Merge branch 'develop'
This commit is contained in:
@@ -44,7 +44,6 @@
|
|||||||
"line-reader": "^0.4.0",
|
"line-reader": "^0.4.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"ncp": "^2.0.0",
|
"ncp": "^2.0.0",
|
||||||
"nedb-promises": "^4.0.1",
|
|
||||||
"node-cron": "^2.0.3",
|
"node-cron": "^2.0.3",
|
||||||
"node-ssh-forward": "^0.7.2",
|
"node-ssh-forward": "^0.7.2",
|
||||||
"portfinder": "^1.0.28",
|
"portfinder": "^1.0.28",
|
||||||
|
|||||||
264
packages/api/src/controllers/apps.js
Normal file
264
packages/api/src/controllers/apps.js
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const path = require('path');
|
||||||
|
const { appdir } = require('../utility/directories');
|
||||||
|
const socket = require('../utility/socket');
|
||||||
|
const connections = require('./connections');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
folders_meta: true,
|
||||||
|
async folders() {
|
||||||
|
const folders = await fs.readdir(appdir());
|
||||||
|
return [
|
||||||
|
...folders.map(name => ({
|
||||||
|
name,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
createFolder_meta: true,
|
||||||
|
async createFolder({ folder }) {
|
||||||
|
const name = await this.getNewAppFolder({ name: folder });
|
||||||
|
await fs.mkdir(path.join(appdir(), name));
|
||||||
|
socket.emitChanged('app-folders-changed');
|
||||||
|
return name;
|
||||||
|
},
|
||||||
|
|
||||||
|
files_meta: true,
|
||||||
|
async files({ folder }) {
|
||||||
|
const dir = path.join(appdir(), folder);
|
||||||
|
if (!(await fs.exists(dir))) return [];
|
||||||
|
const files = await fs.readdir(dir);
|
||||||
|
|
||||||
|
function fileType(ext, type) {
|
||||||
|
return files
|
||||||
|
.filter(name => name.endsWith(ext))
|
||||||
|
.map(name => ({
|
||||||
|
name: name.slice(0, -ext.length),
|
||||||
|
label: path.parse(name.slice(0, -ext.length)).base,
|
||||||
|
type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
...fileType('.command.sql', 'command.sql'),
|
||||||
|
...fileType('.query.sql', 'query.sql'),
|
||||||
|
...fileType('.config.json', 'config.json'),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
async emitChangedDbApp(folder) {
|
||||||
|
const used = await this.getUsedAppFolders();
|
||||||
|
if (used.includes(folder)) {
|
||||||
|
socket.emitChanged('used-apps-changed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFiles_meta: true,
|
||||||
|
async refreshFiles({ folder }) {
|
||||||
|
socket.emitChanged(`app-files-changed-${folder}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshFolders_meta: true,
|
||||||
|
async refreshFolders() {
|
||||||
|
socket.emitChanged(`app-folders-changed`);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFile_meta: true,
|
||||||
|
async deleteFile({ folder, file, fileType }) {
|
||||||
|
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
|
||||||
|
socket.emitChanged(`app-files-changed-${folder}`);
|
||||||
|
this.emitChangedDbApp(folder);
|
||||||
|
},
|
||||||
|
|
||||||
|
renameFile_meta: true,
|
||||||
|
async renameFile({ folder, file, newFile, fileType }) {
|
||||||
|
await fs.rename(
|
||||||
|
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
|
||||||
|
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
|
||||||
|
);
|
||||||
|
socket.emitChanged(`app-files-changed-${folder}`);
|
||||||
|
this.emitChangedDbApp(folder);
|
||||||
|
},
|
||||||
|
|
||||||
|
renameFolder_meta: true,
|
||||||
|
async renameFolder({ folder, newFolder }) {
|
||||||
|
const uniqueName = await this.getNewAppFolder({ name: newFolder });
|
||||||
|
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
|
||||||
|
socket.emitChanged(`app-folders-changed`);
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteFolder_meta: true,
|
||||||
|
async deleteFolder({ folder }) {
|
||||||
|
if (!folder) throw new Error('Missing folder parameter');
|
||||||
|
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
|
||||||
|
socket.emitChanged(`app-folders-changed`);
|
||||||
|
},
|
||||||
|
|
||||||
|
async getNewAppFolder({ name }) {
|
||||||
|
if (!(await fs.exists(path.join(appdir(), name)))) return name;
|
||||||
|
let index = 2;
|
||||||
|
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
return `${name}${index}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
getUsedAppFolders_meta: true,
|
||||||
|
async getUsedAppFolders() {
|
||||||
|
const list = await connections.list();
|
||||||
|
const apps = [];
|
||||||
|
|
||||||
|
for (const connection of list) {
|
||||||
|
for (const db of connection.databases || []) {
|
||||||
|
for (const key of _.keys(db || {})) {
|
||||||
|
if (key.startsWith('useApp:') && db[key]) {
|
||||||
|
apps.push(key.substring('useApp:'.length));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
|
||||||
|
},
|
||||||
|
|
||||||
|
getUsedApps_meta: true,
|
||||||
|
async getUsedApps() {
|
||||||
|
const apps = await this.getUsedAppFolders();
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
for (const folder of apps) {
|
||||||
|
res.push(await this.loadApp({ folder }));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
// getAppsForDb_meta: true,
|
||||||
|
// async getAppsForDb({ conid, database }) {
|
||||||
|
// const connection = await connections.get({ conid });
|
||||||
|
// if (!connection) return [];
|
||||||
|
// const db = (connection.databases || []).find(x => x.name == database);
|
||||||
|
// const apps = [];
|
||||||
|
// const res = [];
|
||||||
|
// if (db) {
|
||||||
|
// for (const key of _.keys(db || {})) {
|
||||||
|
// if (key.startsWith('useApp:') && db[key]) {
|
||||||
|
// apps.push(key.substring('useApp:'.length));
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// for (const folder of apps) {
|
||||||
|
// res.push(await this.loadApp({ folder }));
|
||||||
|
// }
|
||||||
|
// return res;
|
||||||
|
// },
|
||||||
|
|
||||||
|
loadApp_meta: true,
|
||||||
|
async loadApp({ folder }) {
|
||||||
|
const res = {
|
||||||
|
queries: [],
|
||||||
|
commands: [],
|
||||||
|
name: folder,
|
||||||
|
};
|
||||||
|
const dir = path.join(appdir(), folder);
|
||||||
|
if (await fs.exists(dir)) {
|
||||||
|
const files = await fs.readdir(dir);
|
||||||
|
|
||||||
|
async function processType(ext, field) {
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith(ext)) {
|
||||||
|
res[field].push({
|
||||||
|
name: file.slice(0, -ext.length),
|
||||||
|
sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await processType('.command.sql', 'commands');
|
||||||
|
await processType('.query.sql', 'queries');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
res.virtualReferences = JSON.parse(
|
||||||
|
await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
res.virtualReferences = [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
res.dictionaryDescriptions = JSON.parse(
|
||||||
|
await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
res.dictionaryDescriptions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
|
||||||
|
async saveConfigFile(appFolder, filename, filterFunc, newItem) {
|
||||||
|
const file = path.join(appdir(), appFolder, filename);
|
||||||
|
|
||||||
|
let json;
|
||||||
|
try {
|
||||||
|
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||||
|
} catch (err) {
|
||||||
|
json = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterFunc) {
|
||||||
|
json = json.filter(filterFunc);
|
||||||
|
}
|
||||||
|
|
||||||
|
json = [...json, newItem];
|
||||||
|
|
||||||
|
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||||
|
|
||||||
|
socket.emitChanged(`app-files-changed-${appFolder}`);
|
||||||
|
socket.emitChanged('used-apps-changed');
|
||||||
|
},
|
||||||
|
|
||||||
|
saveVirtualReference_meta: true,
|
||||||
|
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||||
|
await this.saveConfigFile(
|
||||||
|
appFolder,
|
||||||
|
'virtual-references.config.json',
|
||||||
|
columns.length == 1
|
||||||
|
? x =>
|
||||||
|
!(
|
||||||
|
x.schemaName == schemaName &&
|
||||||
|
x.pureName == pureName &&
|
||||||
|
x.columns.length == 1 &&
|
||||||
|
x.columns[0].columnName == columns[0].columnName
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
{
|
||||||
|
schemaName,
|
||||||
|
pureName,
|
||||||
|
refSchemaName,
|
||||||
|
refTableName,
|
||||||
|
columns,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
saveDictionaryDescription_meta: true,
|
||||||
|
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||||
|
await this.saveConfigFile(
|
||||||
|
appFolder,
|
||||||
|
'dictionary-descriptions.config.json',
|
||||||
|
x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||||
|
{
|
||||||
|
schemaName,
|
||||||
|
pureName,
|
||||||
|
expression,
|
||||||
|
columns,
|
||||||
|
delimiter,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { fork } = require('child_process');
|
const { fork } = require('child_process');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const nedb = require('nedb-promises');
|
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
|
|
||||||
const { datadir, filesdir } = require('../utility/directories');
|
const { datadir, filesdir } = require('../utility/directories');
|
||||||
@@ -9,6 +8,7 @@ const socket = require('../utility/socket');
|
|||||||
const { encryptConnection } = require('../utility/crypting');
|
const { encryptConnection } = require('../utility/crypting');
|
||||||
const { handleProcessCommunication } = require('../utility/processComm');
|
const { handleProcessCommunication } = require('../utility/processComm');
|
||||||
const { pickSafeConnectionInfo } = require('../utility/crypting');
|
const { pickSafeConnectionInfo } = require('../utility/crypting');
|
||||||
|
const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
||||||
|
|
||||||
const processArgs = require('../utility/processArgs');
|
const processArgs = require('../utility/processArgs');
|
||||||
|
|
||||||
@@ -136,7 +136,7 @@ module.exports = {
|
|||||||
const dir = datadir();
|
const dir = datadir();
|
||||||
if (!portalConnections) {
|
if (!portalConnections) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.datastore = nedb.create(path.join(dir, 'connections.jsonl'));
|
this.datastore = new JsonLinesDatabase(path.join(dir, 'connections.jsonl'));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -173,18 +173,22 @@ module.exports = {
|
|||||||
let res;
|
let res;
|
||||||
const encrypted = encryptConnection(connection);
|
const encrypted = encryptConnection(connection);
|
||||||
if (connection._id) {
|
if (connection._id) {
|
||||||
res = await this.datastore.update(_.pick(connection, '_id'), encrypted);
|
res = await this.datastore.update(encrypted);
|
||||||
} else {
|
} else {
|
||||||
res = await this.datastore.insert(encrypted);
|
res = await this.datastore.insert(encrypted);
|
||||||
}
|
}
|
||||||
socket.emitChanged('connection-list-changed');
|
socket.emitChanged('connection-list-changed');
|
||||||
|
socket.emitChanged('used-apps-changed');
|
||||||
|
// for (const db of connection.databases || []) {
|
||||||
|
// socket.emitChanged(`db-apps-changed-${connection._id}-${db.name}`);
|
||||||
|
// }
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
update_meta: true,
|
update_meta: true,
|
||||||
async update({ _id, values }) {
|
async update({ _id, values }) {
|
||||||
if (portalConnections) return;
|
if (portalConnections) return;
|
||||||
const res = await this.datastore.update({ _id }, { $set: values });
|
const res = await this.datastore.patch(_id, values);
|
||||||
socket.emitChanged('connection-list-changed');
|
socket.emitChanged('connection-list-changed');
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@@ -192,22 +196,24 @@ module.exports = {
|
|||||||
updateDatabase_meta: true,
|
updateDatabase_meta: true,
|
||||||
async updateDatabase({ conid, database, values }) {
|
async updateDatabase({ conid, database, values }) {
|
||||||
if (portalConnections) return;
|
if (portalConnections) return;
|
||||||
const conn = await this.datastore.find({ _id: conid });
|
const conn = await this.datastore.get(conid);
|
||||||
let databases = conn[0].databases || [];
|
let databases = (conn && conn.databases) || [];
|
||||||
if (databases.find(x => x.name == database)) {
|
if (databases.find(x => x.name == database)) {
|
||||||
databases = databases.map(x => (x.name == database ? { ...x, ...values } : x));
|
databases = databases.map(x => (x.name == database ? { ...x, ...values } : x));
|
||||||
} else {
|
} else {
|
||||||
databases = [...databases, { name: database, ...values }];
|
databases = [...databases, { name: database, ...values }];
|
||||||
}
|
}
|
||||||
const res = await this.datastore.update({ _id: conid }, { $set: { databases } });
|
const res = await this.datastore.patch(conid, { databases });
|
||||||
socket.emitChanged('connection-list-changed');
|
socket.emitChanged('connection-list-changed');
|
||||||
|
socket.emitChanged('used-apps-changed');
|
||||||
|
// socket.emitChanged(`db-apps-changed-${conid}-${database}`);
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
|
|
||||||
delete_meta: true,
|
delete_meta: true,
|
||||||
async delete(connection) {
|
async delete(connection) {
|
||||||
if (portalConnections) return;
|
if (portalConnections) return;
|
||||||
const res = await this.datastore.remove(_.pick(connection, '_id'));
|
const res = await this.datastore.remove(connection._id);
|
||||||
socket.emitChanged('connection-list-changed');
|
socket.emitChanged('connection-list-changed');
|
||||||
return res;
|
return res;
|
||||||
},
|
},
|
||||||
@@ -215,8 +221,8 @@ module.exports = {
|
|||||||
get_meta: true,
|
get_meta: true,
|
||||||
async get({ conid }) {
|
async get({ conid }) {
|
||||||
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
|
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
|
||||||
const res = await this.datastore.find({ _id: conid });
|
const res = await this.datastore.get(conid);
|
||||||
return res[0] || null;
|
return res || null;
|
||||||
},
|
},
|
||||||
|
|
||||||
newSqliteDatabase_meta: true,
|
newSqliteDatabase_meta: true,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
const uuidv1 = require('uuid/v1');
|
const uuidv1 = require('uuid/v1');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir } = require('../utility/directories');
|
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir } = require('../utility/directories');
|
||||||
const getChartExport = require('../utility/getChartExport');
|
const getChartExport = require('../utility/getChartExport');
|
||||||
const hasPermission = require('../utility/hasPermission');
|
const hasPermission = require('../utility/hasPermission');
|
||||||
const socket = require('../utility/socket');
|
const socket = require('../utility/socket');
|
||||||
const scheduler = require('./scheduler');
|
const scheduler = require('./scheduler');
|
||||||
const getDiagramExport = require('../utility/getDiagramExport');
|
const getDiagramExport = require('../utility/getDiagramExport');
|
||||||
|
const apps = require('./apps');
|
||||||
|
|
||||||
function serialize(format, data) {
|
function serialize(format, data) {
|
||||||
if (format == 'text') return data;
|
if (format == 'text') return data;
|
||||||
@@ -74,6 +75,11 @@ module.exports = {
|
|||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
});
|
});
|
||||||
return deserialize(format, text);
|
return deserialize(format, text);
|
||||||
|
} else if (folder.startsWith('app:')) {
|
||||||
|
const text = await fs.readFile(path.join(appdir(), folder.substring('app:'.length), file), {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
});
|
||||||
|
return deserialize(format, text);
|
||||||
} else {
|
} else {
|
||||||
if (!hasPermission(`files/${folder}/read`)) return null;
|
if (!hasPermission(`files/${folder}/read`)) return null;
|
||||||
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
||||||
@@ -88,6 +94,12 @@ module.exports = {
|
|||||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||||
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
||||||
return true;
|
return true;
|
||||||
|
} else if (folder.startsWith('app:')) {
|
||||||
|
const app = folder.substring('app:'.length);
|
||||||
|
await fs.writeFile(path.join(appdir(), app, file), serialize(format, data));
|
||||||
|
socket.emitChanged(`app-files-changed-${app}`);
|
||||||
|
apps.emitChangedDbApp(folder);
|
||||||
|
return true;
|
||||||
} else {
|
} else {
|
||||||
if (!hasPermission(`files/${folder}/write`)) return false;
|
if (!hasPermission(`files/${folder}/write`)) return false;
|
||||||
const dir = path.join(filesdir(), folder);
|
const dir = path.join(filesdir(), folder);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
|
|||||||
const jsldata = require('./controllers/jsldata');
|
const jsldata = require('./controllers/jsldata');
|
||||||
const config = require('./controllers/config');
|
const config = require('./controllers/config');
|
||||||
const archive = require('./controllers/archive');
|
const archive = require('./controllers/archive');
|
||||||
|
const apps = require('./controllers/apps');
|
||||||
const uploads = require('./controllers/uploads');
|
const uploads = require('./controllers/uploads');
|
||||||
const plugins = require('./controllers/plugins');
|
const plugins = require('./controllers/plugins');
|
||||||
const files = require('./controllers/files');
|
const files = require('./controllers/files');
|
||||||
@@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
|
|||||||
useController(app, electron, '/files', files);
|
useController(app, electron, '/files', files);
|
||||||
useController(app, electron, '/scheduler', scheduler);
|
useController(app, electron, '/scheduler', scheduler);
|
||||||
useController(app, electron, '/query-history', queryHistory);
|
useController(app, electron, '/query-history', queryHistory);
|
||||||
|
useController(app, electron, '/apps', apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
function initializeElectronSender(electronSender) {
|
function initializeElectronSender(electronSender) {
|
||||||
|
|||||||
142
packages/api/src/utility/JsonLinesDatabase.js
Normal file
142
packages/api/src/utility/JsonLinesDatabase.js
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
const AsyncLock = require('async-lock');
|
||||||
|
const fs = require('fs-extra');
|
||||||
|
const uuidv1 = require('uuid/v1');
|
||||||
|
|
||||||
|
const lock = new AsyncLock();
|
||||||
|
|
||||||
|
// const lineReader = require('line-reader');
|
||||||
|
// const { fetchNextLineFromReader } = require('./JsonLinesDatastore');
|
||||||
|
|
||||||
|
class JsonLinesDatabase {
|
||||||
|
constructor(filename) {
|
||||||
|
this.filename = filename;
|
||||||
|
this.data = [];
|
||||||
|
this.loadedOk = false;
|
||||||
|
this.loadPerformed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _save() {
|
||||||
|
if (!this.loadedOk) {
|
||||||
|
// don't override data
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await fs.writeFile(this.filename, this.data.map(x => JSON.stringify(x)).join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
async _ensureLoaded() {
|
||||||
|
if (!this.loadPerformed) {
|
||||||
|
await lock.acquire('reader', async () => {
|
||||||
|
if (!this.loadPerformed) {
|
||||||
|
if (!(await fs.exists(this.filename))) {
|
||||||
|
this.loadedOk = true;
|
||||||
|
this.loadPerformed = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const text = await fs.readFile(this.filename, { encoding: 'utf-8' });
|
||||||
|
this.data = text
|
||||||
|
.split('\n')
|
||||||
|
.filter(x => x.trim())
|
||||||
|
.map(x => JSON.parse(x));
|
||||||
|
this.loadedOk = true;
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error loading file ${this.filename}`, err);
|
||||||
|
}
|
||||||
|
this.loadPerformed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async insert(obj) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
if (obj._id && (await this.get(obj._id))) {
|
||||||
|
throw new Error(`Cannot insert duplicate ID ${obj._id} into ${this.filename}`);
|
||||||
|
}
|
||||||
|
const elem = obj._id
|
||||||
|
? obj
|
||||||
|
: {
|
||||||
|
...obj,
|
||||||
|
_id: uuidv1(),
|
||||||
|
};
|
||||||
|
this.data.push(elem);
|
||||||
|
await this._save();
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(id) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
return this.data.find(x => x._id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async find(cond) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
if (cond) {
|
||||||
|
return this.data.filter(x => {
|
||||||
|
for (const key of Object.keys(cond)) {
|
||||||
|
if (x[key] != cond[key]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(obj) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
this.data = this.data.map(x => (x._id == obj._id ? obj : x));
|
||||||
|
await this._save();
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
async patch(id, values) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
this.data = this.data.map(x => (x._id == id ? { ...x, ...values } : x));
|
||||||
|
await this._save();
|
||||||
|
return this.data.find(x => x._id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id) {
|
||||||
|
await this._ensureLoaded();
|
||||||
|
const removed = this.data.find(x => x._id == id);
|
||||||
|
this.data = this.data.filter(x => x._id != id);
|
||||||
|
await this._save();
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// async _openReader() {
|
||||||
|
// return new Promise((resolve, reject) =>
|
||||||
|
// lineReader.open(this.filename, (err, reader) => {
|
||||||
|
// if (err) reject(err);
|
||||||
|
// resolve(reader);
|
||||||
|
// })
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async _read() {
|
||||||
|
// this.data = [];
|
||||||
|
// if (!(await fs.exists(this.filename))) return;
|
||||||
|
// try {
|
||||||
|
// const reader = await this._openReader();
|
||||||
|
// for (;;) {
|
||||||
|
// const line = await fetchNextLineFromReader(reader);
|
||||||
|
// if (!line) break;
|
||||||
|
// this.data.push(JSON.parse(line));
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error(`Error loading file ${this.filename}`, err);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async _write() {
|
||||||
|
// const fw = fs.createWriteStream(this.filename);
|
||||||
|
// for (const obj of this.data) {
|
||||||
|
// await fw.write(JSON.stringify(obj));
|
||||||
|
// await fw.write('\n');
|
||||||
|
// }
|
||||||
|
// await fw.end();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = JsonLinesDatabase;
|
||||||
@@ -4,7 +4,7 @@ const lock = new AsyncLock();
|
|||||||
const stableStringify = require('json-stable-stringify');
|
const stableStringify = require('json-stable-stringify');
|
||||||
const { evaluateCondition } = require('dbgate-sqltree');
|
const { evaluateCondition } = require('dbgate-sqltree');
|
||||||
|
|
||||||
async function fetchNextLine(reader) {
|
function fetchNextLineFromReader(reader) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (!reader.hasNextLine()) {
|
if (!reader.hasNextLine()) {
|
||||||
resolve(null);
|
resolve(null);
|
||||||
@@ -62,7 +62,7 @@ class JsonLinesDatastore {
|
|||||||
|
|
||||||
async _readLine(parse) {
|
async _readLine(parse) {
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const line = await fetchNextLine(this.reader);
|
const line = await fetchNextLineFromReader(this.reader);
|
||||||
if (!line) {
|
if (!line) {
|
||||||
// EOF
|
// EOF
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
|
|||||||
const uploadsdir = dirFunc('uploads', true);
|
const uploadsdir = dirFunc('uploads', true);
|
||||||
const pluginsdir = dirFunc('plugins');
|
const pluginsdir = dirFunc('plugins');
|
||||||
const archivedir = dirFunc('archive');
|
const archivedir = dirFunc('archive');
|
||||||
|
const appdir = dirFunc('apps');
|
||||||
const filesdir = dirFunc('files');
|
const filesdir = dirFunc('files');
|
||||||
|
|
||||||
function packagedPluginsDir() {
|
function packagedPluginsDir() {
|
||||||
@@ -103,6 +104,7 @@ module.exports = {
|
|||||||
rundir,
|
rundir,
|
||||||
uploadsdir,
|
uploadsdir,
|
||||||
archivedir,
|
archivedir,
|
||||||
|
appdir,
|
||||||
ensureDirectory,
|
ensureDirectory,
|
||||||
pluginsdir,
|
pluginsdir,
|
||||||
filesdir,
|
filesdir,
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export interface DisplayColumn {
|
|||||||
autoIncrement?: boolean;
|
autoIncrement?: boolean;
|
||||||
isPrimaryKey?: boolean;
|
isPrimaryKey?: boolean;
|
||||||
foreignKey?: ForeignKeyInfo;
|
foreignKey?: ForeignKeyInfo;
|
||||||
|
isForeignKeyUnique?: boolean;
|
||||||
isExpandable?: boolean;
|
isExpandable?: boolean;
|
||||||
isChecked?: boolean;
|
isChecked?: boolean;
|
||||||
hintColumnNames?: string[];
|
hintColumnNames?: string[];
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { filterName } from 'dbgate-tools';
|
import { filterName, isTableColumnUnique } from 'dbgate-tools';
|
||||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||||
import {
|
import {
|
||||||
TableInfo,
|
TableInfo,
|
||||||
@@ -79,10 +79,11 @@ export class TableGridDisplay extends GridDisplay {
|
|||||||
...col,
|
...col,
|
||||||
isChecked: this.isColumnChecked(col),
|
isChecked: this.isColumnChecked(col),
|
||||||
hintColumnNames:
|
hintColumnNames:
|
||||||
this.getFkDictionaryDescription(col.foreignKey)?.columns?.map(
|
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(
|
||||||
columnName => `hint_${col.uniqueName}_${columnName}`
|
columnName => `hint_${col.uniqueName}_${columnName}`
|
||||||
) || null,
|
) || null,
|
||||||
hintColumnDelimiter: this.getFkDictionaryDescription(col.foreignKey)?.delimiter,
|
hintColumnDelimiter: this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)
|
||||||
|
?.delimiter,
|
||||||
isExpandable: !!col.foreignKey,
|
isExpandable: !!col.foreignKey,
|
||||||
})) || []
|
})) || []
|
||||||
);
|
);
|
||||||
@@ -203,7 +204,8 @@ export class TableGridDisplay extends GridDisplay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFkTarget(column: DisplayColumn) {
|
getFkTarget(column: DisplayColumn) {
|
||||||
const { uniqueName, foreignKey } = column;
|
const { uniqueName, foreignKey, isForeignKeyUnique } = column;
|
||||||
|
if (!isForeignKeyUnique) return null;
|
||||||
const pureName = foreignKey.refTableName;
|
const pureName = foreignKey.refTableName;
|
||||||
const schemaName = foreignKey.refSchemaName;
|
const schemaName = foreignKey.refSchemaName;
|
||||||
return this.findTable({ schemaName, pureName });
|
return this.findTable({ schemaName, pureName });
|
||||||
@@ -230,7 +232,7 @@ export class TableGridDisplay extends GridDisplay {
|
|||||||
const uniquePath = [...parentPath, col.columnName];
|
const uniquePath = [...parentPath, col.columnName];
|
||||||
const uniqueName = uniquePath.join('.');
|
const uniqueName = uniquePath.join('.');
|
||||||
// console.log('this.config.addedColumns', this.config.addedColumns, uniquePath);
|
// console.log('this.config.addedColumns', this.config.addedColumns, uniquePath);
|
||||||
return {
|
const res = {
|
||||||
...col,
|
...col,
|
||||||
pureName: table.pureName,
|
pureName: table.pureName,
|
||||||
schemaName: table.schemaName,
|
schemaName: table.schemaName,
|
||||||
@@ -241,7 +243,19 @@ export class TableGridDisplay extends GridDisplay {
|
|||||||
foreignKey:
|
foreignKey:
|
||||||
table.foreignKeys &&
|
table.foreignKeys &&
|
||||||
table.foreignKeys.find(fk => fk.columns.length == 1 && fk.columns[0].columnName == col.columnName),
|
table.foreignKeys.find(fk => fk.columns.length == 1 && fk.columns[0].columnName == col.columnName),
|
||||||
|
isForeignKeyUnique: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (res.foreignKey) {
|
||||||
|
const refTableInfo = this.dbinfo.tables.find(
|
||||||
|
x => x.schemaName == res.foreignKey.refSchemaName && x.pureName == res.foreignKey.refTableName
|
||||||
|
);
|
||||||
|
if (refTableInfo && isTableColumnUnique(refTableInfo, res.foreignKey.columns[0].refColumnName)) {
|
||||||
|
res.isForeignKeyUnique = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
addAddedColumnsToSelect(
|
addAddedColumnsToSelect(
|
||||||
|
|||||||
@@ -29,18 +29,18 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
|||||||
// console.log('ANALYSING', name, structure);
|
// console.log('ANALYSING', name, structure);
|
||||||
if (structure && options.dropIfExists) {
|
if (structure && options.dropIfExists) {
|
||||||
console.log(`Dropping table ${fullNameQuoted}`);
|
console.log(`Dropping table ${fullNameQuoted}`);
|
||||||
await driver.query(pool, `DROP TABLE ${fullNameQuoted}`);
|
await driver.script(pool, `DROP TABLE ${fullNameQuoted}`);
|
||||||
}
|
}
|
||||||
if (options.createIfNotExists && (!structure || options.dropIfExists)) {
|
if (options.createIfNotExists && (!structure || options.dropIfExists)) {
|
||||||
console.log(`Creating table ${fullNameQuoted}`);
|
console.log(`Creating table ${fullNameQuoted}`);
|
||||||
const dmp = driver.createDumper();
|
const dmp = driver.createDumper();
|
||||||
dmp.createTable(prepareTableForImport({ ...writable.structure, ...name }));
|
dmp.createTable(prepareTableForImport({ ...writable.structure, ...name }));
|
||||||
console.log(dmp.s);
|
console.log(dmp.s);
|
||||||
await driver.query(pool, dmp.s);
|
await driver.script(pool, dmp.s);
|
||||||
structure = await driver.analyseSingleTable(pool, name);
|
structure = await driver.analyseSingleTable(pool, name);
|
||||||
}
|
}
|
||||||
if (options.truncate) {
|
if (options.truncate) {
|
||||||
await driver.query(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
|
await driver.script(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
writable.columnNames = _intersection(
|
writable.columnNames = _intersection(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { DatabaseInfo, TableInfo } from 'dbgate-types';
|
import { DatabaseInfo, TableInfo, ApplicationDefinition } from 'dbgate-types';
|
||||||
import _flatten from 'lodash/flatten';
|
import _flatten from 'lodash/flatten';
|
||||||
|
|
||||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||||
@@ -90,3 +90,31 @@ function fillDatabaseExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
|||||||
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
||||||
return fillDatabaseExtendedInfo(addTableDependencies(db));
|
return fillDatabaseExtendedInfo(addTableDependencies(db));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extendDatabaseInfoFromApps(db: DatabaseInfo, apps: ApplicationDefinition[]): DatabaseInfo {
|
||||||
|
if (!db || !apps) return db;
|
||||||
|
const dbExt = {
|
||||||
|
...db,
|
||||||
|
tables: db.tables.map(table => ({
|
||||||
|
...table,
|
||||||
|
foreignKeys: [
|
||||||
|
...(table.foreignKeys || []),
|
||||||
|
..._flatten(apps.map(app => app.virtualReferences || []))
|
||||||
|
.filter(fk => fk.pureName == table.pureName && fk.schemaName == table.schemaName)
|
||||||
|
.map(fk => ({ ...fk, constraintType: 'foreignKey', isVirtual: true })),
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
} as DatabaseInfo;
|
||||||
|
return addTableDependencies(dbExt);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTableColumnUnique(table: TableInfo, column: string) {
|
||||||
|
if (table.primaryKey && table.primaryKey.columns.length == 1 && table.primaryKey.columns[0].columnName == column) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const uqs = [...(table.uniques || []), ...(table.indexes || []).filter(x => x.isUnique)];
|
||||||
|
if (uqs.find(uq => uq.columns.length == 1 && uq.columns[0].columnName == column)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import _cloneDeep from 'lodash/cloneDeep';
|
|||||||
export function prepareTableForImport(table: TableInfo): TableInfo {
|
export function prepareTableForImport(table: TableInfo): TableInfo {
|
||||||
const res = _cloneDeep(table);
|
const res = _cloneDeep(table);
|
||||||
res.foreignKeys = [];
|
res.foreignKeys = [];
|
||||||
|
res.indexes = [];
|
||||||
|
res.uniques = [];
|
||||||
|
res.checks = [];
|
||||||
if (res.primaryKey) res.primaryKey.constraintName = null;
|
if (res.primaryKey) res.primaryKey.constraintName = null;
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|||||||
37
packages/types/appdefs.d.ts
vendored
Normal file
37
packages/types/appdefs.d.ts
vendored
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
interface ApplicationCommand {
|
||||||
|
name: string;
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApplicationQuery {
|
||||||
|
name: string;
|
||||||
|
sql: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface VirtualReferenceDefinition {
|
||||||
|
pureName: string;
|
||||||
|
schemaName?: string;
|
||||||
|
refSchemaName?: string;
|
||||||
|
refTableName: string;
|
||||||
|
columns: {
|
||||||
|
columnName: string;
|
||||||
|
refColumnName: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DictionaryDescriptionDefinition {
|
||||||
|
pureName: string;
|
||||||
|
schemaName?: string;
|
||||||
|
expression: string;
|
||||||
|
columns: string[];
|
||||||
|
delimiter: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApplicationDefinition {
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
queries: ApplicationQuery[];
|
||||||
|
commands: ApplicationCommand[];
|
||||||
|
virtualReferences: VirtualReferenceDefinition[];
|
||||||
|
dictionaryDescriptions: DictionaryDescriptionDefinition[];
|
||||||
|
}
|
||||||
3
packages/types/extensions.d.ts
vendored
3
packages/types/extensions.d.ts
vendored
@@ -22,9 +22,10 @@ export interface FileFormatDefinition {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ThemeDefinition {
|
export interface ThemeDefinition {
|
||||||
className: string;
|
themeClassName: string;
|
||||||
themeName: string;
|
themeName: string;
|
||||||
themeType: 'light' | 'dark';
|
themeType: 'light' | 'dark';
|
||||||
|
themeCss?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PluginDefinition {
|
export interface PluginDefinition {
|
||||||
|
|||||||
1
packages/types/index.d.ts
vendored
1
packages/types/index.d.ts
vendored
@@ -43,3 +43,4 @@ export * from './dumper';
|
|||||||
export * from './dbtypes';
|
export * from './dbtypes';
|
||||||
export * from './extensions';
|
export * from './extensions';
|
||||||
export * from './alter-processor';
|
export * from './alter-processor';
|
||||||
|
export * from './appdefs';
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
import { subscribeConnectionPingers } from './utility/connectionsPinger';
|
import { subscribeConnectionPingers } from './utility/connectionsPinger';
|
||||||
import { subscribePermissionCompiler } from './utility/hasPermission';
|
import { subscribePermissionCompiler } from './utility/hasPermission';
|
||||||
import { apiCall } from './utility/api';
|
import { apiCall } from './utility/api';
|
||||||
|
import { getUsedApps } from './utility/metadataLoaders';
|
||||||
|
|
||||||
let loadedApi = false;
|
let loadedApi = false;
|
||||||
|
|
||||||
@@ -30,7 +31,8 @@
|
|||||||
const settings = await apiCall('config/get-settings');
|
const settings = await apiCall('config/get-settings');
|
||||||
const connections = await apiCall('connections/list');
|
const connections = await apiCall('connections/list');
|
||||||
const config = await apiCall('config/get');
|
const config = await apiCall('config/get');
|
||||||
loadedApi = settings && connections && config;
|
const apps = await getUsedApps();
|
||||||
|
loadedApi = settings && connections && config && apps;
|
||||||
|
|
||||||
if (loadedApi) {
|
if (loadedApi) {
|
||||||
subscribeApiDependendStores();
|
subscribeApiDependendStores();
|
||||||
|
|||||||
@@ -26,6 +26,12 @@
|
|||||||
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
{#if $currentThemeDefinition?.themeCss}
|
||||||
|
{@html `<style id="themePlugin">${$currentThemeDefinition?.themeCss}</style>`}
|
||||||
|
{/if}
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
||||||
use:dragDropFileTarget
|
use:dragDropFileTarget
|
||||||
|
|||||||
119
packages/web/src/appobj/AppFileAppObject.svelte
Normal file
119
packages/web/src/appobj/AppFileAppObject.svelte
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
async function openTextFile(fileName, fileType, folderName, tabComponent, icon) {
|
||||||
|
const connProps: any = {};
|
||||||
|
let tooltip = undefined;
|
||||||
|
|
||||||
|
const resp = await apiCall('files/load', {
|
||||||
|
folder: 'app:' + folderName,
|
||||||
|
file: fileName + '.' + fileType,
|
||||||
|
format: 'text',
|
||||||
|
});
|
||||||
|
|
||||||
|
openNewTab(
|
||||||
|
{
|
||||||
|
title: fileName,
|
||||||
|
icon,
|
||||||
|
tabComponent,
|
||||||
|
tooltip,
|
||||||
|
props: {
|
||||||
|
savedFile:fileName + '.' + fileType,
|
||||||
|
savedFolder: 'app:' + folderName,
|
||||||
|
savedFormat: 'text',
|
||||||
|
appFolder: folderName,
|
||||||
|
...connProps,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ editor: resp }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractKey = data => data.fileName;
|
||||||
|
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
|
||||||
|
const APP_ICONS = {
|
||||||
|
'config.json': 'img json',
|
||||||
|
'command.sql': 'img app-command',
|
||||||
|
'query.sql': 'img app-query',
|
||||||
|
};
|
||||||
|
|
||||||
|
function getAppIcon(data) {
|
||||||
|
return APP_ICONS[data.fileType];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { filterName } from 'dbgate-tools';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
|
||||||
|
import openNewTab from '../utility/openNewTab';
|
||||||
|
import AppObjectCore from './AppObjectCore.svelte';
|
||||||
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
|
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { currentDatabase, currentDatabase } from '../stores';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
value: data.fileName,
|
||||||
|
label: 'New file name',
|
||||||
|
header: 'Rename file',
|
||||||
|
onConfirm: newFile => {
|
||||||
|
apiCall('apps/rename-file', {
|
||||||
|
file: data.fileName,
|
||||||
|
folder: data.folderName,
|
||||||
|
fileType: data.fileType,
|
||||||
|
newFile,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
showModal(ConfirmModal, {
|
||||||
|
message: `Really delete file ${data.fileName}?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
apiCall('apps/delete-file', {
|
||||||
|
file: data.fileName,
|
||||||
|
folder: data.folderName,
|
||||||
|
fileType: data.fileType,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const handleClick = () => {
|
||||||
|
if (data.fileType.endsWith('.sql')) {
|
||||||
|
handleOpenSqlFile();
|
||||||
|
}
|
||||||
|
if (data.fileType.endsWith('.json')) {
|
||||||
|
handleOpenJsonFile();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleOpenSqlFile = () => {
|
||||||
|
openTextFile(data.fileName, data.fileType, data.folderName, 'QueryTab', 'img sql-file');
|
||||||
|
};
|
||||||
|
const handleOpenJsonFile = () => {
|
||||||
|
openTextFile(data.fileName, data.fileType, data.folderName, 'JsonEditorTab', 'img json');
|
||||||
|
};
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return [
|
||||||
|
{ text: 'Delete', onClick: handleDelete },
|
||||||
|
{ text: 'Rename', onClick: handleRename },
|
||||||
|
data.fileType.endsWith('.sql') && { text: 'Open SQL', onClick: handleOpenSqlFile },
|
||||||
|
data.fileType.endsWith('.json') && { text: 'Open JSON', onClick: handleOpenJsonFile },
|
||||||
|
|
||||||
|
// data.fileType.endsWith('.yaml') && { text: 'Open YAML', onClick: handleOpenYamlFile },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppObjectCore
|
||||||
|
{...$$restProps}
|
||||||
|
{data}
|
||||||
|
title={data.fileLabel}
|
||||||
|
icon={getAppIcon(data)}
|
||||||
|
menu={createMenu}
|
||||||
|
on:click={handleClick}
|
||||||
|
/>
|
||||||
96
packages/web/src/appobj/AppFolderAppObject.svelte
Normal file
96
packages/web/src/appobj/AppFolderAppObject.svelte
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
export const extractKey = data => data.name;
|
||||||
|
export const createMatcher = data => filter => filterName(filter, data.name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import _, { find } from 'lodash';
|
||||||
|
import { filterName } from 'dbgate-tools';
|
||||||
|
|
||||||
|
import { currentApplication, currentDatabase } from '../stores';
|
||||||
|
|
||||||
|
import AppObjectCore from './AppObjectCore.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||||
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { useConnectionList } from '../utility/metadataLoaders';
|
||||||
|
|
||||||
|
export let data;
|
||||||
|
|
||||||
|
$: connections = useConnectionList();
|
||||||
|
|
||||||
|
const handleDelete = () => {
|
||||||
|
showModal(ConfirmModal, {
|
||||||
|
message: `Really delete application ${data.name}?`,
|
||||||
|
onConfirm: () => {
|
||||||
|
apiCall('apps/delete-folder', { folder: data.name });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRename = () => {
|
||||||
|
const { name } = data;
|
||||||
|
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
value: name,
|
||||||
|
label: 'New application name',
|
||||||
|
header: 'Rename application',
|
||||||
|
onConfirm: async newFolder => {
|
||||||
|
await apiCall('apps/rename-folder', {
|
||||||
|
folder: data.name,
|
||||||
|
newFolder: newFolder,
|
||||||
|
});
|
||||||
|
if ($currentApplication == data.name) {
|
||||||
|
$currentApplication = newFolder;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function setOnCurrentDb(value) {
|
||||||
|
apiCall('connections/update-database', {
|
||||||
|
conid: $currentDatabase?.connection?._id,
|
||||||
|
database: $currentDatabase?.name,
|
||||||
|
values: {
|
||||||
|
[`useApp:${data.name}`]: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return [
|
||||||
|
{ text: 'Delete', onClick: handleDelete },
|
||||||
|
{ text: 'Rename', onClick: handleRename },
|
||||||
|
|
||||||
|
$currentDatabase && [
|
||||||
|
!isOnCurrentDb($currentDatabase, $connections) && {
|
||||||
|
text: 'Enable on current database',
|
||||||
|
onClick: () => setOnCurrentDb(true),
|
||||||
|
},
|
||||||
|
isOnCurrentDb($currentDatabase, $connections) && {
|
||||||
|
text: 'Disable on current database',
|
||||||
|
onClick: () => setOnCurrentDb(false),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function isOnCurrentDb(currentDb, connections) {
|
||||||
|
const conn = connections.find(x => x._id == currentDb?.connection?._id);
|
||||||
|
const db = conn?.databases?.find(x => x.name == currentDb?.name);
|
||||||
|
return db && db[`useApp:${data.name}`];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AppObjectCore
|
||||||
|
{...$$restProps}
|
||||||
|
{data}
|
||||||
|
title={data.name}
|
||||||
|
icon={'img app'}
|
||||||
|
statusIcon={isOnCurrentDb($currentDatabase, $connections) ? 'icon check' : null}
|
||||||
|
statusTitle={`Application ${data.name} is used for database ${$currentDatabase?.name}`}
|
||||||
|
isBold={data.name == $currentApplication}
|
||||||
|
on:click={() => ($currentApplication = data.name)}
|
||||||
|
menu={createMenu}
|
||||||
|
/>
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||||
import getElectron from '../utility/getElectron';
|
import getElectron from '../utility/getElectron';
|
||||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||||
import { getDatabaseList } from '../utility/metadataLoaders';
|
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||||
import { getLocalStorage } from '../utility/storageCache';
|
import { getLocalStorage } from '../utility/storageCache';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
],
|
],
|
||||||
data.singleDatabase && [
|
data.singleDatabase && [
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
|
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase, $apps),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
@@ -186,6 +186,8 @@
|
|||||||
statusTitle = null;
|
statusTitle = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: apps = useUsedApps();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppObjectCore
|
<AppObjectCore
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
export const extractKey = props => props.name;
|
export const extractKey = props => props.name;
|
||||||
|
|
||||||
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase) {
|
export function getDatabaseMenuItems(connection, name, $extensions, $currentDatabase, $apps) {
|
||||||
|
const apps = filterAppsForDatabase(connection, name, $apps);
|
||||||
const handleNewQuery = () => {
|
const handleNewQuery = () => {
|
||||||
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
|
const tooltip = `${getConnectionLabel(connection)}\n${name}`;
|
||||||
openNewTab({
|
openNewTab({
|
||||||
@@ -157,8 +158,20 @@
|
|||||||
openJsonDocument(db, name);
|
openJsonDocument(db, name);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function handleConfirmSql(sql) {
|
||||||
|
const resp = await apiCall('database-connections/run-script', { conid: connection._id, database: name, sql });
|
||||||
|
const { errorMessage } = resp || {};
|
||||||
|
if (errorMessage) {
|
||||||
|
showModal(ErrorMessageModal, { title: 'Error when executing script', message: errorMessage });
|
||||||
|
} else {
|
||||||
|
showSnackbarSuccess('Saved to database');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const driver = findEngineDriver(connection, getExtensions());
|
const driver = findEngineDriver(connection, getExtensions());
|
||||||
|
|
||||||
|
const commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
||||||
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
|
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
|
||||||
@@ -180,6 +193,20 @@
|
|||||||
|
|
||||||
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
|
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
|
||||||
_.get($currentDatabase, 'name') == name && { onClick: handleDisconnect, text: 'Disconnect' },
|
_.get($currentDatabase, 'name') == name && { onClick: handleDisconnect, text: 'Disconnect' },
|
||||||
|
|
||||||
|
commands.length > 0 && [
|
||||||
|
{ divider: true },
|
||||||
|
commands.map((cmd: any) => ({
|
||||||
|
text: cmd.name,
|
||||||
|
onClick: () => {
|
||||||
|
showModal(ConfirmSqlModal, {
|
||||||
|
sql: cmd.sql,
|
||||||
|
onConfirm: () => handleConfirmSql(cmd.sql),
|
||||||
|
engine: driver.engine,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -207,18 +234,22 @@
|
|||||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
import { findEngineDriver } from 'dbgate-tools';
|
||||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||||
import { apiCall } from '../utility/api';
|
import { apiCall } from '../utility/api';
|
||||||
|
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||||
|
import ConfirmSqlModal from '../modals/ConfirmSqlModal.svelte';
|
||||||
|
import { filterAppsForDatabase } from '../utility/appTools';
|
||||||
|
|
||||||
export let data;
|
export let data;
|
||||||
export let passProps;
|
export let passProps;
|
||||||
|
|
||||||
function createMenu() {
|
function createMenu() {
|
||||||
return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase);
|
return getDatabaseMenuItems(data.connection, data.name, $extensions, $currentDatabase, $apps);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
|
$: isPinned = !!$pinnedDatabases.find(x => x.name == data.name && x.connection?._id == data.connection?._id);
|
||||||
|
$: apps = useUsedApps();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppObjectCore
|
<AppObjectCore
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import runCommand from './runCommand';
|
|||||||
function themeCommand(theme: ThemeDefinition) {
|
function themeCommand(theme: ThemeDefinition) {
|
||||||
return {
|
return {
|
||||||
text: theme.themeName,
|
text: theme.themeName,
|
||||||
onClick: () => currentTheme.set(theme.className),
|
onClick: () => currentTheme.set(theme.themeClassName),
|
||||||
// onPreview: () => {
|
// onPreview: () => {
|
||||||
// const old = get(currentTheme);
|
// const old = get(currentTheme);
|
||||||
// currentTheme.set(css);
|
// currentTheme.set(css);
|
||||||
@@ -128,6 +128,23 @@ registerCommand({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
registerCommand({
|
||||||
|
id: 'new.application',
|
||||||
|
category: 'New',
|
||||||
|
icon: 'img app',
|
||||||
|
name: 'Application',
|
||||||
|
onClick: () => {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
value: '',
|
||||||
|
label: 'New application name',
|
||||||
|
header: 'Create application',
|
||||||
|
onConfirm: async folder => {
|
||||||
|
apiCall('apps/create-folder', { folder });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
registerCommand({
|
registerCommand({
|
||||||
id: 'new.table',
|
id: 'new.table',
|
||||||
category: 'New',
|
category: 'New',
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
import { isTypeDateTime } from 'dbgate-tools';
|
import { isTypeDateTime } from 'dbgate-tools';
|
||||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
|
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||||
import { copyTextToClipboard } from '../utility/clipboard';
|
import { copyTextToClipboard } from '../utility/clipboard';
|
||||||
|
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
|
||||||
export let column;
|
export let column;
|
||||||
export let conid = undefined;
|
export let conid = undefined;
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
export let setSort;
|
export let setSort;
|
||||||
export let grouping = undefined;
|
export let grouping = undefined;
|
||||||
export let order = undefined;
|
export let order = undefined;
|
||||||
|
export let allowDefineVirtualReferences = false;
|
||||||
export let setGrouping;
|
export let setGrouping;
|
||||||
|
|
||||||
const openReferencedTable = () => {
|
const openReferencedTable = () => {
|
||||||
@@ -26,6 +29,16 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDefineVirtualForeignKey = () => {
|
||||||
|
showModal(VirtualForeignKeyEditorModal, {
|
||||||
|
schemaName: column.schemaName,
|
||||||
|
pureName: column.pureName,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
columnName: column.columnName,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function getMenu() {
|
function getMenu() {
|
||||||
return [
|
return [
|
||||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||||
@@ -49,6 +62,11 @@
|
|||||||
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
||||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||||
],
|
],
|
||||||
|
|
||||||
|
allowDefineVirtualReferences && [
|
||||||
|
{ divider: true },
|
||||||
|
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||||
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -303,6 +303,7 @@
|
|||||||
export let errorMessage = undefined;
|
export let errorMessage = undefined;
|
||||||
export let pureName = undefined;
|
export let pureName = undefined;
|
||||||
export let schemaName = undefined;
|
export let schemaName = undefined;
|
||||||
|
export let allowDefineVirtualReferences = false;
|
||||||
|
|
||||||
export let isLoadedAll;
|
export let isLoadedAll;
|
||||||
export let loadedTime;
|
export let loadedTime;
|
||||||
@@ -1425,6 +1426,7 @@
|
|||||||
}}
|
}}
|
||||||
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
||||||
grouping={display.getGrouping(col.uniqueName)}
|
grouping={display.getGrouping(col.uniqueName)}
|
||||||
|
{allowDefineVirtualReferences}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
TableGridDisplay,
|
TableGridDisplay,
|
||||||
} from 'dbgate-datalib';
|
} from 'dbgate-datalib';
|
||||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||||
import { findEngineDriver } from 'dbgate-tools';
|
import { extendDatabaseInfoFromApps, findEngineDriver } from 'dbgate-tools';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
|
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
|
||||||
@@ -16,9 +16,11 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
useConnectionInfo,
|
useConnectionInfo,
|
||||||
|
useConnectionList,
|
||||||
useDatabaseInfo,
|
useDatabaseInfo,
|
||||||
useDatabaseServerVersion,
|
useDatabaseServerVersion,
|
||||||
useServerVersion,
|
useServerVersion,
|
||||||
|
useUsedApps,
|
||||||
} from '../utility/metadataLoaders';
|
} from '../utility/metadataLoaders';
|
||||||
|
|
||||||
import DataGrid from './DataGrid.svelte';
|
import DataGrid from './DataGrid.svelte';
|
||||||
@@ -46,6 +48,9 @@
|
|||||||
$: connection = useConnectionInfo({ conid });
|
$: connection = useConnectionInfo({ conid });
|
||||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||||
|
$: apps = useUsedApps();
|
||||||
|
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
|
||||||
|
$: connections = useConnectionList();
|
||||||
|
|
||||||
// $: console.log('serverVersion', $serverVersion);
|
// $: console.log('serverVersion', $serverVersion);
|
||||||
|
|
||||||
@@ -64,10 +69,10 @@
|
|||||||
setConfig,
|
setConfig,
|
||||||
cache,
|
cache,
|
||||||
setCache,
|
setCache,
|
||||||
$dbinfo,
|
extendedDbInfo,
|
||||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||||
$serverVersion,
|
$serverVersion,
|
||||||
table => getDictionaryDescription(table, conid, database)
|
table => getDictionaryDescription(table, conid, database, $apps, $connections)
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -80,10 +85,10 @@
|
|||||||
setConfig,
|
setConfig,
|
||||||
cache,
|
cache,
|
||||||
setCache,
|
setCache,
|
||||||
$dbinfo,
|
extendedDbInfo,
|
||||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||||
$serverVersion,
|
$serverVersion,
|
||||||
table => getDictionaryDescription(table, conid, database)
|
table => getDictionaryDescription(table, conid, database, $apps, $connections)
|
||||||
)
|
)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
@@ -159,6 +164,7 @@
|
|||||||
macroCondition={macro => macro.type == 'transformValue'}
|
macroCondition={macro => macro.type == 'transformValue'}
|
||||||
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
|
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
|
||||||
multipleGridsOnTab={multipleGridsOnTab || !!reference}
|
multipleGridsOnTab={multipleGridsOnTab || !!reference}
|
||||||
|
allowDefineVirtualReferences
|
||||||
onReferenceClick={value => {
|
onReferenceClick={value => {
|
||||||
if (value && value.referenceId && reference && reference.referenceId == value.referenceId) {
|
if (value && value.referenceId && reference && reference.referenceId == value.referenceId) {
|
||||||
// reference not changed
|
// reference not changed
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
import DesignerTable from './DesignerTable.svelte';
|
import DesignerTable from './DesignerTable.svelte';
|
||||||
import { isConnectedByReference } from './designerTools';
|
import { isConnectedByReference } from './designerTools';
|
||||||
import uuidv1 from 'uuid/v1';
|
import uuidv1 from 'uuid/v1';
|
||||||
import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
|
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
|
||||||
import { currentThemeDefinition } from '../stores';
|
import { currentThemeDefinition } from '../stores';
|
||||||
|
import { extendDatabaseInfoFromApps } from 'dbgate-tools';
|
||||||
|
|
||||||
export let value;
|
export let value;
|
||||||
export let onChange;
|
export let onChange;
|
||||||
@@ -67,10 +68,12 @@
|
|||||||
const targetDragColumn$ = writable(null);
|
const targetDragColumn$ = writable(null);
|
||||||
|
|
||||||
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
||||||
|
$: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
|
||||||
|
|
||||||
$: tables = value?.tables as any[];
|
$: tables = value?.tables as any[];
|
||||||
$: references = value?.references as any[];
|
$: references = value?.references as any[];
|
||||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||||
|
$: apps = useUsedApps();
|
||||||
|
|
||||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||||
|
|
||||||
@@ -94,8 +97,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (dbInfo) {
|
if (dbInfoExtended) {
|
||||||
updateFromDbInfo($dbInfo);
|
updateFromDbInfo(dbInfoExtended as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,13 +107,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (dbInfo && value?.autoLayout) {
|
if (dbInfoExtended && value?.autoLayout) {
|
||||||
performAutoActions($dbInfo);
|
performAutoActions(dbInfoExtended);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFromDbInfo(db = 'auto') {
|
function updateFromDbInfo(db = 'auto') {
|
||||||
if (db == 'auto' && dbInfo) db = $dbInfo;
|
if (db == 'auto' && dbInfo) db = dbInfoExtended as any;
|
||||||
if (!settings?.updateFromDbInfo || !db) return;
|
if (!settings?.updateFromDbInfo || !db) return;
|
||||||
|
|
||||||
onChange(current => {
|
onChange(current => {
|
||||||
@@ -372,8 +375,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleAddTableReferences = async table => {
|
const handleAddTableReferences = async table => {
|
||||||
if (!dbInfo) return;
|
if (!dbInfoExtended) return;
|
||||||
const db = $dbInfo;
|
const db = dbInfoExtended;
|
||||||
if (!db) return;
|
if (!db) return;
|
||||||
callChange(current => {
|
callChange(current => {
|
||||||
return getTablesWithReferences(db, table, current);
|
return getTablesWithReferences(db, table, current);
|
||||||
@@ -692,13 +695,17 @@
|
|||||||
if (css) css += '\n';
|
if (css) css += '\n';
|
||||||
css += cssItem;
|
css += cssItem;
|
||||||
}
|
}
|
||||||
|
if ($currentThemeDefinition?.themeCss) {
|
||||||
|
if (css) css += '\n';
|
||||||
|
css += $currentThemeDefinition?.themeCss;
|
||||||
|
}
|
||||||
saveFileToDisk(async filePath => {
|
saveFileToDisk(async filePath => {
|
||||||
await apiCall('files/export-diagram', {
|
await apiCall('files/export-diagram', {
|
||||||
filePath,
|
filePath,
|
||||||
html: domCanvas.outerHTML,
|
html: domCanvas.outerHTML,
|
||||||
css,
|
css,
|
||||||
themeType: $currentThemeDefinition?.themeType,
|
themeType: $currentThemeDefinition?.themeType,
|
||||||
themeClassName: $currentThemeDefinition?.className,
|
themeClassName: $currentThemeDefinition?.themeClassName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
import { showModal } from '../modals/modalTools';
|
import { showModal } from '../modals/modalTools';
|
||||||
import { currentThemeDefinition } from '../stores';
|
import { currentThemeDefinition } from '../stores';
|
||||||
|
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||||
import contextMenu from '../utility/contextMenu';
|
import contextMenu from '../utility/contextMenu';
|
||||||
import moveDrag from '../utility/moveDrag';
|
import moveDrag from '../utility/moveDrag';
|
||||||
import ColumnLine from './ColumnLine.svelte';
|
import ColumnLine from './ColumnLine.svelte';
|
||||||
@@ -166,6 +167,15 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDefineVirtualForeignKey = table => {
|
||||||
|
showModal(VirtualForeignKeyEditorModal, {
|
||||||
|
schemaName: table.schemaName,
|
||||||
|
pureName: table.pureName,
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
function createMenu() {
|
function createMenu() {
|
||||||
return [
|
return [
|
||||||
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
||||||
@@ -185,6 +195,11 @@
|
|||||||
settings?.allowAddAllReferences &&
|
settings?.allowAddAllReferences &&
|
||||||
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
|
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
|
||||||
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
|
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
|
||||||
|
settings?.allowDefineVirtualReferences &&
|
||||||
|
!isMultipleTableSelection && {
|
||||||
|
text: 'Define virtual foreign key',
|
||||||
|
onClick: () => handleDefineVirtualForeignKey(table),
|
||||||
|
},
|
||||||
settings?.appendTableSystemMenu &&
|
settings?.appendTableSystemMenu &&
|
||||||
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
|
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
allowChangeColor: true,
|
allowChangeColor: true,
|
||||||
appendTableSystemMenu: true,
|
appendTableSystemMenu: true,
|
||||||
customizeStyle: true,
|
customizeStyle: true,
|
||||||
|
allowDefineVirtualReferences: true,
|
||||||
}}
|
}}
|
||||||
referenceComponent={DiagramDesignerReference}
|
referenceComponent={DiagramDesignerReference}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
allowChangeColor: false,
|
allowChangeColor: false,
|
||||||
appendTableSystemMenu: false,
|
appendTableSystemMenu: false,
|
||||||
customizeStyle: false,
|
customizeStyle: false,
|
||||||
|
allowDefineVirtualReferences: false,
|
||||||
}}
|
}}
|
||||||
referenceComponent={QueryDesignerReference}
|
referenceComponent={QueryDesignerReference}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -9,11 +9,13 @@
|
|||||||
export let name;
|
export let name;
|
||||||
export let options;
|
export let options;
|
||||||
export let isClearable = false;
|
export let isClearable = false;
|
||||||
|
export let selectFieldComponent = SelectField;
|
||||||
|
|
||||||
const { values, setFieldValue } = getFormContext();
|
const { values, setFieldValue } = getFormContext();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<SelectField
|
<svelte:component
|
||||||
|
this={selectFieldComponent}
|
||||||
{...$$restProps}
|
{...$$restProps}
|
||||||
value={$values && $values[name]}
|
value={$values && $values[name]}
|
||||||
options={_.compact(options)}
|
options={_.compact(options)}
|
||||||
|
|||||||
48
packages/web/src/forms/TargetApplicationSelect.svelte
Normal file
48
packages/web/src/forms/TargetApplicationSelect.svelte
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
import SelectField from '../forms/SelectField.svelte';
|
||||||
|
import { currentDatabase } from '../stores';
|
||||||
|
import { filterAppsForDatabase } from '../utility/appTools';
|
||||||
|
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
|
||||||
|
|
||||||
|
export let value = '#new';
|
||||||
|
export let disableInitialize = false;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
$: appFolders = useAppFolders();
|
||||||
|
$: usedApps = useUsedApps();
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if (!disableInitialize && value == '#new' && $currentDatabase) {
|
||||||
|
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
|
||||||
|
const common = _.intersection(
|
||||||
|
($appFolders || []).map(x => x.name),
|
||||||
|
filtered.map(x => x.name)
|
||||||
|
);
|
||||||
|
if (common.length > 0) {
|
||||||
|
value = common[0] as string;
|
||||||
|
dispatch('change', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SelectField
|
||||||
|
isNative
|
||||||
|
{...$$restProps}
|
||||||
|
{value}
|
||||||
|
on:change={e => {
|
||||||
|
value = e.detail;
|
||||||
|
dispatch('change', value);
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ label: '(New application linked to current DB)', value: '#new' },
|
||||||
|
...($appFolders || []).map(app => ({
|
||||||
|
label: app.name,
|
||||||
|
value: app.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
/>
|
||||||
@@ -26,6 +26,7 @@
|
|||||||
'icon version': 'mdi mdi-ticket-confirmation',
|
'icon version': 'mdi mdi-ticket-confirmation',
|
||||||
'icon pin': 'mdi mdi-pin',
|
'icon pin': 'mdi mdi-pin',
|
||||||
'icon arrange': 'mdi mdi-arrange-send-to-back',
|
'icon arrange': 'mdi mdi-arrange-send-to-back',
|
||||||
|
'icon app': 'mdi mdi-layers-triple',
|
||||||
|
|
||||||
'icon columns': 'mdi mdi-view-column',
|
'icon columns': 'mdi mdi-view-column',
|
||||||
'icon columns-outline': 'mdi mdi-view-column-outline',
|
'icon columns-outline': 'mdi mdi-view-column-outline',
|
||||||
@@ -126,6 +127,9 @@
|
|||||||
'img diagram': 'mdi mdi-graph color-icon-blue',
|
'img diagram': 'mdi mdi-graph color-icon-blue',
|
||||||
'img yaml': 'mdi mdi-code-brackets color-icon-red',
|
'img yaml': 'mdi mdi-code-brackets color-icon-red',
|
||||||
'img compare': 'mdi mdi-compare color-icon-red',
|
'img compare': 'mdi mdi-compare color-icon-red',
|
||||||
|
'img app': 'mdi mdi-layers-triple color-icon-magenta',
|
||||||
|
'img app-command': 'mdi mdi-flash color-icon-green',
|
||||||
|
'img app-query': 'mdi mdi-view-comfy color-icon-magenta',
|
||||||
|
|
||||||
'img add': 'mdi mdi-plus-circle color-icon-green',
|
'img add': 'mdi mdi-plus-circle color-icon-green',
|
||||||
'img minus': 'mdi mdi-minus-circle color-icon-red',
|
'img minus': 'mdi mdi-minus-circle color-icon-red',
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FormProvider from '../forms/FormProvider.svelte';
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import _ from 'lodash';
|
||||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||||
import ModalBase from './ModalBase.svelte';
|
import ModalBase from './ModalBase.svelte';
|
||||||
import { closeCurrentModal } from './modalTools';
|
import { closeCurrentModal } from './modalTools';
|
||||||
import { useTableInfo } from '../utility/metadataLoaders';
|
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||||
import TableControl from '../elements/TableControl.svelte';
|
import TableControl from '../elements/TableControl.svelte';
|
||||||
import TextField from '../forms/TextField.svelte';
|
import TextField from '../forms/TextField.svelte';
|
||||||
import FormTextField from '../forms/FormTextField.svelte';
|
import FormTextField from '../forms/FormTextField.svelte';
|
||||||
@@ -19,6 +20,10 @@
|
|||||||
} from '../utility/dictionaryDescriptionTools';
|
} from '../utility/dictionaryDescriptionTools';
|
||||||
import { includes } from 'lodash';
|
import { includes } from 'lodash';
|
||||||
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
|
||||||
|
import FormSelectField from '../forms/FormSelectField.svelte';
|
||||||
|
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
|
||||||
|
import { currentDatabase } from '../stores';
|
||||||
|
import { filterAppsForDatabase } from '../utility/appTools';
|
||||||
|
|
||||||
export let conid;
|
export let conid;
|
||||||
export let database;
|
export let database;
|
||||||
@@ -28,18 +33,41 @@
|
|||||||
|
|
||||||
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
|
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
|
||||||
|
|
||||||
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, true);
|
$: apps = useUsedApps();
|
||||||
|
$: appFolders = useAppFolders();
|
||||||
|
$: connections = useConnectionList();
|
||||||
|
|
||||||
const values = writable({});
|
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
|
||||||
|
|
||||||
|
const values = writable({ targetApplication: '#new' } as any);
|
||||||
|
|
||||||
function initValues(descriptionInfo) {
|
function initValues(descriptionInfo) {
|
||||||
$values = {
|
$values = {
|
||||||
|
targetApplication: $values.targetApplication,
|
||||||
columns: descriptionInfo.expression,
|
columns: descriptionInfo.expression,
|
||||||
delimiter: descriptionInfo.delimiter,
|
delimiter: descriptionInfo.delimiter,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
$: if (descriptionInfo) initValues(descriptionInfo);
|
$: {
|
||||||
|
if (descriptionInfo) initValues(descriptionInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($values.targetApplication == '#new' && $currentDatabase) {
|
||||||
|
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []);
|
||||||
|
const common = _.intersection(
|
||||||
|
($appFolders || []).map(x => x.name),
|
||||||
|
filtered.map(x => x.name)
|
||||||
|
);
|
||||||
|
if (common.length > 0) {
|
||||||
|
$values = {
|
||||||
|
...$values,
|
||||||
|
targetApplication: common[0],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FormProviderCore {values}>
|
<FormProviderCore {values}>
|
||||||
@@ -75,7 +103,14 @@
|
|||||||
|
|
||||||
<FormTextField name="delimiter" label="Delimiter" />
|
<FormTextField name="delimiter" label="Delimiter" />
|
||||||
|
|
||||||
<FormCheckboxField name="useForAllDatabases" label="Use for all databases" />
|
<FormSelectField
|
||||||
|
label="Target application"
|
||||||
|
name="targetApplication"
|
||||||
|
disableInitialize
|
||||||
|
selectFieldComponent={TargetApplicationSelect}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
|
||||||
|
|
||||||
<svelte:fragment slot="footer">
|
<svelte:fragment slot="footer">
|
||||||
<FormSubmit
|
<FormSubmit
|
||||||
@@ -89,7 +124,7 @@
|
|||||||
database,
|
database,
|
||||||
$values.columns,
|
$values.columns,
|
||||||
$values.delimiter,
|
$values.delimiter,
|
||||||
$values.useForAllDatabases
|
$values.targetApplication
|
||||||
);
|
);
|
||||||
onConfirm();
|
onConfirm();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
import { closeCurrentModal, showModal } from './modalTools';
|
import { closeCurrentModal, showModal } from './modalTools';
|
||||||
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
|
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
|
||||||
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
|
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
|
||||||
import { getTableInfo } from '../utility/metadataLoaders';
|
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders';
|
||||||
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
|
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { dumpSqlSelect } from 'dbgate-sqltree';
|
import { dumpSqlSelect } from 'dbgate-sqltree';
|
||||||
@@ -33,6 +33,9 @@
|
|||||||
|
|
||||||
let checkedKeys = [];
|
let checkedKeys = [];
|
||||||
|
|
||||||
|
$: apps = useUsedApps();
|
||||||
|
$: connections = useConnectionList();
|
||||||
|
|
||||||
function defineDescription() {
|
function defineDescription() {
|
||||||
showModal(DefineDictionaryDescriptionModal, {
|
showModal(DefineDictionaryDescriptionModal, {
|
||||||
conid,
|
conid,
|
||||||
@@ -45,7 +48,7 @@
|
|||||||
|
|
||||||
async function reload() {
|
async function reload() {
|
||||||
tableInfo = await getTableInfo({ conid, database, schemaName, pureName });
|
tableInfo = await getTableInfo({ conid, database, schemaName, pureName });
|
||||||
description = getDictionaryDescription(tableInfo, conid, database);
|
description = getDictionaryDescription(tableInfo, conid, database, $apps, $connections);
|
||||||
|
|
||||||
if (!tableInfo || !description) return;
|
if (!tableInfo || !description) return;
|
||||||
if (tableInfo?.primaryKey?.columns?.length != 1) return;
|
if (tableInfo?.primaryKey?.columns?.length != 1) return;
|
||||||
@@ -112,6 +115,8 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
search;
|
search;
|
||||||
|
$apps;
|
||||||
|
$connections;
|
||||||
reload();
|
reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script context="module">
|
<script context="module">
|
||||||
export const className = 'theme-dark';
|
export const themeClassName = 'theme-dark';
|
||||||
export const themeName = 'Dark';
|
export const themeName = 'Dark';
|
||||||
export const themeType = 'dark';
|
export const themeType = 'dark';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script context="module">
|
<script context="module">
|
||||||
export const className = 'theme-light';
|
export const themeClassName = 'theme-light';
|
||||||
export const themeName = 'Light';
|
export const themeName = 'Light';
|
||||||
export const themeType = 'light';
|
export const themeType = 'light';
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ export const openedModals = writable([]);
|
|||||||
export const openedSnackbars = writable([]);
|
export const openedSnackbars = writable([]);
|
||||||
export const nullStore = readable(null, () => {});
|
export const nullStore = readable(null, () => {});
|
||||||
export const currentArchive = writableWithStorage('default', 'currentArchive');
|
export const currentArchive = writableWithStorage('default', 'currentArchive');
|
||||||
|
export const currentApplication = writableWithStorage(null, 'currentApplication');
|
||||||
export const isFileDragActive = writable(false);
|
export const isFileDragActive = writable(false);
|
||||||
export const selectedCellsCallback = writable(null);
|
export const selectedCellsCallback = writable(null);
|
||||||
export const loadingPluginStore = writable({
|
export const loadingPluginStore = writable({
|
||||||
@@ -72,7 +73,7 @@ export const loadingPluginStore = writable({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
|
export const currentThemeDefinition = derived([currentTheme, extensions], ([$currentTheme, $extensions]) =>
|
||||||
$extensions.themes.find(x => x.className == $currentTheme)
|
$extensions.themes.find(x => x.themeClassName == $currentTheme)
|
||||||
);
|
);
|
||||||
|
|
||||||
subscribeCssVariable(selectedWidget, x => (x ? 1 : 0), '--dim-visible-left-panel');
|
subscribeCssVariable(selectedWidget, x => (x ? 1 : 0), '--dim-visible-left-panel');
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
||||||
isNative
|
isNative
|
||||||
notSelected
|
notSelected
|
||||||
options={(dbInfo?.tables || []).map(tbl => ({
|
options={_.sortBy(dbInfo?.tables || [], ['schemaName', 'pureName']).map(tbl => ({
|
||||||
label: fullNameToLabel(tbl),
|
label: fullNameToLabel(tbl),
|
||||||
value: fullNameToString(tbl),
|
value: fullNameToString(tbl),
|
||||||
}))}
|
}))}
|
||||||
|
|||||||
190
packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte
Normal file
190
packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||||
|
|
||||||
|
import FormProvider from '../forms/FormProvider.svelte';
|
||||||
|
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||||
|
import ModalBase from '../modals/ModalBase.svelte';
|
||||||
|
import { closeCurrentModal } from '../modals/modalTools';
|
||||||
|
import { fullNameFromString, fullNameToLabel, fullNameToString } from 'dbgate-tools';
|
||||||
|
import SelectField from '../forms/SelectField.svelte';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { useDatabaseInfo, useTableInfo } from '../utility/metadataLoaders';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { saveDbToApp } from '../utility/appTools';
|
||||||
|
|
||||||
|
export let conid;
|
||||||
|
export let database;
|
||||||
|
export let schemaName;
|
||||||
|
export let pureName;
|
||||||
|
export let columnName;
|
||||||
|
|
||||||
|
let dstApp;
|
||||||
|
|
||||||
|
const dbInfo = useDatabaseInfo({ conid, database });
|
||||||
|
const tableInfo = useTableInfo({ conid, database, schemaName, pureName });
|
||||||
|
|
||||||
|
let columns = [];
|
||||||
|
let refTableName = null;
|
||||||
|
let refSchemaName = null;
|
||||||
|
|
||||||
|
$: refTableInfo = $dbInfo?.tables?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
|
||||||
|
// $dbInfo?.views?.find(x => x.pureName == refTableName && x.schemaName == refSchemaName);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
if (columnName) {
|
||||||
|
columns = [
|
||||||
|
...columns,
|
||||||
|
{
|
||||||
|
columnName,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<FormProvider>
|
||||||
|
<ModalBase {...$$restProps}>
|
||||||
|
<svelte:fragment slot="header">Virtual foreign key</svelte:fragment>
|
||||||
|
|
||||||
|
<div class="largeFormMarker">
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Referenced table</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<SelectField
|
||||||
|
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={[
|
||||||
|
..._.sortBy($dbInfo?.tables || [], ['schemaName', 'pureName']),
|
||||||
|
// ..._.sortBy($dbInfo?.views || [], ['schemaName', 'pureName']),
|
||||||
|
].map(tbl => ({
|
||||||
|
label: fullNameToLabel(tbl),
|
||||||
|
value: fullNameToString(tbl),
|
||||||
|
}))}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
const name = fullNameFromString(e.detail);
|
||||||
|
refTableName = name.pureName;
|
||||||
|
refSchemaName = name.schemaName;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 mr-1">
|
||||||
|
Base column - {$tableInfo.pureName}
|
||||||
|
</div>
|
||||||
|
<div class="col-5 ml-1">
|
||||||
|
Ref column - {refTableName || '(table not set)'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each columns as column, index}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-5 mr-1">
|
||||||
|
{#key column.columnName}
|
||||||
|
<SelectField
|
||||||
|
value={column.columnName}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={$tableInfo.columns.map(col => ({
|
||||||
|
label: col.columnName,
|
||||||
|
value: col.columnName,
|
||||||
|
}))}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
columns = columns.map((col, i) => (i == index ? { ...col, columnName: e.detail } : col));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
<div class="col-5 ml-1">
|
||||||
|
{#key column.refColumnName}
|
||||||
|
<SelectField
|
||||||
|
value={column.refColumnName}
|
||||||
|
isNative
|
||||||
|
notSelected
|
||||||
|
options={(refTableInfo?.columns || []).map(col => ({
|
||||||
|
label: col.columnName,
|
||||||
|
value: col.columnName,
|
||||||
|
}))}
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail) {
|
||||||
|
columns = columns.map((col, i) => (i == index ? { ...col, refColumnName: e.detail } : col));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
|
</div>
|
||||||
|
<div class="col-2 button">
|
||||||
|
<FormStyledButton
|
||||||
|
value="Delete"
|
||||||
|
on:click={e => {
|
||||||
|
const x = [...columns];
|
||||||
|
x.splice(index, 1);
|
||||||
|
columns = x;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<FormStyledButton
|
||||||
|
type="button"
|
||||||
|
value="Add column"
|
||||||
|
on:click={() => {
|
||||||
|
columns = [...columns, {}];
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="label col-3">Target application</div>
|
||||||
|
<div class="col-9">
|
||||||
|
<TargetApplicationSelect bind:value={dstApp} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<svelte:fragment slot="footer">
|
||||||
|
<FormSubmit
|
||||||
|
value={'Save'}
|
||||||
|
on:click={async () => {
|
||||||
|
const appFolder = await saveDbToApp(conid, database, dstApp);
|
||||||
|
await apiCall('apps/save-virtual-reference', {
|
||||||
|
appFolder,
|
||||||
|
schemaName,
|
||||||
|
pureName,
|
||||||
|
refSchemaName,
|
||||||
|
refTableName,
|
||||||
|
columns,
|
||||||
|
});
|
||||||
|
closeCurrentModal();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||||
|
</svelte:fragment>
|
||||||
|
</ModalBase>
|
||||||
|
</FormProvider>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.row {
|
||||||
|
margin: var(--dim-large-form-margin);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row .label {
|
||||||
|
white-space: nowrap;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
align-self: center;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
packages/web/src/tabs/JsonEditorTab.svelte
Normal file
83
packages/web/src/tabs/JsonEditorTab.svelte
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const getCurrentEditor = () => getActiveComponent('JsonEditorTab');
|
||||||
|
|
||||||
|
registerFileCommands({
|
||||||
|
idPrefix: 'json',
|
||||||
|
category: 'Json',
|
||||||
|
getCurrentEditor,
|
||||||
|
folder: 'yaml',
|
||||||
|
format: 'text',
|
||||||
|
fileExtension: 'json',
|
||||||
|
|
||||||
|
toggleComment: true,
|
||||||
|
findReplace: true,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { getContext } from 'svelte';
|
||||||
|
import { registerFileCommands } from '../commands/stdCommands';
|
||||||
|
|
||||||
|
import AceEditor from '../query/AceEditor.svelte';
|
||||||
|
import useEditorData from '../query/useEditorData';
|
||||||
|
import invalidateCommands from '../commands/invalidateCommands';
|
||||||
|
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||||
|
|
||||||
|
export let tabid;
|
||||||
|
|
||||||
|
const tabVisible: any = getContext('tabVisible');
|
||||||
|
|
||||||
|
export const activator = createActivator('JsonEditorTab', false);
|
||||||
|
|
||||||
|
let domEditor;
|
||||||
|
|
||||||
|
$: if ($tabVisible && domEditor) {
|
||||||
|
domEditor?.getEditor()?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getData() {
|
||||||
|
return $editorState.value || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleComment() {
|
||||||
|
domEditor.getEditor().execCommand('togglecomment');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function find() {
|
||||||
|
domEditor.getEditor().execCommand('find');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function replace() {
|
||||||
|
domEditor.getEditor().execCommand('replace');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTabId() {
|
||||||
|
return tabid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { editorState, editorValue, setEditorData, saveToStorage } = useEditorData({ tabid });
|
||||||
|
|
||||||
|
function createMenu() {
|
||||||
|
return [
|
||||||
|
{ command: 'json.toggleComment' },
|
||||||
|
{ divider: true },
|
||||||
|
{ command: 'json.save' },
|
||||||
|
{ command: 'json.saveAs' },
|
||||||
|
{ divider: true },
|
||||||
|
{ command: 'json.find' },
|
||||||
|
{ command: 'json.replace' },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AceEditor
|
||||||
|
value={$editorState.value || ''}
|
||||||
|
menu={createMenu()}
|
||||||
|
on:input={e => setEditorData(e.detail)}
|
||||||
|
on:focus={() => {
|
||||||
|
activator.activate();
|
||||||
|
invalidateCommands();
|
||||||
|
}}
|
||||||
|
bind:this={domEditor}
|
||||||
|
mode="json"
|
||||||
|
/>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
|
|
||||||
const tabVisible: any = getContext('tabVisible');
|
const tabVisible: any = getContext('tabVisible');
|
||||||
|
|
||||||
export const activator = createActivator('MarkdownEditorTab', false);
|
export const activator = createActivator('YamlEditorTab', false);
|
||||||
|
|
||||||
let domEditor;
|
let domEditor;
|
||||||
|
|
||||||
@@ -63,8 +63,6 @@
|
|||||||
|
|
||||||
function createMenu() {
|
function createMenu() {
|
||||||
return [
|
return [
|
||||||
{ command: 'yaml.preview' },
|
|
||||||
{ divider: true },
|
|
||||||
{ command: 'yaml.toggleComment' },
|
{ command: 'yaml.toggleComment' },
|
||||||
{ divider: true },
|
{ divider: true },
|
||||||
{ command: 'yaml.save' },
|
{ command: 'yaml.save' },
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import * as FavoriteEditorTab from './FavoriteEditorTab.svelte';
|
|||||||
import * as QueryDesignTab from './QueryDesignTab.svelte';
|
import * as QueryDesignTab from './QueryDesignTab.svelte';
|
||||||
import * as CommandListTab from './CommandListTab.svelte';
|
import * as CommandListTab from './CommandListTab.svelte';
|
||||||
import * as YamlEditorTab from './YamlEditorTab.svelte';
|
import * as YamlEditorTab from './YamlEditorTab.svelte';
|
||||||
|
import * as JsonEditorTab from './JsonEditorTab.svelte';
|
||||||
import * as CompareModelTab from './CompareModelTab.svelte';
|
import * as CompareModelTab from './CompareModelTab.svelte';
|
||||||
import * as JsonTab from './JsonTab.svelte';
|
import * as JsonTab from './JsonTab.svelte';
|
||||||
import * as ChangelogTab from './ChangelogTab.svelte';
|
import * as ChangelogTab from './ChangelogTab.svelte';
|
||||||
@@ -38,6 +39,7 @@ export default {
|
|||||||
QueryDesignTab,
|
QueryDesignTab,
|
||||||
CommandListTab,
|
CommandListTab,
|
||||||
YamlEditorTab,
|
YamlEditorTab,
|
||||||
|
JsonEditorTab,
|
||||||
CompareModelTab,
|
CompareModelTab,
|
||||||
JsonTab,
|
JsonTab,
|
||||||
ChangelogTab,
|
ChangelogTab,
|
||||||
|
|||||||
33
packages/web/src/utility/appTools.ts
Normal file
33
packages/web/src/utility/appTools.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { ApplicationDefinition, StoredConnection } from 'dbgate-types';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
|
||||||
|
export async function saveDbToApp(conid: string, database: string, app: string) {
|
||||||
|
if (app == '#new') {
|
||||||
|
const folder = await apiCall('apps/create-folder', { folder: database });
|
||||||
|
|
||||||
|
await apiCall('connections/update-database', {
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
values: {
|
||||||
|
[`useApp:${folder}`]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
await apiCall('connections/update-database', {
|
||||||
|
conid,
|
||||||
|
database,
|
||||||
|
values: {
|
||||||
|
[`useApp:${app}`]: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return app;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function filterAppsForDatabase(connection, database: string, $apps): ApplicationDefinition[] {
|
||||||
|
const db = (connection?.databases || []).find(x => x.name == database);
|
||||||
|
return $apps.filter(app => db && db[`useApp:${app.name}`]);
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { DictionaryDescription } from 'dbgate-datalib';
|
import { DictionaryDescription } from 'dbgate-datalib';
|
||||||
import { TableInfo } from 'dbgate-types';
|
import { ApplicationDefinition, TableInfo } from 'dbgate-types';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { getLocalStorage, setLocalStorage, removeLocalStorage } from './storageCache';
|
import { apiCall } from './api';
|
||||||
|
import { filterAppsForDatabase, saveDbToApp } from './appTools';
|
||||||
|
|
||||||
function checkDescriptionColumns(columns: string[], table: TableInfo) {
|
function checkDescriptionColumns(columns: string[], table: TableInfo) {
|
||||||
if (!columns?.length) return false;
|
if (!columns?.length) return false;
|
||||||
@@ -14,17 +15,20 @@ export function getDictionaryDescription(
|
|||||||
table: TableInfo,
|
table: TableInfo,
|
||||||
conid: string,
|
conid: string,
|
||||||
database: string,
|
database: string,
|
||||||
|
apps: ApplicationDefinition[],
|
||||||
|
connections,
|
||||||
skipCheckSaved: boolean = false
|
skipCheckSaved: boolean = false
|
||||||
): DictionaryDescription {
|
): DictionaryDescription {
|
||||||
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
|
const conn = connections.find(x => x._id == conid);
|
||||||
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
|
const dbApps = filterAppsForDatabase(conn, database, apps);
|
||||||
|
|
||||||
const cachedSpecific = getLocalStorage(keySpecific);
|
const cached = _.flatten(dbApps.map(x => x.dictionaryDescriptions || [])).find(
|
||||||
const cachedCommon = getLocalStorage(keyCommon);
|
x => x.pureName == table.pureName && x.schemaName == table.schemaName
|
||||||
|
);
|
||||||
|
|
||||||
if (cachedSpecific && (skipCheckSaved || checkDescriptionColumns(cachedSpecific.columns, table)))
|
if (cached && (skipCheckSaved || checkDescriptionColumns(cached.columns, table))) {
|
||||||
return cachedSpecific;
|
return cached;
|
||||||
if (cachedCommon && (skipCheckSaved || checkDescriptionColumns(cachedCommon.columns, table))) return cachedCommon;
|
}
|
||||||
|
|
||||||
const descColumn = table.columns.find(x => x?.dataType?.toLowerCase()?.includes('char'));
|
const descColumn = table.columns.find(x => x?.dataType?.toLowerCase()?.includes('char'));
|
||||||
if (descColumn) {
|
if (descColumn) {
|
||||||
@@ -57,29 +61,22 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
|
|||||||
return parsed.join(',');
|
return parsed.join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveDictionaryDescription(
|
export async function saveDictionaryDescription(
|
||||||
table: TableInfo,
|
table: TableInfo,
|
||||||
conid: string,
|
conid: string,
|
||||||
database: string,
|
database: string,
|
||||||
expression: string,
|
expression: string,
|
||||||
delimiter: string,
|
delimiter: string,
|
||||||
useForAllDatabases: boolean
|
targetApplication: string
|
||||||
) {
|
) {
|
||||||
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
|
const appFolder = await saveDbToApp(conid, database, targetApplication);
|
||||||
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
|
|
||||||
|
|
||||||
removeLocalStorage(keySpecific);
|
await apiCall('apps/save-dictionary-description', {
|
||||||
if (useForAllDatabases) removeLocalStorage(keyCommon);
|
appFolder,
|
||||||
|
schemaName: table.schemaName,
|
||||||
const description = {
|
pureName: table.pureName,
|
||||||
columns: parseDelimitedColumnList(expression),
|
columns: parseDelimitedColumnList(expression),
|
||||||
expression,
|
expression,
|
||||||
delimiter,
|
delimiter,
|
||||||
};
|
});
|
||||||
|
|
||||||
if (useForAllDatabases) {
|
|
||||||
setLocalStorage(keyCommon, description);
|
|
||||||
} else {
|
|
||||||
setLocalStorage(keySpecific, description);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,30 @@ const archiveFilesLoader = ({ folder }) => ({
|
|||||||
reloadTrigger: `archive-files-changed-${folder}`,
|
reloadTrigger: `archive-files-changed-${folder}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const appFoldersLoader = () => ({
|
||||||
|
url: 'apps/folders',
|
||||||
|
params: {},
|
||||||
|
reloadTrigger: `app-folders-changed`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const appFilesLoader = ({ folder }) => ({
|
||||||
|
url: 'apps/files',
|
||||||
|
params: { folder },
|
||||||
|
reloadTrigger: `app-files-changed-${folder}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const dbAppsLoader = ({ conid, database }) => ({
|
||||||
|
// url: 'apps/get-apps-for-db',
|
||||||
|
// params: { conid, database },
|
||||||
|
// reloadTrigger: `db-apps-changed-${conid}-${database}`,
|
||||||
|
// });
|
||||||
|
|
||||||
|
const usedAppsLoader = ({ conid, database }) => ({
|
||||||
|
url: 'apps/get-used-apps',
|
||||||
|
params: { },
|
||||||
|
reloadTrigger: `used-apps-changed`,
|
||||||
|
});
|
||||||
|
|
||||||
const serverStatusLoader = () => ({
|
const serverStatusLoader = () => ({
|
||||||
url: 'server-connections/server-status',
|
url: 'server-connections/server-status',
|
||||||
params: {},
|
params: {},
|
||||||
@@ -401,6 +425,36 @@ export function useArchiveFolders(args = {}) {
|
|||||||
return useCore(archiveFoldersLoader, args);
|
return useCore(archiveFoldersLoader, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAppFiles(args) {
|
||||||
|
return getCore(appFilesLoader, args);
|
||||||
|
}
|
||||||
|
export function useAppFiles(args) {
|
||||||
|
return useCore(appFilesLoader, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAppFolders(args = {}) {
|
||||||
|
return getCore(appFoldersLoader, args);
|
||||||
|
}
|
||||||
|
export function useAppFolders(args = {}) {
|
||||||
|
return useCore(appFoldersLoader, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export function getUsedApps(args = {}) {
|
||||||
|
return getCore(usedAppsLoader, args);
|
||||||
|
}
|
||||||
|
export function useUsedApps(args = {}) {
|
||||||
|
return useCore(usedAppsLoader, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function getDbApps(args = {}) {
|
||||||
|
// return getCore(dbAppsLoader, args);
|
||||||
|
// }
|
||||||
|
// export function useDbApps(args = {}) {
|
||||||
|
// return useCore(dbAppsLoader, args);
|
||||||
|
// }
|
||||||
|
|
||||||
export function getInstalledPlugins(args = {}) {
|
export function getInstalledPlugins(args = {}) {
|
||||||
return getCore(installedPluginsLoader, args) || [];
|
return getCore(installedPluginsLoader, args) || [];
|
||||||
}
|
}
|
||||||
|
|||||||
101
packages/web/src/widgets/AppFilesList.svelte
Normal file
101
packages/web/src/widgets/AppFilesList.svelte
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<script lang="ts" context="module">
|
||||||
|
const APP_LABELS = {
|
||||||
|
'command.sql': 'SQL commands',
|
||||||
|
'query.sql': 'SQL queries',
|
||||||
|
};
|
||||||
|
|
||||||
|
const COMMAND_TEMPLATE = `-- Write SQL command here
|
||||||
|
-- After save, you can execute it from database context menu, for all databases, which use this application
|
||||||
|
`;
|
||||||
|
|
||||||
|
const QUERY_TEMPLATE = `-- Write SQL query here
|
||||||
|
-- After save, you can view it in tables list, for all databases, which use this application
|
||||||
|
`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { createFreeTableModel } from 'dbgate-datalib';
|
||||||
|
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
|
import * as appFileAppObject from '../appobj/AppFileAppObject.svelte';
|
||||||
|
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
|
||||||
|
import DropDownButton from '../elements/DropDownButton.svelte';
|
||||||
|
|
||||||
|
import InlineButton from '../elements/InlineButton.svelte';
|
||||||
|
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||||
|
import { showModal } from '../modals/modalTools';
|
||||||
|
import newQuery from '../query/newQuery';
|
||||||
|
import { currentApplication } from '../stores';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { markArchiveFileAsDataSheet } from '../utility/archiveTools';
|
||||||
|
import { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
|
||||||
|
import openNewTab from '../utility/openNewTab';
|
||||||
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
|
||||||
|
let filter = '';
|
||||||
|
|
||||||
|
$: folder = $currentApplication;
|
||||||
|
$: files = useAppFiles({ folder });
|
||||||
|
|
||||||
|
const handleRefreshFiles = () => {
|
||||||
|
apiCall('apps/refresh-files', { folder });
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleNewSqlFile(fileType, header, initialData) {
|
||||||
|
showModal(InputTextModal, {
|
||||||
|
value: '',
|
||||||
|
label: 'New file name',
|
||||||
|
header,
|
||||||
|
onConfirm: async file => {
|
||||||
|
newQuery({
|
||||||
|
title: file,
|
||||||
|
initialData,
|
||||||
|
// @ts-ignore
|
||||||
|
savedFile: file + '.' + fileType,
|
||||||
|
savedFolder: 'app:' + $currentApplication,
|
||||||
|
savedFormat: 'text',
|
||||||
|
appFolder: $currentApplication,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createAddMenu() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'New SQL command',
|
||||||
|
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
|
||||||
|
},
|
||||||
|
// { text: 'New query view', onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE) },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search application files" bind:value={filter} />
|
||||||
|
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
<DropDownButton icon="icon plus-thick" menu={createAddMenu} />
|
||||||
|
<InlineButton on:click={handleRefreshFiles} title="Refresh files of selected application">
|
||||||
|
<FontIcon icon="icon refresh" />
|
||||||
|
</InlineButton>
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<AppObjectList
|
||||||
|
list={($files || []).map(file => ({
|
||||||
|
fileName: file.name,
|
||||||
|
folderName: folder,
|
||||||
|
fileType: file.type,
|
||||||
|
fileLabel: file.label,
|
||||||
|
}))}
|
||||||
|
groupFunc={data => APP_LABELS[data.fileType] || 'App config'}
|
||||||
|
module={appFileAppObject}
|
||||||
|
{filter}
|
||||||
|
/>
|
||||||
|
</WidgetsInnerContainer>
|
||||||
39
packages/web/src/widgets/AppFolderList.svelte
Normal file
39
packages/web/src/widgets/AppFolderList.svelte
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
|
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||||
|
import * as appFolderAppObject from '../appobj/AppFolderAppObject.svelte';
|
||||||
|
import runCommand from '../commands/runCommand';
|
||||||
|
import CloseSearchButton from '../elements/CloseSearchButton.svelte';
|
||||||
|
|
||||||
|
import InlineButton from '../elements/InlineButton.svelte';
|
||||||
|
|
||||||
|
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||||
|
import SearchInput from '../elements/SearchInput.svelte';
|
||||||
|
import FontIcon from '../icons/FontIcon.svelte';
|
||||||
|
import { apiCall } from '../utility/api';
|
||||||
|
import { useAppFolders } from '../utility/metadataLoaders';
|
||||||
|
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||||
|
|
||||||
|
let filter = '';
|
||||||
|
|
||||||
|
$: folders = useAppFolders();
|
||||||
|
|
||||||
|
const handleRefreshFolders = () => {
|
||||||
|
apiCall('apps/refresh-folders');
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SearchBoxWrapper>
|
||||||
|
<SearchInput placeholder="Search applications" bind:value={filter} />
|
||||||
|
<CloseSearchButton bind:filter />
|
||||||
|
<InlineButton on:click={() => runCommand('new.application')} title="Create new application">
|
||||||
|
<FontIcon icon="icon plus-thick" />
|
||||||
|
</InlineButton>
|
||||||
|
<InlineButton on:click={handleRefreshFolders} title="Refresh application list">
|
||||||
|
<FontIcon icon="icon refresh" />
|
||||||
|
</InlineButton>
|
||||||
|
</SearchBoxWrapper>
|
||||||
|
<WidgetsInnerContainer>
|
||||||
|
<AppObjectList list={_.sortBy($folders, 'name')} module={appFolderAppObject} {filter} />
|
||||||
|
</WidgetsInnerContainer>
|
||||||
19
packages/web/src/widgets/AppWidget.svelte
Normal file
19
packages/web/src/widgets/AppWidget.svelte
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import AppFilesList from './AppFilesList.svelte';
|
||||||
|
|
||||||
|
import WidgetColumnBar from './WidgetColumnBar.svelte';
|
||||||
|
import WidgetColumnBarItem from './WidgetColumnBarItem.svelte';
|
||||||
|
|
||||||
|
import { useFavorites } from '../utility/metadataLoaders';
|
||||||
|
import AppFolderList from './AppFolderList.svelte';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<WidgetColumnBar>
|
||||||
|
<WidgetColumnBarItem title="Applications" name="apps" height="30%" storageName="appsWidget">
|
||||||
|
<AppFolderList />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
|
||||||
|
<WidgetColumnBarItem title="Application files" name="files" storageName="appFilesWidget">
|
||||||
|
<AppFilesList />
|
||||||
|
</WidgetColumnBarItem>
|
||||||
|
</WidgetColumnBar>
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
import PluginsWidget from './PluginsWidget.svelte';
|
import PluginsWidget from './PluginsWidget.svelte';
|
||||||
import CellDataWidget from './CellDataWidget.svelte';
|
import CellDataWidget from './CellDataWidget.svelte';
|
||||||
import HistoryWidget from './HistoryWidget.svelte';
|
import HistoryWidget from './HistoryWidget.svelte';
|
||||||
|
import AppWidget from './AppWidget.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DatabaseWidget hidden={$selectedWidget != 'database'} />
|
<DatabaseWidget hidden={$selectedWidget != 'database'} />
|
||||||
@@ -25,3 +26,6 @@
|
|||||||
{#if $selectedWidget == 'cell-data'}
|
{#if $selectedWidget == 'cell-data'}
|
||||||
<CellDataWidget />
|
<CellDataWidget />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if $selectedWidget == 'app'}
|
||||||
|
<AppWidget />
|
||||||
|
{/if}
|
||||||
|
|||||||
@@ -40,6 +40,11 @@
|
|||||||
name: 'cell-data',
|
name: 'cell-data',
|
||||||
title: 'Selected cell data detail view',
|
title: 'Selected cell data detail view',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: 'icon app',
|
||||||
|
name: 'app',
|
||||||
|
title: 'Application layers',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// icon: 'icon settings',
|
// icon: 'icon settings',
|
||||||
// name: 'settings',
|
// name: 'settings',
|
||||||
|
|||||||
44
yarn.lock
44
yarn.lock
@@ -1844,11 +1844,6 @@ async-lock@^1.2.6:
|
|||||||
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c"
|
resolved "https://registry.yarnpkg.com/async-lock/-/async-lock-1.2.8.tgz#7b02bdfa2de603c0713acecd11184cf97bbc7c4c"
|
||||||
integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==
|
integrity sha512-G+26B2jc0Gw0EG/WN2M6IczuGepBsfR1+DtqLnyFSH4p2C668qkOCtEkGNVEaaNAVlYwEMazy1+/jnLxltBkIQ==
|
||||||
|
|
||||||
async@0.2.10:
|
|
||||||
version "0.2.10"
|
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
|
|
||||||
integrity sha1-trvgsGdLnXGXCMo43owjfLUmw9E=
|
|
||||||
|
|
||||||
async@>=0.6.0:
|
async@>=0.6.0:
|
||||||
version "3.2.0"
|
version "3.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.0.tgz#b3a2685c5ebb641d3de02d161002c60fc9f85720"
|
||||||
@@ -2080,13 +2075,6 @@ binary-extensions@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c"
|
||||||
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow==
|
||||||
|
|
||||||
binary-search-tree@0.2.5:
|
|
||||||
version "0.2.5"
|
|
||||||
resolved "https://registry.yarnpkg.com/binary-search-tree/-/binary-search-tree-0.2.5.tgz#7dbb3b210fdca082450dad2334c304af39bdc784"
|
|
||||||
integrity sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=
|
|
||||||
dependencies:
|
|
||||||
underscore "~1.4.4"
|
|
||||||
|
|
||||||
bindings@^1.5.0:
|
bindings@^1.5.0:
|
||||||
version "1.5.0"
|
version "1.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df"
|
||||||
@@ -6811,13 +6799,6 @@ local-access@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
|
resolved "https://registry.yarnpkg.com/local-access/-/local-access-1.1.0.tgz#e007c76ba2ca83d5877ba1a125fc8dfe23ba4798"
|
||||||
integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
|
integrity sha512-XfegD5pyTAfb+GY6chk283Ox5z8WexG56OvM06RWLpAc/UHozO8X6xAxEkIitZOtsSMM1Yr3DkHgW5W+onLhCw==
|
||||||
|
|
||||||
localforage@^1.3.0:
|
|
||||||
version "1.7.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.7.3.tgz#0082b3ca9734679e1bd534995bdd3b24cf10f204"
|
|
||||||
integrity sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==
|
|
||||||
dependencies:
|
|
||||||
lie "3.1.1"
|
|
||||||
|
|
||||||
localforage@^1.9.0:
|
localforage@^1.9.0:
|
||||||
version "1.9.0"
|
version "1.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
|
resolved "https://registry.yarnpkg.com/localforage/-/localforage-1.9.0.tgz#f3e4d32a8300b362b4634cc4e066d9d00d2f09d1"
|
||||||
@@ -7315,7 +7296,7 @@ mkdirp@0.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e"
|
||||||
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
|
integrity sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=
|
||||||
|
|
||||||
mkdirp@0.x, mkdirp@^0.5.1, mkdirp@~0.5.1:
|
mkdirp@0.x, mkdirp@^0.5.1:
|
||||||
version "0.5.1"
|
version "0.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||||
@@ -7485,24 +7466,6 @@ ncp@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||||
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
|
integrity sha1-GVoh1sRuNh0vsSgbo4uR6d9727M=
|
||||||
|
|
||||||
nedb-promises@^4.0.1:
|
|
||||||
version "4.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/nedb-promises/-/nedb-promises-4.0.1.tgz#4d0bd1553d045acca5d6713ad76eb97aa830b390"
|
|
||||||
integrity sha512-I6nVZ0zjjYGfja2UU8lDSEzjfQTS8bo+8jvn7apILpynYDKzLpl6YRfdPa+uRSUYDN9bH45wJ+gvRWcOjO2g5g==
|
|
||||||
dependencies:
|
|
||||||
nedb "^1.8.0"
|
|
||||||
|
|
||||||
nedb@^1.8.0:
|
|
||||||
version "1.8.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/nedb/-/nedb-1.8.0.tgz#0e3502cd82c004d5355a43c9e55577bd7bd91d88"
|
|
||||||
integrity sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=
|
|
||||||
dependencies:
|
|
||||||
async "0.2.10"
|
|
||||||
binary-search-tree "0.2.5"
|
|
||||||
localforage "^1.3.0"
|
|
||||||
mkdirp "~0.5.1"
|
|
||||||
underscore "~1.4.4"
|
|
||||||
|
|
||||||
negotiator@0.6.2:
|
negotiator@0.6.2:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
|
||||||
@@ -10693,11 +10656,6 @@ undefsafe@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.0.tgz#3ccdcbb824230fc6bf234ad0ddcd83dff4eafe5f"
|
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.13.0.tgz#3ccdcbb824230fc6bf234ad0ddcd83dff4eafe5f"
|
||||||
integrity sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==
|
integrity sha512-sCs4H3pCytsb5K7i072FAEC9YlSYFIbosvM0tAKAlpSSUgD7yC1iXSEGdl5XrDKQ1YUB+p/HDzYrSG2H2Vl36g==
|
||||||
|
|
||||||
underscore@~1.4.4:
|
|
||||||
version "1.4.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
|
|
||||||
integrity sha1-YaajIBBiKvoHljvzJSA88SI51gQ=
|
|
||||||
|
|
||||||
union-value@^1.0.0:
|
union-value@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
||||||
|
|||||||
Reference in New Issue
Block a user