mirror of
https://github.com/DeNNiiInc/dbgate.git
synced 2026-04-17 21:26:00 +00:00
SYNC: Merge pull request #9 from dbgate/feature/apps
This commit is contained in:
@@ -10,6 +10,7 @@ module.exports = ({ editMenu, isMac }) => [
|
||||
{ command: 'new.queryDesign', hideDisabled: true },
|
||||
{ command: 'new.diagram', hideDisabled: true },
|
||||
{ command: 'new.perspective', hideDisabled: true },
|
||||
{ command: 'new.application', hideDisabled: true },
|
||||
{ command: 'new.shell', hideDisabled: true },
|
||||
{ command: 'new.jsonl', hideDisabled: true },
|
||||
{ command: 'new.modelTransform', hideDisabled: true },
|
||||
|
||||
@@ -55,6 +55,10 @@ class AuthProviderBase {
|
||||
return [];
|
||||
}
|
||||
|
||||
async getCurrentFilePermissions(req) {
|
||||
return [];
|
||||
}
|
||||
|
||||
getLoginPageConnections() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,233 +1,98 @@
|
||||
const fs = require('fs-extra');
|
||||
const _ = require('lodash');
|
||||
const path = require('path');
|
||||
const { appdir } = require('../utility/directories');
|
||||
const { appdir, filesdir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const connections = require('./connections');
|
||||
const {
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
hasPermission,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
|
||||
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');
|
||||
this.emitChangedDbApp(folder);
|
||||
return name;
|
||||
},
|
||||
|
||||
files_meta: true,
|
||||
async files({ folder }) {
|
||||
if (!folder) return [];
|
||||
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', { app: 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', { app: 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', { app: 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`);
|
||||
socket.emitChanged('app-files-changed', { app: folder });
|
||||
socket.emitChanged('used-apps-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();
|
||||
getAllApps_meta: true,
|
||||
async getAllApps({}, req) {
|
||||
const dir = path.join(filesdir(), 'apps');
|
||||
const res = [];
|
||||
const loadedPermissions = await loadPermissionsFromRequest(req);
|
||||
const filePermissions = await loadFilePermissionsFromRequest(req);
|
||||
|
||||
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' }),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const file of await fs.readdir(dir)) {
|
||||
if (!hasPermission(`all-files`, loadedPermissions)) {
|
||||
const role = getFilePermissionRole('apps', file, filePermissions);
|
||||
if (role == 'deny') continue;
|
||||
}
|
||||
const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
|
||||
const appJson = JSON.parse(content);
|
||||
// const app = {
|
||||
// appid: file,
|
||||
// name: appJson.applicationName,
|
||||
// usageRules: appJson.usageRules || [],
|
||||
// icon: appJson.applicationIcon || 'img app',
|
||||
// color: appJson.applicationColor,
|
||||
// queries: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'query')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// commands: Object.values(appJson.files || {})
|
||||
// .filter(x => x.type == 'command')
|
||||
// .map(x => ({
|
||||
// name: x.label,
|
||||
// sql: x.sql,
|
||||
// })),
|
||||
// virtualReferences: appJson.virtualReferences,
|
||||
// dictionaryDescriptions: appJson.dictionaryDescriptions,
|
||||
// };
|
||||
const app = {
|
||||
...appJson,
|
||||
appid: file,
|
||||
};
|
||||
|
||||
await processType('.command.sql', 'commands');
|
||||
await processType('.query.sql', 'queries');
|
||||
res.push(app);
|
||||
}
|
||||
|
||||
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 = [];
|
||||
createAppFromDb_meta: true,
|
||||
async createAppFromDb({ appName, server, database }, req) {
|
||||
const appdir = path.join(filesdir(), 'apps');
|
||||
if (!fs.existsSync(appdir)) {
|
||||
await fs.mkdir(appdir);
|
||||
}
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
const appId = _.kebabCase(appName);
|
||||
let suffix = undefined;
|
||||
while (fs.existsSync(path.join(appdir, `${appId}${suffix || ''}`))) {
|
||||
if (!suffix) suffix = 2;
|
||||
else suffix++;
|
||||
}
|
||||
const finalAppId = `${appId}${suffix || ''}`;
|
||||
|
||||
json = [...json, newItem];
|
||||
const appJson = {
|
||||
applicationName: appName,
|
||||
usageRules: [
|
||||
{
|
||||
serverHostsList: server,
|
||||
databaseNamesList: database,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
|
||||
await fs.writeFile(path.join(appdir, `${finalAppId}`), JSON.stringify(appJson, undefined, 2));
|
||||
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return finalAppId;
|
||||
},
|
||||
|
||||
saveVirtualReference_meta: true,
|
||||
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'virtual-references.config.json',
|
||||
async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'virtualReferences',
|
||||
columns.length == 1
|
||||
? x =>
|
||||
!(
|
||||
@@ -245,14 +110,17 @@ module.exports = {
|
||||
columns,
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
saveDictionaryDescription_meta: true,
|
||||
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigFile(
|
||||
appFolder,
|
||||
'dictionary-descriptions.config.json',
|
||||
async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
|
||||
await this.saveConfigItem(
|
||||
appid,
|
||||
'dictionaryDescriptions',
|
||||
x => !(x.schemaName == schemaName && x.pureName == pureName),
|
||||
{
|
||||
schemaName,
|
||||
@@ -263,18 +131,271 @@ module.exports = {
|
||||
}
|
||||
);
|
||||
|
||||
socket.emitChanged(`files-changed`, { folder: 'apps' });
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
createConfigFile_meta: true,
|
||||
async createConfigFile({ appFolder, fileName, content }) {
|
||||
const file = path.join(appdir(), appFolder, fileName);
|
||||
if (!(await fs.exists(file))) {
|
||||
await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
socket.emitChanged('used-apps-changed');
|
||||
return true;
|
||||
async saveConfigItem(appid, fieldName, filterFunc, newItem) {
|
||||
const file = path.join(filesdir(), 'apps', appid);
|
||||
|
||||
const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
|
||||
let json = appJson[fieldName] || [];
|
||||
|
||||
if (filterFunc) {
|
||||
json = json.filter(filterFunc);
|
||||
}
|
||||
return false;
|
||||
|
||||
json = [...json, newItem];
|
||||
|
||||
await fs.writeFile(
|
||||
file,
|
||||
JSON.stringify(
|
||||
{
|
||||
...appJson,
|
||||
[fieldName]: json,
|
||||
},
|
||||
undefined,
|
||||
2
|
||||
)
|
||||
);
|
||||
|
||||
socket.emitChanged('files-changed', { folder: 'apps' });
|
||||
},
|
||||
|
||||
// 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');
|
||||
// this.emitChangedDbApp(folder);
|
||||
// return name;
|
||||
// },
|
||||
|
||||
// files_meta: true,
|
||||
// async files({ folder }) {
|
||||
// if (!folder) return [];
|
||||
// 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', { app: 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', { app: 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', { app: 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`);
|
||||
// socket.emitChanged('app-files-changed', { app: folder });
|
||||
// socket.emitChanged('used-apps-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()));
|
||||
// },
|
||||
|
||||
// // 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', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// },
|
||||
|
||||
// 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;
|
||||
// },
|
||||
|
||||
// createConfigFile_meta: true,
|
||||
// async createConfigFile({ appFolder, fileName, content }) {
|
||||
// const file = path.join(appdir(), appFolder, fileName);
|
||||
// if (!(await fs.exists(file))) {
|
||||
// await fs.writeFile(file, JSON.stringify(content, undefined, 2));
|
||||
// socket.emitChanged('app-files-changed', { app: appFolder });
|
||||
// socket.emitChanged('used-apps-changed');
|
||||
// return true;
|
||||
// }
|
||||
// return false;
|
||||
// },
|
||||
};
|
||||
|
||||
@@ -3,7 +3,12 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
|
||||
const getChartExport = require('../utility/getChartExport');
|
||||
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
|
||||
const {
|
||||
hasPermission,
|
||||
loadPermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getFilePermissionRole,
|
||||
} = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
const getDiagramExport = require('../utility/getDiagramExport');
|
||||
|
||||
@@ -745,6 +745,88 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "file_permission_roles",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "file_permission_roles",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "file_permission_roles",
|
||||
"columnName": "name",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"primaryKey": {
|
||||
"pureName": "file_permission_roles",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_file_permission_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preloadedRows": [
|
||||
{
|
||||
"id": -1,
|
||||
"name": "allow"
|
||||
},
|
||||
{
|
||||
"id": -2,
|
||||
"name": "deny"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columnName": "name",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"primaryKey": {
|
||||
"pureName": "roles",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preloadedRows": [
|
||||
{
|
||||
"id": -1,
|
||||
"name": "anonymous-user"
|
||||
},
|
||||
{
|
||||
"id": -2,
|
||||
"name": "logged-user"
|
||||
},
|
||||
{
|
||||
"id": -3,
|
||||
"name": "superadmin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "role_connections",
|
||||
"columns": [
|
||||
@@ -899,6 +981,85 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "role_id",
|
||||
"dataType": "int",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "folder_name",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "file_names_list",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "file_names_regex",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "role_files",
|
||||
"columnName": "file_permission_role_id",
|
||||
"dataType": "int",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_role_files_role_id",
|
||||
"pureName": "role_files",
|
||||
"refTableName": "roles",
|
||||
"deleteAction": "CASCADE",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "role_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_role_files_file_permission_role_id",
|
||||
"pureName": "role_files",
|
||||
"refTableName": "file_permission_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "file_permission_role_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "role_files",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_role_files",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "role_permissions",
|
||||
"columns": [
|
||||
@@ -1082,49 +1243,6 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "roles",
|
||||
"columnName": "name",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"primaryKey": {
|
||||
"pureName": "roles",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
"preloadedRows": [
|
||||
{
|
||||
"id": -1,
|
||||
"name": "anonymous-user"
|
||||
},
|
||||
{
|
||||
"id": -2,
|
||||
"name": "logged-user"
|
||||
},
|
||||
{
|
||||
"id": -3,
|
||||
"name": "superadmin"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "table_permission_roles",
|
||||
"columns": [
|
||||
@@ -1243,6 +1361,47 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "login",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "password",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "email",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"primaryKey": {
|
||||
"pureName": "users",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_users",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "user_connections",
|
||||
"columns": [
|
||||
@@ -1397,6 +1556,85 @@ module.exports = {
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "user_id",
|
||||
"dataType": "int",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "folder_name",
|
||||
"dataType": "varchar(100)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "file_names_list",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "file_names_regex",
|
||||
"dataType": "varchar(1000)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "user_files",
|
||||
"columnName": "file_permission_role_id",
|
||||
"dataType": "int",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"foreignKeys": [
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_files_user_id",
|
||||
"pureName": "user_files",
|
||||
"refTableName": "users",
|
||||
"deleteAction": "CASCADE",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "user_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"constraintType": "foreignKey",
|
||||
"constraintName": "FK_user_files_file_permission_role_id",
|
||||
"pureName": "user_files",
|
||||
"refTableName": "file_permission_roles",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "file_permission_role_id",
|
||||
"refColumnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"pureName": "user_files",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_user_files",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "user_permissions",
|
||||
"columns": [
|
||||
@@ -1641,47 +1879,6 @@ module.exports = {
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columns": [
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "id",
|
||||
"dataType": "int",
|
||||
"autoIncrement": true,
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "login",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "password",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"pureName": "users",
|
||||
"columnName": "email",
|
||||
"dataType": "varchar(250)",
|
||||
"notNull": false
|
||||
}
|
||||
],
|
||||
"foreignKeys": [],
|
||||
"primaryKey": {
|
||||
"pureName": "users",
|
||||
"constraintType": "primaryKey",
|
||||
"constraintName": "PK_users",
|
||||
"columns": [
|
||||
{
|
||||
"columnName": "id"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"collections": [],
|
||||
|
||||
@@ -85,6 +85,16 @@ async function loadTablePermissionsFromRequest(req) {
|
||||
return tablePermissions;
|
||||
}
|
||||
|
||||
async function loadFilePermissionsFromRequest(req) {
|
||||
const authProvider = getAuthProviderFromReq(req);
|
||||
if (!req) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const filePermissions = await authProvider.getCurrentFilePermissions(req);
|
||||
return filePermissions;
|
||||
}
|
||||
|
||||
function matchDatabasePermissionRow(conid, database, permissionRow) {
|
||||
if (permissionRow.connection_id) {
|
||||
if (conid != permissionRow.connection_id) {
|
||||
@@ -135,6 +145,27 @@ function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissi
|
||||
return true;
|
||||
}
|
||||
|
||||
function matchFilePermissionRow(folder, file, permissionRow) {
|
||||
if (permissionRow.folder_name) {
|
||||
if (folder != permissionRow.folder_name) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_list) {
|
||||
const items = permissionRow.file_names_list.split('\n');
|
||||
if (!items.find(item => item.trim()?.toLowerCase() === file?.toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (permissionRow.file_names_regex) {
|
||||
const regex = new RegExp(permissionRow.file_names_regex, 'i');
|
||||
if (!regex.test(file)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const DATABASE_ROLE_ID_NAMES = {
|
||||
'-1': 'view',
|
||||
'-2': 'read_content',
|
||||
@@ -143,6 +174,11 @@ const DATABASE_ROLE_ID_NAMES = {
|
||||
'-5': 'deny',
|
||||
};
|
||||
|
||||
const FILE_ROLE_ID_NAMES = {
|
||||
'-1': 'allow',
|
||||
'-2': 'deny',
|
||||
};
|
||||
|
||||
function getDatabaseRoleLevelIndex(roleName) {
|
||||
if (!roleName) {
|
||||
return 6;
|
||||
@@ -198,6 +234,17 @@ function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
|
||||
return res;
|
||||
}
|
||||
|
||||
function getFilePermissionRole(folder, file, loadedFilePermissions) {
|
||||
let res = 'deny';
|
||||
for (const permissionRow of loadedFilePermissions) {
|
||||
if (!matchFilePermissionRow(folder, file, permissionRow)) {
|
||||
continue;
|
||||
}
|
||||
res = FILE_ROLE_ID_NAMES[permissionRow.file_permission_role_id];
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
const TABLE_ROLE_ID_NAMES = {
|
||||
'-1': 'read',
|
||||
'-2': 'update_only',
|
||||
@@ -308,8 +355,10 @@ module.exports = {
|
||||
loadPermissionsFromRequest,
|
||||
loadDatabasePermissionsFromRequest,
|
||||
loadTablePermissionsFromRequest,
|
||||
loadFilePermissionsFromRequest,
|
||||
getDatabasePermissionRole,
|
||||
getTablePermissionRole,
|
||||
getFilePermissionRole,
|
||||
testStandardPermission,
|
||||
testDatabaseRolePermission,
|
||||
getTablePermissionRoleLevelIndex,
|
||||
|
||||
@@ -111,3 +111,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
export const DATA_FOLDER_NAMES = [
|
||||
{ name: 'sql', label: 'SQL scripts' },
|
||||
{ name: 'shell', label: 'Shell scripts' },
|
||||
{ name: 'markdown', label: 'Markdown files' },
|
||||
{ name: 'charts', label: 'Charts' },
|
||||
{ name: 'query', label: 'Query designs' },
|
||||
{ name: 'sqlite', label: 'SQLite files' },
|
||||
{ name: 'duckdb', label: 'DuckDB files' },
|
||||
{ name: 'diagrams', label: 'Diagrams' },
|
||||
{ name: 'perspectives', label: 'Perspectives' },
|
||||
{ name: 'impexp', label: 'Import/Export jobs' },
|
||||
{ name: 'modtrans', label: 'Model transforms' },
|
||||
{ name: 'datadeploy', label: 'Data deploy jobs' },
|
||||
{ name: 'dbcompare', label: 'Database compare jobs' },
|
||||
{ name: 'apps', label: 'Applications' },
|
||||
];
|
||||
|
||||
50
packages/types/appdefs.d.ts
vendored
50
packages/types/appdefs.d.ts
vendored
@@ -1,12 +1,12 @@
|
||||
interface ApplicationCommand {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationCommand {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface ApplicationQuery {
|
||||
name: string;
|
||||
sql: string;
|
||||
}
|
||||
// interface ApplicationQuery {
|
||||
// name: string;
|
||||
// sql: string;
|
||||
// }
|
||||
|
||||
interface VirtualReferenceDefinition {
|
||||
pureName: string;
|
||||
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
|
||||
delimiter: string;
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
name: string;
|
||||
|
||||
queries: ApplicationQuery[];
|
||||
commands: ApplicationCommand[];
|
||||
virtualReferences: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions: DictionaryDescriptionDefinition[];
|
||||
interface ApplicationUsageRule {
|
||||
conditionGroup?: string;
|
||||
serverHostsRegex?: string;
|
||||
serverHostsList?: string[];
|
||||
databaseNamesRegex?: string;
|
||||
databaseNamesList?: string[];
|
||||
tableNamesRegex?: string;
|
||||
tableNamesList?: string[];
|
||||
columnNamesRegex?: string;
|
||||
columnNamesList?: string[];
|
||||
}
|
||||
|
||||
export interface ApplicationDefinition {
|
||||
appid: string;
|
||||
applicationName: string;
|
||||
applicationIcon?: string;
|
||||
applicationColor?: string;
|
||||
usageRules?: ApplicationUsageRule[];
|
||||
files?: {
|
||||
[key: string]: {
|
||||
label: string;
|
||||
sql: string;
|
||||
type: 'query' | 'command';
|
||||
};
|
||||
};
|
||||
virtualReferences?: VirtualReferenceDefinition[];
|
||||
dictionaryDescriptions?: DictionaryDescriptionDefinition[];
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
installNewVolatileConnectionListener,
|
||||
refreshPublicCloudFiles,
|
||||
} from './utility/api';
|
||||
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders';
|
||||
import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
|
||||
import AppTitleProvider from './utility/AppTitleProvider.svelte';
|
||||
import getElectron from './utility/getElectron';
|
||||
import AppStartInfo from './widgets/AppStartInfo.svelte';
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
const connections = await apiCall('connections/list');
|
||||
const settings = await getSettings();
|
||||
const apps = await getUsedApps();
|
||||
const apps = await getAllApps();
|
||||
const loadedApiValue = !!(settings && connections && config && apps);
|
||||
|
||||
if (loadedApiValue) {
|
||||
|
||||
35
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
35
packages/web/src/admin/FolderPermissionChooser.svelte
Normal file
@@ -0,0 +1,35 @@
|
||||
<script lang='ts'>
|
||||
import PermissionCheckBox from './PermissionCheckBox.svelte';
|
||||
import { getFormContext } from '../forms/FormProviderCore.svelte';
|
||||
|
||||
const { values } = getFormContext();
|
||||
|
||||
export let onSetPermission;
|
||||
export let label;
|
||||
export let folder;
|
||||
</script>
|
||||
|
||||
<PermissionCheckBox
|
||||
{label}
|
||||
permission={`files/${folder}/*`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<PermissionCheckBox
|
||||
label="Read"
|
||||
permission={`files/${folder}/read`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
<PermissionCheckBox
|
||||
label="Write"
|
||||
permission={`files/${folder}/write`}
|
||||
permissions={$values.permissions}
|
||||
basePermissions={$values.basePermissions}
|
||||
{onSetPermission}
|
||||
/>
|
||||
</div>
|
||||
@@ -36,6 +36,7 @@
|
||||
export let filter = null;
|
||||
export let disableHover = false;
|
||||
export let divProps = {};
|
||||
export let additionalIcons = null;
|
||||
|
||||
$: isChecked =
|
||||
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
|
||||
@@ -160,6 +161,11 @@
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
{#if additionalIcons}
|
||||
{#each additionalIcons as ic}
|
||||
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
|
||||
{/each}
|
||||
{/if}
|
||||
{#if extInfo}
|
||||
<span class="ext-info">
|
||||
<TokenizedFilteredText text={extInfo} {filter} />
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
|
||||
import { getLocalStorage } from '../utility/storageCache';
|
||||
import { apiCall, removeVolatileMapping } from '../utility/api';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
@@ -383,7 +383,7 @@
|
||||
$currentDatabase,
|
||||
$apps,
|
||||
$openedSingleDatabaseConnections,
|
||||
data.databasePermissionRole,
|
||||
data.databasePermissionRole
|
||||
),
|
||||
],
|
||||
|
||||
@@ -427,7 +427,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
|
||||
@@ -405,9 +405,25 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
});
|
||||
};
|
||||
|
||||
const handleCreateNewApp = () => {
|
||||
showModal(InputTextModal, {
|
||||
header: 'New application',
|
||||
label: 'Application name',
|
||||
value: _.startCase(name),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: connection?.server,
|
||||
database: name,
|
||||
});
|
||||
openApplicationEditor(newAppId);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const driver = findEngineDriver(connection, getExtensions());
|
||||
|
||||
const commands = _.flatten((apps || []).map(x => x.commands || []));
|
||||
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
|
||||
|
||||
const isSqlOrDoc =
|
||||
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
|
||||
@@ -564,11 +580,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
hasPermission(`files/apps/write`) && {
|
||||
onClick: handleCreateNewApp,
|
||||
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
|
||||
},
|
||||
|
||||
isProApp() &&
|
||||
apps?.length > 0 && {
|
||||
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
|
||||
submenu: apps.map((app: any) => ({
|
||||
text: app.applicationName,
|
||||
onClick: () => openApplicationEditor(app.appid),
|
||||
})),
|
||||
},
|
||||
|
||||
{ divider: true },
|
||||
|
||||
commands.length > 0 && [
|
||||
commands.map((cmd: any) => ({
|
||||
text: cmd.name,
|
||||
text: cmd.label,
|
||||
onClick: () => {
|
||||
showModal(ConfirmSqlModal, {
|
||||
sql: cmd.sql,
|
||||
@@ -618,12 +649,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
getConnectionLabel,
|
||||
} from 'dbgate-tools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
|
||||
import { openJsonDocument } from '../tabs/JsonTab.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
|
||||
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
|
||||
import newQuery from '../query/newQuery';
|
||||
import ConfirmModal from '../modals/ConfirmModal.svelte';
|
||||
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
|
||||
@@ -639,7 +670,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
import { getNumberIcon } from '../icons/FontIcon.svelte';
|
||||
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
|
||||
import { _t } from '../translations';
|
||||
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
|
||||
import { tick } from 'svelte';
|
||||
|
||||
export let data;
|
||||
export let passProps;
|
||||
@@ -657,8 +688,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
}
|
||||
|
||||
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
|
||||
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
|
||||
|
||||
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
|
||||
|
||||
// $: console.log('AppsForDB:', data?.name, appsForDb);
|
||||
</script>
|
||||
|
||||
<AppObjectCore
|
||||
@@ -681,6 +717,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
|
||||
switchCurrentDatabase(data);
|
||||
}
|
||||
}}
|
||||
additionalIcons={appsForDb?.length > 0
|
||||
? appsForDb.map(ic => ({
|
||||
icon: ic.applicationIcon || 'img app',
|
||||
title: ic.applicationName,
|
||||
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
|
||||
}))
|
||||
: null}
|
||||
on:mousedown={() => {
|
||||
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
|
||||
}}
|
||||
|
||||
@@ -45,16 +45,16 @@
|
||||
schedulerEvents: 'icon scheduler-event',
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
collections: 'CollectionDataTab',
|
||||
views: 'ViewDataTab',
|
||||
matviews: 'ViewDataTab',
|
||||
queries: 'QueryDataTab',
|
||||
procedures: 'SqlObjectTab',
|
||||
functions: 'SqlObjectTab',
|
||||
triggers: 'SqlObjectTab',
|
||||
};
|
||||
// const defaultTabs = {
|
||||
// tables: 'TableDataTab',
|
||||
// collections: 'CollectionDataTab',
|
||||
// views: 'ViewDataTab',
|
||||
// matviews: 'ViewDataTab',
|
||||
// queries: 'QueryDataTab',
|
||||
// procedures: 'SqlObjectTab',
|
||||
// functions: 'SqlObjectTab',
|
||||
// triggers: 'SqlObjectTab',
|
||||
// };
|
||||
|
||||
function createScriptTemplatesSubmenu(objectTypeField) {
|
||||
return {
|
||||
@@ -724,7 +724,7 @@
|
||||
});
|
||||
|
||||
const filteredNoEmptySubmenus = filteredSumenus.filter(x => !x.submenu || x.submenu.length > 0);
|
||||
|
||||
|
||||
return filteredNoEmptySubmenus;
|
||||
}
|
||||
|
||||
@@ -741,7 +741,7 @@
|
||||
export async function openDatabaseObjectDetail(
|
||||
tabComponent,
|
||||
scriptTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode },
|
||||
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
|
||||
forceNewTab?,
|
||||
initialData?,
|
||||
icon?,
|
||||
@@ -776,6 +776,7 @@
|
||||
initialArgs: scriptTemplate ? { scriptTemplate } : null,
|
||||
defaultActionId,
|
||||
isRawMode,
|
||||
sql,
|
||||
},
|
||||
},
|
||||
initialData,
|
||||
@@ -797,7 +798,7 @@
|
||||
data,
|
||||
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {}
|
||||
) {
|
||||
const { schemaName, pureName, conid, database, objectTypeField } = data;
|
||||
const { schemaName, pureName, conid, database, objectTypeField, sql } = data;
|
||||
const driver = findEngineDriver(data, getExtensions());
|
||||
|
||||
const activeTab = getActiveTab();
|
||||
@@ -843,6 +844,7 @@
|
||||
objectTypeField,
|
||||
defaultActionId: prefferedAction.defaultActionId,
|
||||
isRawMode: prefferedAction?.isRawMode ?? false,
|
||||
sql,
|
||||
},
|
||||
forceNewTab,
|
||||
prefferedAction?.initialData,
|
||||
|
||||
@@ -142,6 +142,18 @@
|
||||
label: 'Model transform file',
|
||||
};
|
||||
|
||||
const apps: FileTypeHandler = isProApp()
|
||||
? {
|
||||
icon: 'img app',
|
||||
format: 'json',
|
||||
tabComponent: 'AppEditorTab',
|
||||
folder: 'apps',
|
||||
currentConnection: false,
|
||||
extension: 'json',
|
||||
label: 'Application file',
|
||||
}
|
||||
: undefined;
|
||||
|
||||
export const SAVED_FILE_HANDLERS = {
|
||||
sql,
|
||||
shell,
|
||||
@@ -154,6 +166,7 @@
|
||||
modtrans,
|
||||
datadeploy,
|
||||
dbcompare,
|
||||
apps,
|
||||
};
|
||||
|
||||
export const extractKey = data => data.file;
|
||||
|
||||
@@ -100,4 +100,12 @@ export const defaultDatabaseObjectAppObjectActions = {
|
||||
icon: 'img sql-file',
|
||||
},
|
||||
],
|
||||
queries: [
|
||||
{
|
||||
label: 'Show query',
|
||||
tab: 'QueryDataTab',
|
||||
defaultActionId: 'showAppQuery',
|
||||
icon: 'img app-query',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -268,6 +268,23 @@ if (isProApp()) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isProApp()) {
|
||||
registerCommand({
|
||||
id: 'new.application',
|
||||
category: 'New',
|
||||
icon: 'img app',
|
||||
name: 'Application',
|
||||
menuName: 'New application',
|
||||
onClick: () => {
|
||||
openNewTab({
|
||||
title: 'Application #',
|
||||
icon: 'img app',
|
||||
tabComponent: 'AppEditorTab',
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registerCommand({
|
||||
id: 'new.diagram',
|
||||
category: 'New',
|
||||
@@ -297,22 +314,22 @@ 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.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',
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import DefineDictionaryDescriptionModal from '../modals/DefineDictionaryDescriptionModal.svelte';
|
||||
import { sleep } from '../utility/common';
|
||||
|
||||
export let column;
|
||||
export let conid = undefined;
|
||||
@@ -24,6 +26,7 @@
|
||||
export let allowDefineVirtualReferences = false;
|
||||
export let setGrouping;
|
||||
export let seachInColumns = '';
|
||||
export let onReload = undefined;
|
||||
|
||||
const openReferencedTable = () => {
|
||||
openDatabaseObjectDetail('TableDataTab', null, {
|
||||
@@ -45,6 +48,19 @@
|
||||
});
|
||||
};
|
||||
|
||||
const handleCustomizeDescriptions = () => {
|
||||
showModal(DefineDictionaryDescriptionModal, {
|
||||
conid,
|
||||
database,
|
||||
schemaName: column.foreignKey.refSchemaName,
|
||||
pureName: column.foreignKey.refTableName,
|
||||
onConfirm: async () => {
|
||||
await sleep(100);
|
||||
onReload?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
function getMenu() {
|
||||
return [
|
||||
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
|
||||
@@ -72,10 +88,13 @@
|
||||
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
|
||||
],
|
||||
|
||||
allowDefineVirtualReferences && [
|
||||
{ divider: true },
|
||||
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
],
|
||||
{ divider: true },
|
||||
|
||||
allowDefineVirtualReferences && { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
|
||||
column.foreignKey && {
|
||||
onClick: handleCustomizeDescriptions,
|
||||
text: 'Customize description',
|
||||
},
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2003,6 +2003,7 @@
|
||||
grouping={display.getGrouping(col.uniqueName)}
|
||||
{allowDefineVirtualReferences}
|
||||
seachInColumns={display.config?.searchInColumns}
|
||||
onReload={refresh}
|
||||
/>
|
||||
</td>
|
||||
{/each}
|
||||
|
||||
@@ -15,13 +15,13 @@
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
|
||||
import {
|
||||
useAllApps,
|
||||
useConnectionInfo,
|
||||
useConnectionList,
|
||||
useDatabaseInfo,
|
||||
useDatabaseServerVersion,
|
||||
useServerVersion,
|
||||
useSettings,
|
||||
useUsedApps,
|
||||
} from '../utility/metadataLoaders';
|
||||
|
||||
import DataGrid from './DataGrid.svelte';
|
||||
@@ -53,7 +53,7 @@
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: dbinfo = useDatabaseInfo({ conid, database });
|
||||
$: serverVersion = useDatabaseServerVersion({ conid, database });
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
|
||||
$: connections = useConnectionList();
|
||||
const settingsValue = useSettings();
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
import DesignerTable from './DesignerTable.svelte';
|
||||
import { isConnectedByReference } from './designerTools';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useAllApps, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import cleanupDesignColumns from './cleanupDesignColumns';
|
||||
import _ from 'lodash';
|
||||
import { writable } from 'svelte/store';
|
||||
@@ -108,7 +108,7 @@
|
||||
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
|
||||
) as any[];
|
||||
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
|
||||
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;
|
||||
|
||||
|
||||
444
packages/web/src/forms/FormIconField.svelte
Normal file
444
packages/web/src/forms/FormIconField.svelte
Normal file
@@ -0,0 +1,444 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import { getFormContext } from './FormProviderCore.svelte';
|
||||
|
||||
export let name;
|
||||
export let label;
|
||||
export let defaultIcon;
|
||||
|
||||
export let templateProps = {};
|
||||
const { template, values, setFieldValue } = getFormContext();
|
||||
|
||||
let showPicker = false;
|
||||
|
||||
// Real-world subject icons for application identification
|
||||
const ICONS = [
|
||||
{ icon: defaultIcon, name: '(Default icon)' },
|
||||
|
||||
// Applications & Tools
|
||||
{ icon: 'mdi mdi-application', name: 'Application' },
|
||||
{ icon: 'mdi mdi-cog', name: 'Settings' },
|
||||
{ icon: 'mdi mdi-tools', name: 'Tools' },
|
||||
{ icon: 'mdi mdi-wrench', name: 'Wrench' },
|
||||
{ icon: 'mdi mdi-hammer', name: 'Hammer' },
|
||||
{ icon: 'mdi mdi-screwdriver', name: 'Screwdriver' },
|
||||
{ icon: 'mdi mdi-palette', name: 'Palette' },
|
||||
{ icon: 'mdi mdi-brush', name: 'Brush' },
|
||||
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
|
||||
|
||||
// Files & Folders
|
||||
{ icon: 'mdi mdi-file', name: 'File' },
|
||||
{ icon: 'mdi mdi-folder', name: 'Folder' },
|
||||
{ icon: 'mdi mdi-folder-open', name: 'Folder Open' },
|
||||
{ icon: 'mdi mdi-file-document', name: 'Document' },
|
||||
{ icon: 'mdi mdi-file-image', name: 'Image File' },
|
||||
{ icon: 'mdi mdi-file-video', name: 'Video File' },
|
||||
{ icon: 'mdi mdi-file-music', name: 'Music File' },
|
||||
{ icon: 'mdi mdi-archive', name: 'Archive' },
|
||||
|
||||
|
||||
// Core Applications
|
||||
{ icon: 'mdi mdi-database', name: 'Database' },
|
||||
{ icon: 'mdi mdi-server', name: 'Server' },
|
||||
{ icon: 'mdi mdi-web', name: 'Web' },
|
||||
{ icon: 'mdi mdi-cloud', name: 'Cloud' },
|
||||
{ icon: 'mdi mdi-monitor', name: 'Monitor' },
|
||||
{ icon: 'mdi mdi-laptop', name: 'Laptop' },
|
||||
{ icon: 'mdi mdi-cellphone', name: 'Mobile' },
|
||||
|
||||
// Business & Finance
|
||||
{ icon: 'mdi mdi-briefcase', name: 'Business' },
|
||||
{ icon: 'mdi mdi-bank', name: 'Banking' },
|
||||
{ icon: 'mdi mdi-currency-usd', name: 'Finance' },
|
||||
{ icon: 'mdi mdi-chart-line', name: 'Analytics' },
|
||||
{ icon: 'mdi mdi-chart-bar', name: 'Reports' },
|
||||
{ icon: 'mdi mdi-chart-pie', name: 'Statistics' },
|
||||
{ icon: 'mdi mdi-calculator', name: 'Calculator' },
|
||||
{ icon: 'mdi mdi-cash-register', name: 'Sales' },
|
||||
{ icon: 'mdi mdi-credit-card', name: 'Payments' },
|
||||
{ icon: 'mdi mdi-receipt', name: 'Invoicing' },
|
||||
|
||||
// Communication & Social
|
||||
{ icon: 'mdi mdi-email', name: 'Email' },
|
||||
{ icon: 'mdi mdi-phone', name: 'Phone' },
|
||||
{ icon: 'mdi mdi-message', name: 'Messaging' },
|
||||
{ icon: 'mdi mdi-chat', name: 'Chat' },
|
||||
{ icon: 'mdi mdi-forum', name: 'Forum' },
|
||||
{ icon: 'mdi mdi-account-group', name: 'Team' },
|
||||
{ icon: 'mdi mdi-bullhorn', name: 'Marketing' },
|
||||
{ icon: 'mdi mdi-newspaper', name: 'News' },
|
||||
|
||||
// Education & Knowledge
|
||||
{ icon: 'mdi mdi-school', name: 'Education' },
|
||||
{ icon: 'mdi mdi-book', name: 'Library' },
|
||||
{ icon: 'mdi mdi-book-open', name: 'Learning' },
|
||||
{ icon: 'mdi mdi-certificate', name: 'Certification' },
|
||||
{ icon: 'mdi mdi-graduation-cap', name: 'Academic' },
|
||||
{ icon: 'mdi mdi-microscope', name: 'Research' },
|
||||
{ icon: 'mdi mdi-flask', name: 'Laboratory' },
|
||||
{ icon: 'mdi mdi-library', name: 'Archive' },
|
||||
|
||||
// Healthcare & Medical
|
||||
{ icon: 'mdi mdi-hospital-building', name: 'Hospital' },
|
||||
{ icon: 'mdi mdi-medical-bag', name: 'Medical' },
|
||||
{ icon: 'mdi mdi-heart-pulse', name: 'Health' },
|
||||
{ icon: 'mdi mdi-pill', name: 'Pharmacy' },
|
||||
{ icon: 'mdi mdi-tooth', name: 'Dental' },
|
||||
{ icon: 'mdi mdi-eye', name: 'Vision' },
|
||||
{ icon: 'mdi mdi-stethoscope', name: 'Clinic' },
|
||||
|
||||
// Transportation & Logistics
|
||||
{ icon: 'mdi mdi-truck', name: 'Logistics' },
|
||||
{ icon: 'mdi mdi-car', name: 'Automotive' },
|
||||
{ icon: 'mdi mdi-airplane', name: 'Aviation' },
|
||||
{ icon: 'mdi mdi-ship-wheel', name: 'Maritime' },
|
||||
{ icon: 'mdi mdi-train', name: 'Railway' },
|
||||
{ icon: 'mdi mdi-bus', name: 'Transit' },
|
||||
{ icon: 'mdi mdi-bike', name: 'Cycling' },
|
||||
{ icon: 'mdi mdi-map', name: 'Navigation' },
|
||||
{ icon: 'mdi mdi-gas-station', name: 'Fuel' },
|
||||
|
||||
// Real Estate & Construction
|
||||
{ icon: 'mdi mdi-home', name: 'Real Estate' },
|
||||
{ icon: 'mdi mdi-office-building', name: 'Commercial' },
|
||||
{ icon: 'mdi mdi-factory', name: 'Industrial' },
|
||||
{ icon: 'mdi mdi-hammer', name: 'Construction' },
|
||||
{ icon: 'mdi mdi-wrench', name: 'Maintenance' },
|
||||
{ icon: 'mdi mdi-tools', name: 'Tools' },
|
||||
{ icon: 'mdi mdi-city', name: 'Urban Planning' },
|
||||
|
||||
// Retail & E-commerce
|
||||
{ icon: 'mdi mdi-store', name: 'Retail' },
|
||||
{ icon: 'mdi mdi-shopping', name: 'Shopping' },
|
||||
{ icon: 'mdi mdi-cart', name: 'E-commerce' },
|
||||
{ icon: 'mdi mdi-barcode', name: 'Inventory' },
|
||||
{ icon: 'mdi mdi-package-variant', name: 'Shipping' },
|
||||
{ icon: 'mdi mdi-gift', name: 'Gifts' },
|
||||
|
||||
// Entertainment & Media
|
||||
{ icon: 'mdi mdi-camera', name: 'Photography' },
|
||||
{ icon: 'mdi mdi-video', name: 'Video' },
|
||||
{ icon: 'mdi mdi-music', name: 'Music' },
|
||||
{ icon: 'mdi mdi-gamepad-variant', name: 'Gaming' },
|
||||
{ icon: 'mdi mdi-movie', name: 'Cinema' },
|
||||
{ icon: 'mdi mdi-television', name: 'Broadcasting' },
|
||||
{ icon: 'mdi mdi-radio', name: 'Radio' },
|
||||
{ icon: 'mdi mdi-theater', name: 'Theater' },
|
||||
|
||||
// Food & Hospitality
|
||||
{ icon: 'mdi mdi-food', name: 'Food Service' },
|
||||
{ icon: 'mdi mdi-coffee', name: 'Cafe' },
|
||||
{ icon: 'mdi mdi-silverware-fork-knife', name: 'Restaurant' },
|
||||
{ icon: 'mdi mdi-pizza', name: 'Pizza' },
|
||||
{ icon: 'mdi mdi-cake', name: 'Bakery' },
|
||||
{ icon: 'mdi mdi-glass-wine', name: 'Bar' },
|
||||
{ icon: 'mdi mdi-bed', name: 'Hotel' },
|
||||
|
||||
// Sports & Fitness
|
||||
{ icon: 'mdi mdi-dumbbell', name: 'Fitness' },
|
||||
{ icon: 'mdi mdi-basketball', name: 'Basketball' },
|
||||
{ icon: 'mdi mdi-soccer', name: 'Soccer' },
|
||||
{ icon: 'mdi mdi-tennis', name: 'Tennis' },
|
||||
{ icon: 'mdi mdi-golf', name: 'Golf' },
|
||||
{ icon: 'mdi mdi-run', name: 'Running' },
|
||||
{ icon: 'mdi mdi-swim', name: 'Swimming' },
|
||||
{ icon: 'mdi mdi-yoga', name: 'Yoga' },
|
||||
|
||||
// Nature & Environment
|
||||
{ icon: 'mdi mdi-tree', name: 'Forestry' },
|
||||
{ icon: 'mdi mdi-flower', name: 'Gardening' },
|
||||
{ icon: 'mdi mdi-leaf', name: 'Environment' },
|
||||
{ icon: 'mdi mdi-weather-sunny', name: 'Weather' },
|
||||
{ icon: 'mdi mdi-earth', name: 'Geography' },
|
||||
{ icon: 'mdi mdi-water', name: 'Water' },
|
||||
{ icon: 'mdi mdi-fire', name: 'Energy' },
|
||||
{ icon: 'mdi mdi-lightning-bolt', name: 'Power' },
|
||||
|
||||
// Science & Technology
|
||||
{ icon: 'mdi mdi-rocket', name: 'Aerospace' },
|
||||
{ icon: 'mdi mdi-atom', name: 'Physics' },
|
||||
{ icon: 'mdi mdi-dna', name: 'Genetics' },
|
||||
{ icon: 'mdi mdi-telescope', name: 'Astronomy' },
|
||||
{ icon: 'mdi mdi-robot', name: 'Robotics' },
|
||||
{ icon: 'mdi mdi-chip', name: 'Electronics' },
|
||||
|
||||
// Security & Safety
|
||||
{ icon: 'mdi mdi-shield', name: 'Security' },
|
||||
{ icon: 'mdi mdi-lock', name: 'Access Control' },
|
||||
{ icon: 'mdi mdi-key', name: 'Authentication' },
|
||||
{ icon: 'mdi mdi-fire-truck', name: 'Emergency' },
|
||||
{ icon: 'mdi mdi-police-badge', name: 'Law Enforcement' },
|
||||
|
||||
// Time & Scheduling
|
||||
{ icon: 'mdi mdi-calendar', name: 'Calendar' },
|
||||
{ icon: 'mdi mdi-clock', name: 'Time Tracking' },
|
||||
{ icon: 'mdi mdi-timer', name: 'Timer' },
|
||||
{ icon: 'mdi mdi-alarm', name: 'Reminders' },
|
||||
|
||||
// Creative & Design
|
||||
{ icon: 'mdi mdi-palette', name: 'Design' },
|
||||
{ icon: 'mdi mdi-brush', name: 'Art' },
|
||||
{ icon: 'mdi mdi-draw', name: 'Drawing' },
|
||||
{ icon: 'mdi mdi-image', name: 'Graphics' },
|
||||
{ icon: 'mdi mdi-format-paint', name: 'Painting' },
|
||||
|
||||
// Alpha Icons
|
||||
{ icon: 'mdi mdi-alpha-a-circle', name: 'A' },
|
||||
{ icon: 'mdi mdi-alpha-b-circle', name: 'B' },
|
||||
{ icon: 'mdi mdi-alpha-c-circle', name: 'C' },
|
||||
{ icon: 'mdi mdi-alpha-d-circle', name: 'D' },
|
||||
{ icon: 'mdi mdi-alpha-e-circle', name: 'E' },
|
||||
{ icon: 'mdi mdi-alpha-f-circle', name: 'F' },
|
||||
{ icon: 'mdi mdi-alpha-g-circle', name: 'G' },
|
||||
{ icon: 'mdi mdi-alpha-h-circle', name: 'H' },
|
||||
{ icon: 'mdi mdi-alpha-i-circle', name: 'I' },
|
||||
{ icon: 'mdi mdi-alpha-j-circle', name: 'J' },
|
||||
{ icon: 'mdi mdi-alpha-k-circle', name: 'K' },
|
||||
{ icon: 'mdi mdi-alpha-l-circle', name: 'L' },
|
||||
{ icon: 'mdi mdi-alpha-m-circle', name: 'M' },
|
||||
{ icon: 'mdi mdi-alpha-n-circle', name: 'N' },
|
||||
{ icon: 'mdi mdi-alpha-o-circle', name: 'O' },
|
||||
{ icon: 'mdi mdi-alpha-p-circle', name: 'P' },
|
||||
{ icon: 'mdi mdi-alpha-q-circle', name: 'Q' },
|
||||
{ icon: 'mdi mdi-alpha-r-circle', name: 'R' },
|
||||
{ icon: 'mdi mdi-alpha-s-circle', name: 'S' },
|
||||
{ icon: 'mdi mdi-alpha-t-circle', name: 'T' },
|
||||
{ icon: 'mdi mdi-alpha-u-circle', name: 'U' },
|
||||
{ icon: 'mdi mdi-alpha-v-circle', name: 'V' },
|
||||
{ icon: 'mdi mdi-alpha-w-circle', name: 'W' },
|
||||
{ icon: 'mdi mdi-alpha-x-circle', name: 'X' },
|
||||
{ icon: 'mdi mdi-alpha-y-circle', name: 'Y' },
|
||||
{ icon: 'mdi mdi-alpha-z-circle', name: 'Z' },
|
||||
|
||||
// Numeric Icons
|
||||
{ icon: 'mdi mdi-numeric-0-circle', name: '0' },
|
||||
{ icon: 'mdi mdi-numeric-1-circle', name: '1' },
|
||||
{ icon: 'mdi mdi-numeric-2-circle', name: '2' },
|
||||
{ icon: 'mdi mdi-numeric-3-circle', name: '3' },
|
||||
{ icon: 'mdi mdi-numeric-4-circle', name: '4' },
|
||||
{ icon: 'mdi mdi-numeric-5-circle', name: '5' },
|
||||
{ icon: 'mdi mdi-numeric-6-circle', name: '6' },
|
||||
{ icon: 'mdi mdi-numeric-7-circle', name: '7' },
|
||||
{ icon: 'mdi mdi-numeric-8-circle', name: '8' },
|
||||
{ icon: 'mdi mdi-numeric-9-circle', name: '9' },
|
||||
{ icon: 'mdi mdi-numeric-10-circle', name: '10' },
|
||||
|
||||
// Alpha Outline Icons
|
||||
{ icon: 'mdi mdi-alpha-a-circle-outline', name: 'A Outline' },
|
||||
{ icon: 'mdi mdi-alpha-b-circle-outline', name: 'B Outline' },
|
||||
{ icon: 'mdi mdi-alpha-c-circle-outline', name: 'C Outline' },
|
||||
{ icon: 'mdi mdi-alpha-d-circle-outline', name: 'D Outline' },
|
||||
{ icon: 'mdi mdi-alpha-e-circle-outline', name: 'E Outline' },
|
||||
{ icon: 'mdi mdi-alpha-f-circle-outline', name: 'F Outline' },
|
||||
{ icon: 'mdi mdi-alpha-g-circle-outline', name: 'G Outline' },
|
||||
{ icon: 'mdi mdi-alpha-h-circle-outline', name: 'H Outline' },
|
||||
{ icon: 'mdi mdi-alpha-i-circle-outline', name: 'I Outline' },
|
||||
{ icon: 'mdi mdi-alpha-j-circle-outline', name: 'J Outline' },
|
||||
{ icon: 'mdi mdi-alpha-k-circle-outline', name: 'K Outline' },
|
||||
{ icon: 'mdi mdi-alpha-l-circle-outline', name: 'L Outline' },
|
||||
{ icon: 'mdi mdi-alpha-m-circle-outline', name: 'M Outline' },
|
||||
{ icon: 'mdi mdi-alpha-n-circle-outline', name: 'N Outline' },
|
||||
{ icon: 'mdi mdi-alpha-o-circle-outline', name: 'O Outline' },
|
||||
{ icon: 'mdi mdi-alpha-p-circle-outline', name: 'P Outline' },
|
||||
{ icon: 'mdi mdi-alpha-q-circle-outline', name: 'Q Outline' },
|
||||
{ icon: 'mdi mdi-alpha-r-circle-outline', name: 'R Outline' },
|
||||
{ icon: 'mdi mdi-alpha-s-circle-outline', name: 'S Outline' },
|
||||
{ icon: 'mdi mdi-alpha-t-circle-outline', name: 'T Outline' },
|
||||
{ icon: 'mdi mdi-alpha-u-circle-outline', name: 'U Outline' },
|
||||
{ icon: 'mdi mdi-alpha-v-circle-outline', name: 'V Outline' },
|
||||
{ icon: 'mdi mdi-alpha-w-circle-outline', name: 'W Outline' },
|
||||
{ icon: 'mdi mdi-alpha-x-circle-outline', name: 'X Outline' },
|
||||
{ icon: 'mdi mdi-alpha-y-circle-outline', name: 'Y Outline' },
|
||||
{ icon: 'mdi mdi-alpha-z-circle-outline', name: 'Z Outline' },
|
||||
|
||||
// Numeric Outline Icons
|
||||
{ icon: 'mdi mdi-numeric-0-circle-outline', name: '0 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-1-circle-outline', name: '1 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-2-circle-outline', name: '2 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-3-circle-outline', name: '3 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-4-circle-outline', name: '4 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-5-circle-outline', name: '5 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-6-circle-outline', name: '6 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-7-circle-outline', name: '7 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-8-circle-outline', name: '8 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-9-circle-outline', name: '9 Outline' },
|
||||
{ icon: 'mdi mdi-numeric-10-circle-outline', name: '10 Outline' },
|
||||
];
|
||||
|
||||
function selectIcon(iconName) {
|
||||
setFieldValue(name, iconName);
|
||||
showPicker = false;
|
||||
}
|
||||
|
||||
function togglePicker() {
|
||||
showPicker = !showPicker;
|
||||
}
|
||||
|
||||
function handleKeydown(event, action) {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
event.preventDefault();
|
||||
action();
|
||||
}
|
||||
}
|
||||
|
||||
$: iconValue = $values?.[name];
|
||||
</script>
|
||||
|
||||
<svelte:component this={template} type="select" {label} {...templateProps}>
|
||||
<div class="icon-field-container">
|
||||
<div
|
||||
class="selected-icon"
|
||||
on:click={togglePicker}
|
||||
on:keydown={e => handleKeydown(e, togglePicker)}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<FontIcon icon={iconValue || defaultIcon} />
|
||||
<span class="icon-name">{ICONS.find(icon => icon.icon === iconValue)?.name || '(Default icon)'}</span>
|
||||
<FontIcon icon="icon chevron-down" />
|
||||
</div>
|
||||
|
||||
{#if showPicker}
|
||||
<div class="icon-picker">
|
||||
<div class="icon-picker-header">
|
||||
<span>Choose an icon</span>
|
||||
<InlineButton on:click={togglePicker}>
|
||||
<FontIcon icon="icon close" />
|
||||
</InlineButton>
|
||||
</div>
|
||||
|
||||
<div class="icon-grid">
|
||||
{#each ICONS as { icon, name: iconDisplayName }}
|
||||
<div
|
||||
class="icon-option"
|
||||
class:selected={iconValue === icon}
|
||||
on:click={() => selectIcon(icon)}
|
||||
on:keydown={e => handleKeydown(e, () => selectIcon(icon))}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
title={iconDisplayName}
|
||||
>
|
||||
<FontIcon {icon} />
|
||||
<span class="icon-label">{iconDisplayName}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:component>
|
||||
|
||||
<style>
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
.icon-field-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.selected-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
background: var(--theme-bg-0);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.selected-icon:hover {
|
||||
border-color: var(--theme-border-hover);
|
||||
}
|
||||
|
||||
.selected-icon:focus {
|
||||
outline: none;
|
||||
border-color: var(--theme-font-link);
|
||||
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
|
||||
}
|
||||
|
||||
.icon-name {
|
||||
flex: 1;
|
||||
color: var(--theme-font-1);
|
||||
}
|
||||
|
||||
.icon-picker {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: var(--theme-bg-0);
|
||||
border: 1px solid var(--theme-border);
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
max-height: 400px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.icon-picker-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--theme-border);
|
||||
background: var(--theme-bg-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.icon-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
|
||||
gap: 1px;
|
||||
padding: 0.5rem;
|
||||
overflow-y: auto;
|
||||
max-height: 320px;
|
||||
}
|
||||
|
||||
.icon-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-option:hover {
|
||||
background: var(--theme-bg-hover);
|
||||
}
|
||||
|
||||
.icon-option.selected {
|
||||
background: var(--theme-bg-selected);
|
||||
color: var(--theme-font-link);
|
||||
}
|
||||
|
||||
.icon-option:focus {
|
||||
outline: none;
|
||||
background: var(--theme-bg-hover);
|
||||
box-shadow: 0 0 0 2px var(--theme-font-link-opacity);
|
||||
}
|
||||
|
||||
.icon-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--theme-font-2);
|
||||
line-height: 1.2;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.icon-option.selected .icon-label {
|
||||
color: var(--theme-font-link);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<TextField
|
||||
{...$$restProps}
|
||||
value={$values[name] ?? defaultValue}
|
||||
value={$values?.[name] ?? defaultValue}
|
||||
on:input={e => setFieldValue(name, e.target['value'])}
|
||||
on:input={e => {
|
||||
if (saveOnInput) {
|
||||
|
||||
@@ -1,48 +1,78 @@
|
||||
<script lang="ts">
|
||||
import _ from 'lodash';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { createEventDispatcher, tick } from 'svelte';
|
||||
|
||||
import SelectField from '../forms/SelectField.svelte';
|
||||
import { currentDatabase } from '../stores';
|
||||
import { filterAppsForDatabase } from '../utility/appTools';
|
||||
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getConnectionInfo, useAllApps, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
|
||||
import InlineButton from '../buttons/InlineButton.svelte';
|
||||
import FontIcon from '../icons/FontIcon.svelte';
|
||||
import { showModal } from '../modals/modalTools';
|
||||
import InputTextModal from '../modals/InputTextModal.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
export let value = '#new';
|
||||
export let disableInitialize = false;
|
||||
export let value = '';
|
||||
export let conid;
|
||||
export let database;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let selectFieldKey = 0;
|
||||
|
||||
$: appFolders = useAppFolders();
|
||||
$: usedApps = useUsedApps();
|
||||
$: dbInfo = useDatabaseInfo({ conid, database });
|
||||
$: connectionInfo = useConnectionInfo({ conid });
|
||||
|
||||
$: {
|
||||
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;
|
||||
$: allApps = useAllApps();
|
||||
$: apps = filterAppsForDatabase($connectionInfo, database, $allApps || [], $dbInfo);
|
||||
|
||||
$: if (apps?.length == 1) {
|
||||
value = apps[0].appid;
|
||||
selectFieldKey++;
|
||||
dispatch('change', value);
|
||||
}
|
||||
|
||||
async function handleAddNewApplication() {
|
||||
showModal(InputTextModal, {
|
||||
header: 'New application',
|
||||
label: 'Application name',
|
||||
value: _.startCase(database),
|
||||
onConfirm: async appName => {
|
||||
const newAppId = await apiCall('apps/create-app-from-db', {
|
||||
appName,
|
||||
server: $connectionInfo?.server,
|
||||
database,
|
||||
});
|
||||
await tick();
|
||||
value = newAppId;
|
||||
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,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
<div class="flex">
|
||||
{#key selectFieldKey}
|
||||
<SelectField
|
||||
isNative
|
||||
{...$$restProps}
|
||||
{value}
|
||||
on:change={e => {
|
||||
value = e.detail;
|
||||
dispatch('change', value);
|
||||
}}
|
||||
options={[
|
||||
{
|
||||
label: '(not selected)',
|
||||
value: '',
|
||||
},
|
||||
...(apps || []).map(app => ({
|
||||
label: app.applicationName,
|
||||
value: app.appid,
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
{/key}
|
||||
<InlineButton on:click={handleAddNewApplication} square>
|
||||
<FontIcon icon="icon plus-thick" padLeft padRight />
|
||||
</InlineButton>
|
||||
</div>
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
'icon send': 'mdi mdi-send',
|
||||
'icon regex': 'mdi mdi-regex',
|
||||
'icon list': 'mdi mdi-format-list-bulleted-triangle',
|
||||
'icon help': 'mdi mdi-help',
|
||||
|
||||
'icon window-restore': 'mdi mdi-window-restore',
|
||||
'icon window-maximize': 'mdi mdi-window-maximize',
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<script lang="ts">
|
||||
import FormProvider from '../forms/FormProvider.svelte';
|
||||
import _ from 'lodash';
|
||||
import FormSubmit from '../forms/FormSubmit.svelte';
|
||||
import FormStyledButton from '../buttons/FormStyledButton.svelte';
|
||||
import ModalBase from './ModalBase.svelte';
|
||||
import { closeCurrentModal } from './modalTools';
|
||||
import { useAppFolders, useConnectionList, useTableInfo, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { useAllApps, useConnectionList, useTableInfo } from '../utility/metadataLoaders';
|
||||
import TableControl from '../elements/TableControl.svelte';
|
||||
import TextField from '../forms/TextField.svelte';
|
||||
import FormTextField from '../forms/FormTextField.svelte';
|
||||
@@ -16,14 +15,12 @@
|
||||
checkDescriptionExpression,
|
||||
getDictionaryDescription,
|
||||
parseDelimitedColumnList,
|
||||
saveDictionaryDescription,
|
||||
} 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';
|
||||
import { apiCall } from '../utility/api';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -33,13 +30,12 @@
|
||||
|
||||
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: appFolders = useAppFolders();
|
||||
$: apps = useAllApps();
|
||||
$: connections = useConnectionList();
|
||||
|
||||
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
|
||||
|
||||
const values = writable({ targetApplication: '#new' } as any);
|
||||
const values = writable({ targetApplication: '' } as any);
|
||||
|
||||
function initValues(descriptionInfo) {
|
||||
$values = {
|
||||
@@ -52,28 +48,21 @@
|
||||
$: {
|
||||
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}>
|
||||
<ModalBase {...$$restProps}>
|
||||
<svelte:fragment slot="header">Define description</svelte:fragment>
|
||||
|
||||
<FormSelectField
|
||||
label="Target application (mandatory)"
|
||||
name="targetApplication"
|
||||
disableInitialize
|
||||
selectFieldComponent={TargetApplicationSelect}
|
||||
{conid}
|
||||
{database}
|
||||
/>
|
||||
|
||||
<div class="wrapper">
|
||||
<TableControl
|
||||
rows={$tableInfo?.columns || []}
|
||||
@@ -103,30 +92,34 @@
|
||||
|
||||
<FormTextField name="delimiter" label="Delimiter" />
|
||||
|
||||
<FormSelectField
|
||||
label="Target application"
|
||||
name="targetApplication"
|
||||
disableInitialize
|
||||
selectFieldComponent={TargetApplicationSelect}
|
||||
/>
|
||||
|
||||
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
|
||||
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value="OK"
|
||||
disabled={!checkDescriptionExpression($values?.columns, $tableInfo)}
|
||||
on:click={() => {
|
||||
disabled={!checkDescriptionExpression($values?.columns, $tableInfo) || !$values.targetApplication}
|
||||
on:click={async () => {
|
||||
closeCurrentModal();
|
||||
saveDictionaryDescription(
|
||||
$tableInfo,
|
||||
conid,
|
||||
database,
|
||||
$values.columns,
|
||||
$values.delimiter,
|
||||
$values.targetApplication
|
||||
);
|
||||
onConfirm();
|
||||
|
||||
const expression = $values.columns;
|
||||
await apiCall('apps/save-dictionary-description', {
|
||||
appid: $values.targetApplication,
|
||||
schemaName: $tableInfo.schemaName,
|
||||
pureName: $tableInfo.pureName,
|
||||
columns: parseDelimitedColumnList(expression),
|
||||
expression,
|
||||
delimiter: $values.delimiter,
|
||||
});
|
||||
|
||||
// saveDictionaryDescription(
|
||||
// $tableInfo,
|
||||
// conid,
|
||||
// database,
|
||||
// $values.columns,
|
||||
// $values.delimiter,
|
||||
// $values.targetApplication
|
||||
// );
|
||||
onConfirm?.();
|
||||
}}
|
||||
/>
|
||||
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { closeCurrentModal, showModal } from './modalTools';
|
||||
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
|
||||
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
|
||||
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders';
|
||||
import { getTableInfo, useAllApps, useConnectionList } from '../utility/metadataLoaders';
|
||||
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
|
||||
import { onMount } from 'svelte';
|
||||
import { dumpSqlSelect } from 'dbgate-sqltree';
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
let checkedKeys = [];
|
||||
|
||||
$: apps = useUsedApps();
|
||||
$: apps = useAllApps();
|
||||
$: connections = useConnectionList();
|
||||
|
||||
function defineDescription() {
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
import { onMount, tick } from 'svelte';
|
||||
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
|
||||
import { apiCall } from '../utility/api';
|
||||
import { saveDbToApp } from '../utility/appTools';
|
||||
// import { apiCall } from '../utility/api';
|
||||
// import { saveDbToApp } from '../utility/appTools';
|
||||
|
||||
export let conid;
|
||||
export let database;
|
||||
@@ -173,7 +174,7 @@
|
||||
<div class="row">
|
||||
<div class="label col-3">Target application</div>
|
||||
<div class="col-9">
|
||||
<TargetApplicationSelect bind:value={dstApp} />
|
||||
<TargetApplicationSelect bind:value={dstApp} {conid} {database} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -181,10 +182,10 @@
|
||||
<svelte:fragment slot="footer">
|
||||
<FormSubmit
|
||||
value={'Save'}
|
||||
disabled={!dstApp}
|
||||
on:click={async () => {
|
||||
const appFolder = await saveDbToApp(conid, database, dstApp);
|
||||
await apiCall('apps/save-virtual-reference', {
|
||||
appFolder,
|
||||
appid: dstApp,
|
||||
schemaName,
|
||||
pureName,
|
||||
refSchemaName,
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
import { apiCall, apiOff, apiOn } from '../utility/api';
|
||||
import createActivator, { getActiveComponent } from '../utility/createActivator';
|
||||
import useEffect from '../utility/useEffect';
|
||||
import { getSqlFrontMatter } from 'dbgate-tools';
|
||||
import yaml from 'js-yaml';
|
||||
import JslChart from '../charts/JslChart.svelte';
|
||||
import ToolStripButton from '../buttons/ToolStripButton.svelte';
|
||||
|
||||
export const activator = createActivator('QueryDataTab', true);
|
||||
|
||||
@@ -40,6 +44,8 @@
|
||||
let jslid;
|
||||
let loading = false;
|
||||
|
||||
$: frontMatter = getSqlFrontMatter(sql, yaml);
|
||||
|
||||
async function loadData(conid, database, sql) {
|
||||
const resp = await apiCall('sessions/execute-reader', {
|
||||
conid,
|
||||
@@ -96,17 +102,30 @@
|
||||
}
|
||||
}
|
||||
$: $effect;
|
||||
|
||||
$: selectedChart = frontMatter?.['selected-chart'];
|
||||
$: fixedChartDefinition = selectedChart && frontMatter ? frontMatter?.[`chart-${selectedChart}`] : null;
|
||||
</script>
|
||||
|
||||
<ToolStripContainer>
|
||||
{#if jslid}
|
||||
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
|
||||
{:else}
|
||||
{#if loading}
|
||||
<LoadingInfo message="Loading data..." />
|
||||
{:else if jslid}
|
||||
{#if fixedChartDefinition}
|
||||
<JslChart {jslid} fixedDefinition={fixedChartDefinition} />
|
||||
{:else}
|
||||
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
|
||||
{/if}
|
||||
{/if}
|
||||
<svelte:fragment slot="toolstrip">
|
||||
<ToolStripCommandButton command="dataGrid.refresh" />
|
||||
{#if fixedChartDefinition}
|
||||
<ToolStripButton on:click={handleRefresh} icon="icon refresh">Refresh</ToolStripButton>
|
||||
{:else}
|
||||
<ToolStripCommandButton command="dataGrid.refresh" />
|
||||
{/if}
|
||||
<ToolStripCommandButton command="queryData.stopLoading" />
|
||||
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
|
||||
{#if !fixedChartDefinition}
|
||||
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</ToolStripContainer>
|
||||
|
||||
@@ -1,33 +1,131 @@
|
||||
import type { ApplicationDefinition, StoredConnection } from 'dbgate-types';
|
||||
import type { ApplicationDefinition, DatabaseInfo, StoredConnection } from 'dbgate-types';
|
||||
import { apiCall } from '../utility/api';
|
||||
import _ from 'lodash';
|
||||
import { match } from 'fuzzy';
|
||||
import { getConnectionInfo } from './metadataLoaders';
|
||||
import openNewTab from './openNewTab';
|
||||
|
||||
export async function saveDbToApp(conid: string, database: string, app: string) {
|
||||
if (app == '#new') {
|
||||
const folder = await apiCall('apps/create-folder', { folder: database });
|
||||
// export async function saveDbToApp(conid: string, database: string, app: string) {
|
||||
// const connection = await getConnectionInfo({ conid });
|
||||
|
||||
await apiCall('connections/update-database', {
|
||||
conid,
|
||||
database,
|
||||
values: {
|
||||
[`useApp:${folder}`]: true,
|
||||
},
|
||||
});
|
||||
// if (app == '#new') {
|
||||
// const appJson = {
|
||||
// applicationName: _.startCase(database),
|
||||
// usageRules: [
|
||||
// {
|
||||
// serverHostsList: connection?.server ? [connection.server] : undefined,
|
||||
// databaseNamesList: [database],
|
||||
// conditionGroup: '1',
|
||||
// },
|
||||
// ],
|
||||
// };
|
||||
|
||||
return folder;
|
||||
// const file =
|
||||
|
||||
// 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[],
|
||||
dbinfo: DatabaseInfo = null
|
||||
): ApplicationDefinition[] {
|
||||
if (!apps) {
|
||||
return [];
|
||||
}
|
||||
|
||||
await apiCall('connections/update-database', {
|
||||
conid,
|
||||
database,
|
||||
values: {
|
||||
[`useApp:${app}`]: true,
|
||||
},
|
||||
// console.log('ALL APPS:', apps);
|
||||
// console.log('DB INFO:', dbinfo);
|
||||
// console.log('CONNECTION:', connection);
|
||||
// console.log('DATABASE:', database);
|
||||
return apps.filter(app => {
|
||||
const groupedConditions = _.groupBy(app.usageRules, rule => rule.conditionGroup || '1');
|
||||
for (const group of Object.values(groupedConditions)) {
|
||||
let groupMatch = true;
|
||||
for (const rule of group) {
|
||||
let ruleMatch = true;
|
||||
if (rule.serverHostsRegex) {
|
||||
const re = new RegExp(rule.serverHostsRegex);
|
||||
ruleMatch = ruleMatch && !!connection?.server && re.test(connection.server);
|
||||
}
|
||||
if (rule.serverHostsList) {
|
||||
ruleMatch = ruleMatch && !!connection?.server && rule.serverHostsList.includes(connection.server);
|
||||
}
|
||||
if (rule.databaseNamesRegex) {
|
||||
const re = new RegExp(rule.databaseNamesRegex);
|
||||
ruleMatch = ruleMatch && !!database && re.test(database);
|
||||
}
|
||||
if (rule.databaseNamesList) {
|
||||
ruleMatch = ruleMatch && !!database && rule.databaseNamesList.includes(database);
|
||||
}
|
||||
let matchedTables = dbinfo?.tables;
|
||||
if (rule.tableNamesRegex) {
|
||||
const re = new RegExp(rule.tableNamesRegex);
|
||||
matchedTables = dbinfo?.tables?.filter(table => !!table && re.test(table.pureName)) || [];
|
||||
ruleMatch = ruleMatch && matchedTables.length > 0;
|
||||
}
|
||||
if (rule.tableNamesList) {
|
||||
matchedTables =
|
||||
dbinfo?.tables?.filter(table => !!table && rule.tableNamesList.includes(table.pureName)) || [];
|
||||
ruleMatch = ruleMatch && matchedTables.length > 0;
|
||||
}
|
||||
if (rule.columnNamesRegex) {
|
||||
const re = new RegExp(rule.columnNamesRegex);
|
||||
ruleMatch =
|
||||
ruleMatch &&
|
||||
matchedTables.some(table => !!table?.columns?.some(column => !!column && re.test(column.columnName)));
|
||||
}
|
||||
if (rule.columnNamesList) {
|
||||
ruleMatch =
|
||||
ruleMatch &&
|
||||
matchedTables.some(
|
||||
table => !!table?.columns?.some(column => !!column && rule.columnNamesList.includes(column.columnName))
|
||||
);
|
||||
}
|
||||
groupMatch = groupMatch && ruleMatch;
|
||||
}
|
||||
if (groupMatch) return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
return app;
|
||||
// const db = (connection?.databases || []).find(x => x.name == database);
|
||||
// return apps?.filter(app => db && db[`useApp:${app.name}`]);
|
||||
}
|
||||
|
||||
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}`]);
|
||||
export async function openApplicationEditor(appid) {
|
||||
const dataContent = await apiCall('files/load', { folder: 'apps', file: appid, format: 'json' });
|
||||
openNewTab(
|
||||
{
|
||||
title: appid,
|
||||
icon: 'img app',
|
||||
tabComponent: 'AppEditorTab',
|
||||
props: {
|
||||
savedFile: appid,
|
||||
savedFolder: 'apps',
|
||||
savedFormat: 'json',
|
||||
},
|
||||
},
|
||||
{ editor: dataContent }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const cachedByKey = {};
|
||||
const cachedPromisesByKey = {};
|
||||
const cachedKeysByReloadTrigger = {};
|
||||
const subscriptionsByReloadTrigger = {};
|
||||
const subscriptionsByByCacheKeyPeek = {};
|
||||
const cacheGenerationByKey = {};
|
||||
|
||||
let cacheGeneration = 0;
|
||||
@@ -29,6 +30,7 @@ function cacheSet(cacheKey, value, reloadTrigger, generation) {
|
||||
addCacheKeyToReloadTrigger(cacheKey, reloadTrigger);
|
||||
delete cachedPromisesByKey[cacheKey];
|
||||
cacheGenerationByKey[cacheKey] = generation;
|
||||
dispatchCacheChangePeek(cacheKey);
|
||||
}
|
||||
|
||||
function cacheClean(reloadTrigger) {
|
||||
@@ -64,6 +66,10 @@ function getCacheGenerationForKey(cacheKey) {
|
||||
return cacheGenerationByKey[cacheKey] || 0;
|
||||
}
|
||||
|
||||
export function getCachedValue(cacheKey) {
|
||||
return cacheGet(cacheKey);
|
||||
}
|
||||
|
||||
export async function loadCachedValue(reloadTrigger, cacheKey, func) {
|
||||
const fromCache = cacheGet(cacheKey);
|
||||
if (fromCache) {
|
||||
@@ -107,12 +113,36 @@ export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHand
|
||||
x => x != reloadHandler
|
||||
);
|
||||
}
|
||||
if (subscriptionsByReloadTrigger[itemString].length == 0) {
|
||||
if (subscriptionsByReloadTrigger[itemString]?.length == 0) {
|
||||
delete subscriptionsByReloadTrigger[itemString];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function subscribeCachePeek(cacheKey, peekHandler) {
|
||||
if (!subscriptionsByByCacheKeyPeek[cacheKey]) {
|
||||
subscriptionsByByCacheKeyPeek[cacheKey] = [];
|
||||
}
|
||||
subscriptionsByByCacheKeyPeek[cacheKey].push(peekHandler);
|
||||
}
|
||||
|
||||
export function unsubscribeCachePeek(cacheKey, peekHandler) {
|
||||
if (subscriptionsByByCacheKeyPeek[cacheKey]) {
|
||||
subscriptionsByByCacheKeyPeek[cacheKey] = subscriptionsByByCacheKeyPeek[cacheKey].filter(x => x != peekHandler);
|
||||
}
|
||||
if (subscriptionsByByCacheKeyPeek[cacheKey]?.length == 0) {
|
||||
delete subscriptionsByByCacheKeyPeek[cacheKey];
|
||||
}
|
||||
}
|
||||
|
||||
function dispatchCacheChangePeek(cacheKey) {
|
||||
if (subscriptionsByByCacheKeyPeek[cacheKey]) {
|
||||
for (const handler of subscriptionsByByCacheKeyPeek[cacheKey]) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchCacheChange(reloadTrigger) {
|
||||
cacheClean(reloadTrigger);
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { DictionaryDescription } from 'dbgate-datalib';
|
||||
import type { ApplicationDefinition, TableInfo } from 'dbgate-types';
|
||||
import type { ApplicationDefinition, DatabaseInfo, TableInfo } from 'dbgate-types';
|
||||
import _ from 'lodash';
|
||||
import { apiCall } from './api';
|
||||
import { filterAppsForDatabase, saveDbToApp } from './appTools';
|
||||
import { filterAppsForDatabase } from './appTools';
|
||||
// import { filterAppsForDatabase, saveDbToApp } from './appTools';
|
||||
|
||||
function checkDescriptionColumns(columns: string[], table: TableInfo) {
|
||||
if (!columns?.length) return false;
|
||||
@@ -17,7 +18,8 @@ export function getDictionaryDescription(
|
||||
database: string,
|
||||
apps: ApplicationDefinition[],
|
||||
connections,
|
||||
skipCheckSaved: boolean = false
|
||||
skipCheckSaved: boolean = false,
|
||||
dbInfo: DatabaseInfo = null
|
||||
): DictionaryDescription {
|
||||
const conn = connections?.find(x => x._id == conid);
|
||||
|
||||
@@ -25,7 +27,7 @@ export function getDictionaryDescription(
|
||||
return null;
|
||||
}
|
||||
|
||||
const dbApps = filterAppsForDatabase(conn, database, apps);
|
||||
const dbApps = filterAppsForDatabase(conn, database, apps, dbInfo);
|
||||
|
||||
if (!dbApps) {
|
||||
return null;
|
||||
@@ -70,22 +72,20 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
|
||||
return parsed.join(',');
|
||||
}
|
||||
|
||||
export async function saveDictionaryDescription(
|
||||
table: TableInfo,
|
||||
conid: string,
|
||||
database: string,
|
||||
expression: string,
|
||||
delimiter: string,
|
||||
targetApplication: string
|
||||
) {
|
||||
const appFolder = await saveDbToApp(conid, database, targetApplication);
|
||||
|
||||
await apiCall('apps/save-dictionary-description', {
|
||||
appFolder,
|
||||
schemaName: table.schemaName,
|
||||
pureName: table.pureName,
|
||||
columns: parseDelimitedColumnList(expression),
|
||||
expression,
|
||||
delimiter,
|
||||
});
|
||||
}
|
||||
// export async function saveDictionaryDescription(
|
||||
// table: TableInfo,
|
||||
// conid: string,
|
||||
// database: string,
|
||||
// expression: string,
|
||||
// delimiter: string,
|
||||
// targetApplication: string
|
||||
// ) {
|
||||
// await apiCall('apps/save-dictionary-description', {
|
||||
// appFolder,
|
||||
// schemaName: table.schemaName,
|
||||
// pureName: table.pureName,
|
||||
// columns: parseDelimitedColumnList(expression),
|
||||
// expression,
|
||||
// delimiter,
|
||||
// });
|
||||
// }
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
import _ from 'lodash';
|
||||
import { loadCachedValue, subscribeCacheChange, unsubscribeCacheChange } from './cache';
|
||||
import {
|
||||
getCachedValue,
|
||||
loadCachedValue,
|
||||
subscribeCacheChange,
|
||||
subscribeCachePeek,
|
||||
unsubscribeCacheChange,
|
||||
unsubscribeCachePeek,
|
||||
} from './cache';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { derived } from 'svelte/store';
|
||||
import { extendDatabaseInfo } from 'dbgate-tools';
|
||||
@@ -107,17 +114,17 @@ const archiveFilesLoader = ({ folder }) => ({
|
||||
reloadTrigger: { key: `archive-files-changed`, folder },
|
||||
});
|
||||
|
||||
const appFoldersLoader = () => ({
|
||||
url: 'apps/folders',
|
||||
params: {},
|
||||
reloadTrigger: { key: `app-folders-changed` },
|
||||
});
|
||||
// const appFoldersLoader = () => ({
|
||||
// url: 'apps/folders',
|
||||
// params: {},
|
||||
// reloadTrigger: { key: `app-folders-changed` },
|
||||
// });
|
||||
|
||||
const appFilesLoader = ({ folder }) => ({
|
||||
url: 'apps/files',
|
||||
params: { folder },
|
||||
reloadTrigger: { key: `app-files-changed`, app: folder },
|
||||
});
|
||||
// const appFilesLoader = ({ folder }) => ({
|
||||
// url: 'apps/files',
|
||||
// params: { folder },
|
||||
// reloadTrigger: { key: `app-files-changed`, app: folder },
|
||||
// });
|
||||
|
||||
// const dbAppsLoader = ({ conid, database }) => ({
|
||||
// url: 'apps/get-apps-for-db',
|
||||
@@ -125,10 +132,10 @@ const appFilesLoader = ({ folder }) => ({
|
||||
// reloadTrigger: `db-apps-changed-${conid}-${database}`,
|
||||
// });
|
||||
|
||||
const usedAppsLoader = ({ conid, database }) => ({
|
||||
url: 'apps/get-used-apps',
|
||||
const allAppsLoader = () => ({
|
||||
url: 'apps/get-all-apps',
|
||||
params: {},
|
||||
reloadTrigger: { key: `used-apps-changed` },
|
||||
reloadTrigger: { key: `files-changed`, folder: 'apps' },
|
||||
});
|
||||
|
||||
const serverStatusLoader = () => ({
|
||||
@@ -227,6 +234,37 @@ function useCore(loader, args) {
|
||||
};
|
||||
}
|
||||
|
||||
function useCorePeek(loader, args) {
|
||||
const { url, params, reloadTrigger, transform, onLoaded } = loader(args);
|
||||
const cacheKey = stableStringify({ url, ...params });
|
||||
let openedCount = 0;
|
||||
|
||||
return {
|
||||
subscribe: onChange => {
|
||||
async function handlePeek() {
|
||||
const res = getCachedValue(cacheKey);
|
||||
if (openedCount > 0) {
|
||||
onChange(res);
|
||||
}
|
||||
}
|
||||
openedCount += 1;
|
||||
handlePeek();
|
||||
|
||||
if (reloadTrigger) {
|
||||
subscribeCachePeek(cacheKey, handlePeek);
|
||||
return () => {
|
||||
openedCount -= 1;
|
||||
unsubscribeCachePeek(cacheKey, handlePeek);
|
||||
};
|
||||
} else {
|
||||
return () => {
|
||||
openedCount -= 1;
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @returns {Promise<import('dbgate-types').DatabaseInfo>} */
|
||||
export function getDatabaseInfo(args) {
|
||||
return getCore(databaseInfoLoader, args);
|
||||
@@ -237,6 +275,10 @@ export function useDatabaseInfo(args) {
|
||||
return useCore(databaseInfoLoader, args);
|
||||
}
|
||||
|
||||
export function useDatabaseInfoPeek(args) {
|
||||
return useCorePeek(databaseInfoLoader, args);
|
||||
}
|
||||
|
||||
export async function getDbCore(args, objectTypeField = undefined) {
|
||||
const db = await getDatabaseInfo(args);
|
||||
if (!db) return null;
|
||||
@@ -392,25 +434,25 @@ 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 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 getAppFolders(args = {}) {
|
||||
// return getCore(appFoldersLoader, args);
|
||||
// }
|
||||
// export function useAppFolders(args = {}) {
|
||||
// return useCore(appFoldersLoader, args);
|
||||
// }
|
||||
|
||||
export function getUsedApps(args = {}) {
|
||||
return getCore(usedAppsLoader, args);
|
||||
export function getAllApps(args = {}) {
|
||||
return getCore(allAppsLoader, args);
|
||||
}
|
||||
export function useUsedApps(args = {}) {
|
||||
return useCore(usedAppsLoader, args);
|
||||
export function useAllApps(args = {}) {
|
||||
return useCore(allAppsLoader, args);
|
||||
}
|
||||
|
||||
// export function getDbApps(args = {}) {
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
<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 '../buttons/CloseSearchButton.svelte';
|
||||
import DropDownButton from '../buttons/DropDownButton.svelte';
|
||||
|
||||
import InlineButton from '../buttons/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 { useAppFiles, useArchiveFolders } from '../utility/metadataLoaders';
|
||||
import openNewTab from '../utility/openNewTab';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import { showSnackbarError } from '../utility/snackbar';
|
||||
|
||||
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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleNewConfigFile(fileName, content) {
|
||||
if (!(await apiCall('apps/create-config-file', { fileName, content, appFolder: $currentApplication }))) {
|
||||
showSnackbarError('File not created, probably already exists');
|
||||
}
|
||||
}
|
||||
|
||||
function createAddMenu() {
|
||||
return [
|
||||
{
|
||||
text: 'New SQL command',
|
||||
onClick: () => handleNewSqlFile('command.sql', 'Create new SQL command', COMMAND_TEMPLATE),
|
||||
},
|
||||
{
|
||||
text: 'New SQL query',
|
||||
onClick: () => handleNewSqlFile('query.sql', 'Create new SQL query', QUERY_TEMPLATE),
|
||||
},
|
||||
{
|
||||
text: 'New virtual references file',
|
||||
onClick: () => handleNewConfigFile('virtual-references.config.json', []),
|
||||
},
|
||||
{
|
||||
text: 'New dictionary descriptions file',
|
||||
onClick: () => handleNewConfigFile('dictionary-descriptions.config.json', []),
|
||||
},
|
||||
|
||||
// { 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>
|
||||
@@ -1,39 +0,0 @@
|
||||
<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 '../buttons/CloseSearchButton.svelte';
|
||||
|
||||
import InlineButton from '../buttons/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>
|
||||
@@ -1,19 +0,0 @@
|
||||
<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>
|
||||
@@ -12,6 +12,7 @@
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import { isProApp } from '../utility/proTools';
|
||||
import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
|
||||
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
|
||||
|
||||
let filter = '';
|
||||
|
||||
@@ -27,6 +28,7 @@
|
||||
const dbCompareJobFiles = useFiles({ folder: 'dbcompare' });
|
||||
const perspectiveFiles = useFiles({ folder: 'perspectives' });
|
||||
const modelTransformFiles = useFiles({ folder: 'modtrans' });
|
||||
const appFiles = useFiles({ folder: 'apps' });
|
||||
|
||||
$: files = [
|
||||
...($sqlFiles || []),
|
||||
@@ -41,32 +43,18 @@
|
||||
...($modelTransformFiles || []),
|
||||
...((isProApp() && $dataDeployJobFiles) || []),
|
||||
...((isProApp() && $dbCompareJobFiles) || []),
|
||||
...((isProApp() && $appFiles) || []),
|
||||
];
|
||||
|
||||
function handleRefreshFiles() {
|
||||
apiCall('files/refresh', {
|
||||
folders: [
|
||||
'sql',
|
||||
'shell',
|
||||
'markdown',
|
||||
'charts',
|
||||
'query',
|
||||
'sqlite',
|
||||
'diagrams',
|
||||
'perspectives',
|
||||
'impexp',
|
||||
'modtrans',
|
||||
'datadeploy',
|
||||
'dbcompare',
|
||||
],
|
||||
folders: DATA_FOLDER_NAMES.map(folder => folder.name),
|
||||
});
|
||||
}
|
||||
|
||||
function dataFolderTitle(folder) {
|
||||
if (folder == 'modtrans') return 'Model transforms';
|
||||
if (folder == 'datadeploy') return 'Data deploy jobs';
|
||||
if (folder == 'dbcompare') return 'Database compare jobs';
|
||||
return _.startCase(folder);
|
||||
const foundFolder = DATA_FOLDER_NAMES.find(f => f.name === folder);
|
||||
return foundFolder ? foundFolder.label : _.startCase(folder);
|
||||
}
|
||||
|
||||
async function handleUploadedFile(filePath, fileName) {
|
||||
|
||||
@@ -17,11 +17,11 @@
|
||||
import SearchInput from '../elements/SearchInput.svelte';
|
||||
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
|
||||
import {
|
||||
useAllApps,
|
||||
useConnectionInfo,
|
||||
useDatabaseInfo,
|
||||
useDatabaseStatus,
|
||||
useSchemaList,
|
||||
useUsedApps,
|
||||
} from '../utility/metadataLoaders';
|
||||
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
|
||||
import AppObjectList from '../appobj/AppObjectList.svelte';
|
||||
@@ -73,9 +73,8 @@
|
||||
$: connection = useConnectionInfo({ conid });
|
||||
$: driver = findEngineDriver($connection, $extensions);
|
||||
|
||||
$: apps = useUsedApps();
|
||||
|
||||
$: dbApps = filterAppsForDatabase($currentDatabase?.connection, $currentDatabase?.name, $apps || []);
|
||||
$: apps = useAllApps();
|
||||
$: appsForDb = filterAppsForDatabase($connection, database, $apps || [], $objects);
|
||||
|
||||
// $: console.log('OBJECTS', $objects);
|
||||
|
||||
@@ -87,13 +86,14 @@
|
||||
['schemaName', 'pureName']
|
||||
)
|
||||
),
|
||||
...dbApps.map(app =>
|
||||
app.queries.map(query => ({
|
||||
objectTypeField: 'queries',
|
||||
pureName: query.name,
|
||||
schemaName: app.name,
|
||||
sql: query.sql,
|
||||
}))
|
||||
...appsForDb.map(app =>
|
||||
Object.values(app.files || {})
|
||||
.filter(x => x.type == 'query')
|
||||
.map(query => ({
|
||||
objectTypeField: 'queries',
|
||||
pureName: query.label,
|
||||
sql: query.sql,
|
||||
}))
|
||||
),
|
||||
]);
|
||||
|
||||
@@ -281,7 +281,7 @@
|
||||
>
|
||||
<AppObjectList
|
||||
list={objectList
|
||||
.filter(x => ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
|
||||
.filter(x => x.schemaName == null || ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
|
||||
.map(x => ({ ...x, conid, database }))}
|
||||
module={databaseObjectAppObject}
|
||||
groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import PluginsWidget from './PluginsWidget.svelte';
|
||||
import CellDataWidget from './CellDataWidget.svelte';
|
||||
import HistoryWidget from './HistoryWidget.svelte';
|
||||
import AppWidget from './AppWidget.svelte';
|
||||
import AdminMenuWidget from './AdminMenuWidget.svelte';
|
||||
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
|
||||
import PublicCloudWidget from './PublicCloudWidget.svelte';
|
||||
@@ -14,8 +13,9 @@
|
||||
import hasPermission from '../utility/hasPermission';
|
||||
</script>
|
||||
|
||||
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
|
||||
|
||||
{#if hasPermission('widgets/database')}
|
||||
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
|
||||
{/if}
|
||||
{#if $visibleSelectedWidget == 'file' && hasPermission('widgets/file')}
|
||||
<FilesWidget />
|
||||
{/if}
|
||||
@@ -31,9 +31,6 @@
|
||||
{#if $visibleSelectedWidget == 'cell-data' && hasPermission('widgets/cell-data')}
|
||||
<CellDataWidget />
|
||||
{/if}
|
||||
{#if $visibleSelectedWidget == 'app' && hasPermission('widgets/app')}
|
||||
<AppWidget />
|
||||
{/if}
|
||||
{#if $visibleSelectedWidget == 'admin' && hasPermission('widgets/admin')}
|
||||
<AdminMenuWidget />
|
||||
{/if}
|
||||
|
||||
@@ -110,13 +110,6 @@
|
||||
hasPermission('settings/change') && { command: 'settings.show' },
|
||||
{ command: 'theme.changeTheme' },
|
||||
hasPermission('settings/change') && { command: 'settings.commands' },
|
||||
hasPermission('widgets/app') && {
|
||||
text: 'View applications',
|
||||
onClick: () => {
|
||||
$selectedWidget = 'app';
|
||||
$visibleWidgetSideBar = true;
|
||||
},
|
||||
},
|
||||
hasPermission('widgets/plugins') && {
|
||||
text: 'Manage plugins',
|
||||
onClick: () => {
|
||||
|
||||
Reference in New Issue
Block a user