mirror of
https://github.com/dbgate/dbgate
synced 2024-11-23 08:51:02 +00:00
Merge branch 'develop'
This commit is contained in:
commit
03c2a58557
@ -44,7 +44,6 @@
|
||||
"line-reader": "^0.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"node-cron": "^2.0.3",
|
||||
"node-ssh-forward": "^0.7.2",
|
||||
"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 { fork } = require('child_process');
|
||||
const _ = require('lodash');
|
||||
const nedb = require('nedb-promises');
|
||||
const fs = require('fs-extra');
|
||||
|
||||
const { datadir, filesdir } = require('../utility/directories');
|
||||
@ -9,6 +8,7 @@ const socket = require('../utility/socket');
|
||||
const { encryptConnection } = require('../utility/crypting');
|
||||
const { handleProcessCommunication } = require('../utility/processComm');
|
||||
const { pickSafeConnectionInfo } = require('../utility/crypting');
|
||||
const JsonLinesDatabase = require('../utility/JsonLinesDatabase');
|
||||
|
||||
const processArgs = require('../utility/processArgs');
|
||||
|
||||
@ -136,7 +136,7 @@ module.exports = {
|
||||
const dir = datadir();
|
||||
if (!portalConnections) {
|
||||
// @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;
|
||||
const encrypted = encryptConnection(connection);
|
||||
if (connection._id) {
|
||||
res = await this.datastore.update(_.pick(connection, '_id'), encrypted);
|
||||
res = await this.datastore.update(encrypted);
|
||||
} else {
|
||||
res = await this.datastore.insert(encrypted);
|
||||
}
|
||||
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;
|
||||
},
|
||||
|
||||
update_meta: true,
|
||||
async update({ _id, values }) {
|
||||
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');
|
||||
return res;
|
||||
},
|
||||
@ -192,22 +196,24 @@ module.exports = {
|
||||
updateDatabase_meta: true,
|
||||
async updateDatabase({ conid, database, values }) {
|
||||
if (portalConnections) return;
|
||||
const conn = await this.datastore.find({ _id: conid });
|
||||
let databases = conn[0].databases || [];
|
||||
const conn = await this.datastore.get(conid);
|
||||
let databases = (conn && conn.databases) || [];
|
||||
if (databases.find(x => x.name == database)) {
|
||||
databases = databases.map(x => (x.name == database ? { ...x, ...values } : x));
|
||||
} else {
|
||||
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('used-apps-changed');
|
||||
// socket.emitChanged(`db-apps-changed-${conid}-${database}`);
|
||||
return res;
|
||||
},
|
||||
|
||||
delete_meta: true,
|
||||
async delete(connection) {
|
||||
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');
|
||||
return res;
|
||||
},
|
||||
@ -215,8 +221,8 @@ module.exports = {
|
||||
get_meta: true,
|
||||
async get({ conid }) {
|
||||
if (portalConnections) return portalConnections.find(x => x._id == conid) || null;
|
||||
const res = await this.datastore.find({ _id: conid });
|
||||
return res[0] || null;
|
||||
const res = await this.datastore.get(conid);
|
||||
return res || null;
|
||||
},
|
||||
|
||||
newSqliteDatabase_meta: true,
|
||||
|
@ -1,12 +1,13 @@
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const fs = require('fs-extra');
|
||||
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 hasPermission = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
const apps = require('./apps');
|
||||
|
||||
function serialize(format, data) {
|
||||
if (format == 'text') return data;
|
||||
@ -74,6 +75,11 @@ module.exports = {
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
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 {
|
||||
if (!hasPermission(`files/${folder}/read`)) return null;
|
||||
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));
|
||||
socket.emitChanged(`archive-files-changed-${folder.substring('archive:'.length)}`);
|
||||
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 {
|
||||
if (!hasPermission(`files/${folder}/write`)) return false;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
|
@ -19,6 +19,7 @@ const runners = require('./controllers/runners');
|
||||
const jsldata = require('./controllers/jsldata');
|
||||
const config = require('./controllers/config');
|
||||
const archive = require('./controllers/archive');
|
||||
const apps = require('./controllers/apps');
|
||||
const uploads = require('./controllers/uploads');
|
||||
const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
@ -157,6 +158,7 @@ function useAllControllers(app, electron) {
|
||||
useController(app, electron, '/files', files);
|
||||
useController(app, electron, '/scheduler', scheduler);
|
||||
useController(app, electron, '/query-history', queryHistory);
|
||||
useController(app, electron, '/apps', apps);
|
||||
}
|
||||
|
||||
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 { evaluateCondition } = require('dbgate-sqltree');
|
||||
|
||||
async function fetchNextLine(reader) {
|
||||
function fetchNextLineFromReader(reader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!reader.hasNextLine()) {
|
||||
resolve(null);
|
||||
@ -62,7 +62,7 @@ class JsonLinesDatastore {
|
||||
|
||||
async _readLine(parse) {
|
||||
for (;;) {
|
||||
const line = await fetchNextLine(this.reader);
|
||||
const line = await fetchNextLineFromReader(this.reader);
|
||||
if (!line) {
|
||||
// EOF
|
||||
return null;
|
||||
|
@ -38,6 +38,7 @@ const rundir = dirFunc('run', true);
|
||||
const uploadsdir = dirFunc('uploads', true);
|
||||
const pluginsdir = dirFunc('plugins');
|
||||
const archivedir = dirFunc('archive');
|
||||
const appdir = dirFunc('apps');
|
||||
const filesdir = dirFunc('files');
|
||||
|
||||
function packagedPluginsDir() {
|
||||
@ -103,6 +104,7 @@ module.exports = {
|
||||
rundir,
|
||||
uploadsdir,
|
||||
archivedir,
|
||||
appdir,
|
||||
ensureDirectory,
|
||||
pluginsdir,
|
||||
filesdir,
|
||||
|
@ -28,6 +28,7 @@ export interface DisplayColumn {
|
||||
autoIncrement?: boolean;
|
||||
isPrimaryKey?: boolean;
|
||||
foreignKey?: ForeignKeyInfo;
|
||||
isForeignKeyUnique?: boolean;
|
||||
isExpandable?: boolean;
|
||||
isChecked?: boolean;
|
||||
hintColumnNames?: string[];
|
||||
|
@ -1,5 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { filterName } from 'dbgate-tools';
|
||||
import { filterName, isTableColumnUnique } from 'dbgate-tools';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import {
|
||||
TableInfo,
|
||||
@ -79,10 +79,11 @@ export class TableGridDisplay extends GridDisplay {
|
||||
...col,
|
||||
isChecked: this.isColumnChecked(col),
|
||||
hintColumnNames:
|
||||
this.getFkDictionaryDescription(col.foreignKey)?.columns?.map(
|
||||
this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)?.columns?.map(
|
||||
columnName => `hint_${col.uniqueName}_${columnName}`
|
||||
) || null,
|
||||
hintColumnDelimiter: this.getFkDictionaryDescription(col.foreignKey)?.delimiter,
|
||||
hintColumnDelimiter: this.getFkDictionaryDescription(col.isForeignKeyUnique ? col.foreignKey : null)
|
||||
?.delimiter,
|
||||
isExpandable: !!col.foreignKey,
|
||||
})) || []
|
||||
);
|
||||
@ -203,7 +204,8 @@ export class TableGridDisplay extends GridDisplay {
|
||||
}
|
||||
|
||||
getFkTarget(column: DisplayColumn) {
|
||||
const { uniqueName, foreignKey } = column;
|
||||
const { uniqueName, foreignKey, isForeignKeyUnique } = column;
|
||||
if (!isForeignKeyUnique) return null;
|
||||
const pureName = foreignKey.refTableName;
|
||||
const schemaName = foreignKey.refSchemaName;
|
||||
return this.findTable({ schemaName, pureName });
|
||||
@ -230,7 +232,7 @@ export class TableGridDisplay extends GridDisplay {
|
||||
const uniquePath = [...parentPath, col.columnName];
|
||||
const uniqueName = uniquePath.join('.');
|
||||
// console.log('this.config.addedColumns', this.config.addedColumns, uniquePath);
|
||||
return {
|
||||
const res = {
|
||||
...col,
|
||||
pureName: table.pureName,
|
||||
schemaName: table.schemaName,
|
||||
@ -241,7 +243,19 @@ export class TableGridDisplay extends GridDisplay {
|
||||
foreignKey:
|
||||
table.foreignKeys &&
|
||||
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(
|
||||
|
@ -29,18 +29,18 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
||||
// console.log('ANALYSING', name, structure);
|
||||
if (structure && options.dropIfExists) {
|
||||
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)) {
|
||||
console.log(`Creating table ${fullNameQuoted}`);
|
||||
const dmp = driver.createDumper();
|
||||
dmp.createTable(prepareTableForImport({ ...writable.structure, ...name }));
|
||||
console.log(dmp.s);
|
||||
await driver.query(pool, dmp.s);
|
||||
await driver.script(pool, dmp.s);
|
||||
structure = await driver.analyseSingleTable(pool, name);
|
||||
}
|
||||
if (options.truncate) {
|
||||
await driver.query(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||
await driver.script(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
export function addTableDependencies(db: DatabaseInfo): DatabaseInfo {
|
||||
@ -90,3 +90,31 @@ function fillDatabaseExtendedInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
export function extendDatabaseInfo(db: DatabaseInfo): DatabaseInfo {
|
||||
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 {
|
||||
const res = _cloneDeep(table);
|
||||
res.foreignKeys = [];
|
||||
res.indexes = [];
|
||||
res.uniques = [];
|
||||
res.checks = [];
|
||||
if (res.primaryKey) res.primaryKey.constraintName = null;
|
||||
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 {
|
||||
className: string;
|
||||
themeClassName: string;
|
||||
themeName: string;
|
||||
themeType: 'light' | 'dark';
|
||||
themeCss?: string;
|
||||
}
|
||||
|
||||
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 './extensions';
|
||||
export * from './alter-processor';
|
||||
export * from './appdefs';
|
||||
|
@ -15,6 +15,7 @@
|
||||
import { subscribeConnectionPingers } from './utility/connectionsPinger';
|
||||
import { subscribePermissionCompiler } from './utility/hasPermission';
|
||||
import { apiCall } from './utility/api';
|
||||
import { getUsedApps } from './utility/metadataLoaders';
|
||||
|
||||
let loadedApi = false;
|
||||
|
||||
@ -30,7 +31,8 @@
|
||||
const settings = await apiCall('config/get-settings');
|
||||
const connections = await apiCall('connections/list');
|
||||
const config = await apiCall('config/get');
|
||||
loadedApi = settings && connections && config;
|
||||
const apps = await getUsedApps();
|
||||
loadedApi = settings && connections && config && apps;
|
||||
|
||||
if (loadedApi) {
|
||||
subscribeApiDependendStores();
|
||||
|
@ -26,6 +26,12 @@
|
||||
$: currentThemeType = $currentThemeDefinition?.themeType == 'dark' ? 'theme-type-dark' : 'theme-type-light';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
{#if $currentThemeDefinition?.themeCss}
|
||||
{@html `<style id="themePlugin">${$currentThemeDefinition?.themeCss}</style>`}
|
||||
{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class={`${$currentTheme} ${currentThemeType} root dbgate-screen`}
|
||||
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}
|
||||
/>
|
@ -81,7 +81,7 @@
|
||||
markArchiveFileAsDataSheet,
|
||||
markArchiveFileAsReadonly,
|
||||
} from '../utility/archiveTools';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
export let data;
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import getConnectionLabel from '../utility/getConnectionLabel';
|
||||
import { getDatabaseList } from '../utility/metadataLoaders';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
@ -97,7 +97,7 @@
|
||||
value: 'newdb',
|
||||
label: 'Database name',
|
||||
onConfirm: name =>
|
||||
apiCall('server-connections/create-database', {
|
||||
apiCall('server-connections/create-database', {
|
||||
conid: data._id,
|
||||
name,
|
||||
}),
|
||||
@ -153,7 +153,7 @@
|
||||
],
|
||||
data.singleDatabase && [
|
||||
{ divider: true },
|
||||
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase),
|
||||
getDatabaseMenuItems(data, data.defaultDatabase, $extensions, $currentDatabase, $apps),
|
||||
],
|
||||
];
|
||||
};
|
||||
@ -186,6 +186,8 @@
|
||||
statusTitle = null;
|
||||
}
|
||||
}
|
||||
|
||||
$: apps = useUsedApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
|
@ -1,7 +1,8 @@
|
||||
<script lang="ts" context="module">
|
||||
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 tooltip = `${getConnectionLabel(connection)}\n${name}`;
|
||||
openNewTab({
|
||||
@ -157,8 +158,20 @@
|
||||
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 commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||
|
||||
return [
|
||||
{ onClick: handleNewQuery, text: 'New query', isNewQuery: true },
|
||||
!driver?.dialect?.nosql && { onClick: handleNewTable, text: 'New table' },
|
||||
@ -180,6 +193,20 @@
|
||||
|
||||
_.get($currentDatabase, 'connection._id') == _.get(connection, '_id') &&
|
||||
_.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>
|
||||
@ -207,18 +234,22 @@
|
||||
import { showSnackbarSuccess } from '../utility/snackbar';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { getDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
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 passProps;
|
||||
|
||||
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);
|
||||
$: apps = useUsedApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
|
@ -30,7 +30,7 @@ import runCommand from './runCommand';
|
||||
function themeCommand(theme: ThemeDefinition) {
|
||||
return {
|
||||
text: theme.themeName,
|
||||
onClick: () => currentTheme.set(theme.className),
|
||||
onClick: () => currentTheme.set(theme.themeClassName),
|
||||
// onPreview: () => {
|
||||
// const old = get(currentTheme);
|
||||
// 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({
|
||||
id: 'new.table',
|
||||
category: 'New',
|
||||
|
@ -7,6 +7,8 @@
|
||||
import { isTypeDateTime } from 'dbgate-tools';
|
||||
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
@ -14,6 +16,7 @@
|
||||
export let setSort;
|
||||
export let grouping = undefined;
|
||||
export let order = undefined;
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
@ -26,6 +29,16 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleDefineVirtualForeignKey = () => {
|
||||
showModal(VirtualForeignKeyEditorModal, {
|
||||
schemaName: column.schemaName,
|
||||
pureName: column.pureName,
|
||||
conid,
|
||||
database,
|
||||
columnName: column.columnName,
|
||||
});
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
@ -49,6 +62,11 @@
|
||||
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
|
||||
allowDefineVirtualReferences && [
|
||||
{ divider: true },
|
||||
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
],
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
@ -303,6 +303,7 @@
|
||||
export let errorMessage = undefined;
|
||||
export let pureName = undefined;
|
||||
export let schemaName = undefined;
|
||||
export let allowDefineVirtualReferences = false;
|
||||
|
||||
export let isLoadedAll;
|
||||
export let loadedTime;
|
||||
@ -1425,6 +1426,7 @@
|
||||
}}
|
||||
setGrouping={display.groupable ? groupFunc => display.setGrouping(col.uniqueName, groupFunc) : null}
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
{allowDefineVirtualReferences}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
|
@ -7,7 +7,7 @@
|
||||
TableGridDisplay,
|
||||
} from 'dbgate-datalib';
|
||||
import { getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import { extendDatabaseInfoFromApps, findEngineDriver } from 'dbgate-tools';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
import VerticalSplitter from '../elements/VerticalSplitter.svelte';
|
||||
@ -16,9 +16,11 @@
|
||||
|
||||
import {
|
||||
useConnectionInfo,
|
||||
useConnectionList,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
useUsedApps,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
@ -46,6 +48,9 @@
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
$: apps = useUsedApps();
|
||||
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
|
||||
$: connections = useConnectionList();
|
||||
|
||||
// $: console.log('serverVersion', $serverVersion);
|
||||
|
||||
@ -64,10 +69,10 @@
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
extendedDbInfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion,
|
||||
table => getDictionaryDescription(table, conid, database)
|
||||
table => getDictionaryDescription(table, conid, database, $apps, $connections)
|
||||
)
|
||||
: null;
|
||||
|
||||
@ -80,10 +85,10 @@
|
||||
setConfig,
|
||||
cache,
|
||||
setCache,
|
||||
$dbinfo,
|
||||
extendedDbInfo,
|
||||
{ showHintColumns: getBoolSettingsValue('dataGrid.showHintColumns', true) },
|
||||
$serverVersion,
|
||||
table => getDictionaryDescription(table, conid, database)
|
||||
table => getDictionaryDescription(table, conid, database, $apps, $connections)
|
||||
)
|
||||
: null;
|
||||
|
||||
@ -159,6 +164,7 @@
|
||||
macroCondition={macro => macro.type == 'transformValue'}
|
||||
onReferenceSourceChanged={reference ? handleReferenceSourceChanged : null}
|
||||
multipleGridsOnTab={multipleGridsOnTab || !!reference}
|
||||
allowDefineVirtualReferences
|
||||
onReferenceClick={value => {
|
||||
if (value && value.referenceId && reference && reference.referenceId == value.referenceId) {
|
||||
// reference not changed
|
||||
|
@ -29,7 +29,7 @@
|
||||
import DesignerTable from './DesignerTable.svelte';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { getTableInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
@ -46,6 +46,7 @@
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import ChooseColorModal from '../modals/ChooseColorModal.svelte';
|
||||
import { currentThemeDefinition } from '../stores';
|
||||
import { extendDatabaseInfoFromApps } from 'dbgate-tools';
|
||||
|
||||
export let value;
|
||||
export let onChange;
|
||||
@ -67,10 +68,12 @@
|
||||
const targetDragColumn$ = writable(null);
|
||||
|
||||
const dbInfo = settings?.updateFromDbInfo ? useDatabaseInfo({ conid, database }) : null;
|
||||
$: dbInfoExtended = $dbInfo ? extendDatabaseInfoFromApps($dbInfo, $apps) : null;
|
||||
|
||||
$: tables = value?.tables as any[];
|
||||
$: references = value?.references as any[];
|
||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||
$: apps = useUsedApps();
|
||||
|
||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||
|
||||
@ -94,8 +97,8 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
if (dbInfo) {
|
||||
updateFromDbInfo($dbInfo);
|
||||
if (dbInfoExtended) {
|
||||
updateFromDbInfo(dbInfoExtended as any);
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,13 +107,13 @@
|
||||
}
|
||||
|
||||
$: {
|
||||
if (dbInfo && value?.autoLayout) {
|
||||
performAutoActions($dbInfo);
|
||||
if (dbInfoExtended && value?.autoLayout) {
|
||||
performAutoActions(dbInfoExtended);
|
||||
}
|
||||
}
|
||||
|
||||
function updateFromDbInfo(db = 'auto') {
|
||||
if (db == 'auto' && dbInfo) db = $dbInfo;
|
||||
if (db == 'auto' && dbInfo) db = dbInfoExtended as any;
|
||||
if (!settings?.updateFromDbInfo || !db) return;
|
||||
|
||||
onChange(current => {
|
||||
@ -372,8 +375,8 @@
|
||||
};
|
||||
|
||||
const handleAddTableReferences = async table => {
|
||||
if (!dbInfo) return;
|
||||
const db = $dbInfo;
|
||||
if (!dbInfoExtended) return;
|
||||
const db = dbInfoExtended;
|
||||
if (!db) return;
|
||||
callChange(current => {
|
||||
return getTablesWithReferences(db, table, current);
|
||||
@ -692,13 +695,17 @@
|
||||
if (css) css += '\n';
|
||||
css += cssItem;
|
||||
}
|
||||
if ($currentThemeDefinition?.themeCss) {
|
||||
if (css) css += '\n';
|
||||
css += $currentThemeDefinition?.themeCss;
|
||||
}
|
||||
saveFileToDisk(async filePath => {
|
||||
await apiCall('files/export-diagram', {
|
||||
filePath,
|
||||
html: domCanvas.outerHTML,
|
||||
css,
|
||||
themeType: $currentThemeDefinition?.themeType,
|
||||
themeClassName: $currentThemeDefinition?.className,
|
||||
themeClassName: $currentThemeDefinition?.themeClassName,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import { currentThemeDefinition } from '../stores';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import contextMenu from '../utility/contextMenu';
|
||||
import moveDrag from '../utility/moveDrag';
|
||||
import ColumnLine from './ColumnLine.svelte';
|
||||
@ -166,6 +167,15 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleDefineVirtualForeignKey = table => {
|
||||
showModal(VirtualForeignKeyEditorModal, {
|
||||
schemaName: table.schemaName,
|
||||
pureName: table.pureName,
|
||||
conid,
|
||||
database,
|
||||
});
|
||||
};
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ text: 'Remove', onClick: () => onRemoveTable({ designerId }) },
|
||||
@ -185,6 +195,11 @@
|
||||
settings?.allowAddAllReferences &&
|
||||
!isMultipleTableSelection && { text: 'Add references', onClick: () => onAddAllReferences(table) },
|
||||
settings?.allowChangeColor && { text: 'Change color', onClick: () => onChangeTableColor(table) },
|
||||
settings?.allowDefineVirtualReferences &&
|
||||
!isMultipleTableSelection && {
|
||||
text: 'Define virtual foreign key',
|
||||
onClick: () => handleDefineVirtualForeignKey(table),
|
||||
},
|
||||
settings?.appendTableSystemMenu &&
|
||||
!isMultipleTableSelection && [{ divider: true }, createDatabaseObjectMenu({ ...table, conid, database })],
|
||||
];
|
||||
|
@ -21,6 +21,7 @@
|
||||
allowChangeColor: true,
|
||||
appendTableSystemMenu: true,
|
||||
customizeStyle: true,
|
||||
allowDefineVirtualReferences: true,
|
||||
}}
|
||||
referenceComponent={DiagramDesignerReference}
|
||||
/>
|
||||
|
@ -21,6 +21,7 @@
|
||||
allowChangeColor: false,
|
||||
appendTableSystemMenu: false,
|
||||
customizeStyle: false,
|
||||
allowDefineVirtualReferences: false,
|
||||
}}
|
||||
referenceComponent={QueryDesignerReference}
|
||||
/>
|
||||
|
@ -9,11 +9,13 @@
|
||||
export let name;
|
||||
export let options;
|
||||
export let isClearable = false;
|
||||
export let selectFieldComponent = SelectField;
|
||||
|
||||
const { values, setFieldValue } = getFormContext();
|
||||
</script>
|
||||
|
||||
<SelectField
|
||||
<svelte:component
|
||||
this={selectFieldComponent}
|
||||
{...$$restProps}
|
||||
value={$values && $values[name]}
|
||||
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 pin': 'mdi mdi-pin',
|
||||
'icon arrange': 'mdi mdi-arrange-send-to-back',
|
||||
'icon app': 'mdi mdi-layers-triple',
|
||||
|
||||
'icon columns': 'mdi mdi-view-column',
|
||||
'icon columns-outline': 'mdi mdi-view-column-outline',
|
||||
@ -126,6 +127,9 @@
|
||||
'img diagram': 'mdi mdi-graph color-icon-blue',
|
||||
'img yaml': 'mdi mdi-code-brackets 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 minus': 'mdi mdi-minus-circle color-icon-red',
|
||||
|
@ -1,10 +1,11 @@
|
||||
<script lang="ts">
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import _ from 'lodash';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FormStyledButton from '../elements/FormStyledButton.svelte';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
import { useTableInfo } from '../utility/metadataLoaders';
|
||||
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import TableControl from '../elements/TableControl.svelte';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
@ -19,6 +20,10 @@
|
||||
} from '../utility/dictionaryDescriptionTools';
|
||||
import { includes } from 'lodash';
|
||||
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 database;
|
||||
@ -28,18 +33,41 @@
|
||||
|
||||
$: 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) {
|
||||
$values = {
|
||||
targetApplication: $values.targetApplication,
|
||||
columns: descriptionInfo.expression,
|
||||
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>
|
||||
|
||||
<FormProviderCore {values}>
|
||||
@ -75,7 +103,14 @@
|
||||
|
||||
<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">
|
||||
<FormSubmit
|
||||
@ -89,7 +124,7 @@
|
||||
database,
|
||||
$values.columns,
|
||||
$values.delimiter,
|
||||
$values.useForAllDatabases
|
||||
$values.targetApplication
|
||||
);
|
||||
onConfirm();
|
||||
}}
|
||||
|
@ -6,7 +6,7 @@
|
||||
import { closeCurrentModal, showModal } from './modalTools';
|
||||
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.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 { onMount } from 'svelte';
|
||||
import { dumpSqlSelect } from 'dbgate-sqltree';
|
||||
@ -33,6 +33,9 @@
|
||||
|
||||
let checkedKeys = [];
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: connections = useConnectionList();
|
||||
|
||||
function defineDescription() {
|
||||
showModal(DefineDictionaryDescriptionModal, {
|
||||
conid,
|
||||
@ -45,7 +48,7 @@
|
||||
|
||||
async function reload() {
|
||||
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?.primaryKey?.columns?.length != 1) return;
|
||||
@ -112,6 +115,8 @@
|
||||
|
||||
$: {
|
||||
search;
|
||||
$apps;
|
||||
$connections;
|
||||
reload();
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script context="module">
|
||||
export const className = 'theme-dark';
|
||||
export const themeClassName = 'theme-dark';
|
||||
export const themeName = 'Dark';
|
||||
export const themeType = 'dark';
|
||||
</script>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script context="module">
|
||||
export const className = 'theme-light';
|
||||
export const themeClassName = 'theme-light';
|
||||
export const themeName = 'Light';
|
||||
export const themeType = 'light';
|
||||
</script>
|
||||
|
@ -64,6 +64,7 @@ export const openedModals = writable([]);
|
||||
export const openedSnackbars = writable([]);
|
||||
export const nullStore = readable(null, () => {});
|
||||
export const currentArchive = writableWithStorage('default', 'currentArchive');
|
||||
export const currentApplication = writableWithStorage(null, 'currentApplication');
|
||||
export const isFileDragActive = writable(false);
|
||||
export const selectedCellsCallback = writable(null);
|
||||
export const loadingPluginStore = writable({
|
||||
@ -72,7 +73,7 @@ export const loadingPluginStore = writable({
|
||||
});
|
||||
|
||||
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');
|
||||
|
@ -81,7 +81,7 @@
|
||||
value={fullNameToString({ pureName: refTableName, schemaName: refSchemaName })}
|
||||
isNative
|
||||
notSelected
|
||||
options={(dbInfo?.tables || []).map(tbl => ({
|
||||
options={_.sortBy(dbInfo?.tables || [], ['schemaName', 'pureName']).map(tbl => ({
|
||||
label: fullNameToLabel(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');
|
||||
|
||||
export const activator = createActivator('MarkdownEditorTab', false);
|
||||
export const activator = createActivator('YamlEditorTab', false);
|
||||
|
||||
let domEditor;
|
||||
|
||||
@ -63,8 +63,6 @@
|
||||
|
||||
function createMenu() {
|
||||
return [
|
||||
{ command: 'yaml.preview' },
|
||||
{ divider: true },
|
||||
{ command: 'yaml.toggleComment' },
|
||||
{ divider: true },
|
||||
{ command: 'yaml.save' },
|
||||
|
@ -15,6 +15,7 @@ import * as FavoriteEditorTab from './FavoriteEditorTab.svelte';
|
||||
import * as QueryDesignTab from './QueryDesignTab.svelte';
|
||||
import * as CommandListTab from './CommandListTab.svelte';
|
||||
import * as YamlEditorTab from './YamlEditorTab.svelte';
|
||||
import * as JsonEditorTab from './JsonEditorTab.svelte';
|
||||
import * as CompareModelTab from './CompareModelTab.svelte';
|
||||
import * as JsonTab from './JsonTab.svelte';
|
||||
import * as ChangelogTab from './ChangelogTab.svelte';
|
||||
@ -38,6 +39,7 @@ export default {
|
||||
QueryDesignTab,
|
||||
CommandListTab,
|
||||
YamlEditorTab,
|
||||
JsonEditorTab,
|
||||
CompareModelTab,
|
||||
JsonTab,
|
||||
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 { TableInfo } from 'dbgate-types';
|
||||
import { ApplicationDefinition, TableInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { getLocalStorage, setLocalStorage, removeLocalStorage } from './storageCache';
|
||||
import { apiCall } from './api';
|
||||
import { filterAppsForDatabase, saveDbToApp } from './appTools';
|
||||
|
||||
function checkDescriptionColumns(columns: string[], table: TableInfo) {
|
||||
if (!columns?.length) return false;
|
||||
@ -14,17 +15,20 @@ export function getDictionaryDescription(
|
||||
table: TableInfo,
|
||||
conid: string,
|
||||
database: string,
|
||||
apps: ApplicationDefinition[],
|
||||
connections,
|
||||
skipCheckSaved: boolean = false
|
||||
): DictionaryDescription {
|
||||
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
|
||||
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
|
||||
const conn = connections.find(x => x._id == conid);
|
||||
const dbApps = filterAppsForDatabase(conn, database, apps);
|
||||
|
||||
const cachedSpecific = getLocalStorage(keySpecific);
|
||||
const cachedCommon = getLocalStorage(keyCommon);
|
||||
const cached = _.flatten(dbApps.map(x => x.dictionaryDescriptions || [])).find(
|
||||
x => x.pureName == table.pureName && x.schemaName == table.schemaName
|
||||
);
|
||||
|
||||
if (cachedSpecific && (skipCheckSaved || checkDescriptionColumns(cachedSpecific.columns, table)))
|
||||
return cachedSpecific;
|
||||
if (cachedCommon && (skipCheckSaved || checkDescriptionColumns(cachedCommon.columns, table))) return cachedCommon;
|
||||
if (cached && (skipCheckSaved || checkDescriptionColumns(cached.columns, table))) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
const descColumn = table.columns.find(x => x?.dataType?.toLowerCase()?.includes('char'));
|
||||
if (descColumn) {
|
||||
@ -57,29 +61,22 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
|
||||
return parsed.join(',');
|
||||
}
|
||||
|
||||
export function saveDictionaryDescription(
|
||||
export async function saveDictionaryDescription(
|
||||
table: TableInfo,
|
||||
conid: string,
|
||||
database: string,
|
||||
expression: string,
|
||||
delimiter: string,
|
||||
useForAllDatabases: boolean
|
||||
targetApplication: string
|
||||
) {
|
||||
const keySpecific = `dictionary_spec_${table.schemaName}||${table.pureName}||${conid}||${database}`;
|
||||
const keyCommon = `dictionary_spec_${table.schemaName}||${table.pureName}`;
|
||||
const appFolder = await saveDbToApp(conid, database, targetApplication);
|
||||
|
||||
removeLocalStorage(keySpecific);
|
||||
if (useForAllDatabases) removeLocalStorage(keyCommon);
|
||||
|
||||
const description = {
|
||||
await apiCall('apps/save-dictionary-description', {
|
||||
appFolder,
|
||||
schemaName: table.schemaName,
|
||||
pureName: table.pureName,
|
||||
columns: parseDelimitedColumnList(expression),
|
||||
expression,
|
||||
delimiter,
|
||||
};
|
||||
|
||||
if (useForAllDatabases) {
|
||||
setLocalStorage(keyCommon, description);
|
||||
} else {
|
||||
setLocalStorage(keySpecific, description);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -103,6 +103,30 @@ const archiveFilesLoader = ({ 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 = () => ({
|
||||
url: 'server-connections/server-status',
|
||||
params: {},
|
||||
@ -401,6 +425,36 @@ export function useArchiveFolders(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 = {}) {
|
||||
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 CellDataWidget from './CellDataWidget.svelte';
|
||||
import HistoryWidget from './HistoryWidget.svelte';
|
||||
import AppWidget from './AppWidget.svelte';
|
||||
</script>
|
||||
|
||||
<DatabaseWidget hidden={$selectedWidget != 'database'} />
|
||||
@ -25,3 +26,6 @@
|
||||
{#if $selectedWidget == 'cell-data'}
|
||||
<CellDataWidget />
|
||||
{/if}
|
||||
{#if $selectedWidget == 'app'}
|
||||
<AppWidget />
|
||||
{/if}
|
||||
|
@ -40,6 +40,11 @@
|
||||
name: 'cell-data',
|
||||
title: 'Selected cell data detail view',
|
||||
},
|
||||
{
|
||||
icon: 'icon app',
|
||||
name: 'app',
|
||||
title: 'Application layers',
|
||||
},
|
||||
// {
|
||||
// icon: 'icon 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"
|
||||
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:
|
||||
version "3.2.0"
|
||||
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"
|
||||
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:
|
||||
version "1.5.0"
|
||||
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"
|
||||
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:
|
||||
version "1.9.0"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
|
||||
integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=
|
||||
@ -7485,24 +7466,6 @@ ncp@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
|
||||
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:
|
||||
version "0.6.2"
|
||||
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"
|
||||
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:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/union-value/-/union-value-1.0.1.tgz#0b6fe7b835aecda61c6ea4d4f02c14221e109847"
|
||||
|
Loading…
Reference in New Issue
Block a user