SYNC: Merge pull request #9 from dbgate/feature/apps

This commit is contained in:
Jan Prochazka
2025-09-11 13:10:36 +02:00
committed by Diflow
parent ef15f299d2
commit 11a4f0ef32
40 changed files with 1770 additions and 754 deletions

View File

@@ -10,6 +10,7 @@ module.exports = ({ editMenu, isMac }) => [
{ command: 'new.queryDesign', hideDisabled: true }, { command: 'new.queryDesign', hideDisabled: true },
{ command: 'new.diagram', hideDisabled: true }, { command: 'new.diagram', hideDisabled: true },
{ command: 'new.perspective', hideDisabled: true }, { command: 'new.perspective', hideDisabled: true },
{ command: 'new.application', hideDisabled: true },
{ command: 'new.shell', hideDisabled: true }, { command: 'new.shell', hideDisabled: true },
{ command: 'new.jsonl', hideDisabled: true }, { command: 'new.jsonl', hideDisabled: true },
{ command: 'new.modelTransform', hideDisabled: true }, { command: 'new.modelTransform', hideDisabled: true },

View File

@@ -55,6 +55,10 @@ class AuthProviderBase {
return []; return [];
} }
async getCurrentFilePermissions(req) {
return [];
}
getLoginPageConnections() { getLoginPageConnections() {
return null; return null;
} }

View File

@@ -1,233 +1,98 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const _ = require('lodash'); const _ = require('lodash');
const path = require('path'); const path = require('path');
const { appdir } = require('../utility/directories'); const { appdir, filesdir } = require('../utility/directories');
const socket = require('../utility/socket'); const socket = require('../utility/socket');
const connections = require('./connections'); const connections = require('./connections');
const {
loadPermissionsFromRequest,
loadFilePermissionsFromRequest,
hasPermission,
getFilePermissionRole,
} = require('../utility/hasPermission');
module.exports = { module.exports = {
folders_meta: true, getAllApps_meta: true,
async folders() { async getAllApps({}, req) {
const folders = await fs.readdir(appdir()); const dir = path.join(filesdir(), 'apps');
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();
const res = []; const res = [];
const loadedPermissions = await loadPermissionsFromRequest(req);
const filePermissions = await loadFilePermissionsFromRequest(req);
for (const folder of apps) { for (const file of await fs.readdir(dir)) {
res.push(await this.loadApp({ folder })); if (!hasPermission(`all-files`, loadedPermissions)) {
const role = getFilePermissionRole('apps', file, filePermissions);
if (role == 'deny') continue;
} }
return res; const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
}, const appJson = JSON.parse(content);
// const app = {
// getAppsForDb_meta: true, // appid: file,
// async getAppsForDb({ conid, database }) { // name: appJson.applicationName,
// const connection = await connections.get({ conid }); // usageRules: appJson.usageRules || [],
// if (!connection) return []; // icon: appJson.applicationIcon || 'img app',
// const db = (connection.databases || []).find(x => x.name == database); // color: appJson.applicationColor,
// const apps = []; // queries: Object.values(appJson.files || {})
// const res = []; // .filter(x => x.type == 'query')
// if (db) { // .map(x => ({
// for (const key of _.keys(db || {})) { // name: x.label,
// if (key.startsWith('useApp:') && db[key]) { // sql: x.sql,
// apps.push(key.substring('useApp:'.length)); // })),
// } // commands: Object.values(appJson.files || {})
// } // .filter(x => x.type == 'command')
// } // .map(x => ({
// for (const folder of apps) { // name: x.label,
// res.push(await this.loadApp({ folder })); // sql: x.sql,
// } // })),
// return res; // virtualReferences: appJson.virtualReferences,
// }, // dictionaryDescriptions: appJson.dictionaryDescriptions,
// };
loadApp_meta: true, const app = {
async loadApp({ folder }) { ...appJson,
const res = { appid: file,
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) { res.push(app);
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; return res;
}, },
async saveConfigFile(appFolder, filename, filterFunc, newItem) { createAppFromDb_meta: true,
const file = path.join(appdir(), appFolder, filename); async createAppFromDb({ appName, server, database }, req) {
const appdir = path.join(filesdir(), 'apps');
let json; if (!fs.existsSync(appdir)) {
try { await fs.mkdir(appdir);
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
} catch (err) {
json = [];
} }
const appId = _.kebabCase(appName);
if (filterFunc) { let suffix = undefined;
json = json.filter(filterFunc); 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(`files-changed`, { folder: 'apps' });
socket.emitChanged('used-apps-changed');
return finalAppId;
}, },
saveVirtualReference_meta: true, saveVirtualReference_meta: true,
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) { async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
await this.saveConfigFile( await this.saveConfigItem(
appFolder, appid,
'virtual-references.config.json', 'virtualReferences',
columns.length == 1 columns.length == 1
? x => ? x =>
!( !(
@@ -245,14 +110,17 @@ module.exports = {
columns, columns,
} }
); );
socket.emitChanged(`files-changed`, { folder: 'apps' });
return true; return true;
}, },
saveDictionaryDescription_meta: true, saveDictionaryDescription_meta: true,
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) { async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
await this.saveConfigFile( await this.saveConfigItem(
appFolder, appid,
'dictionary-descriptions.config.json', 'dictionaryDescriptions',
x => !(x.schemaName == schemaName && x.pureName == pureName), x => !(x.schemaName == schemaName && x.pureName == pureName),
{ {
schemaName, schemaName,
@@ -263,18 +131,271 @@ module.exports = {
} }
); );
socket.emitChanged(`files-changed`, { folder: 'apps' });
return true; return true;
}, },
createConfigFile_meta: true, async saveConfigItem(appid, fieldName, filterFunc, newItem) {
async createConfigFile({ appFolder, fileName, content }) { const file = path.join(filesdir(), 'apps', appid);
const file = path.join(appdir(), appFolder, fileName);
if (!(await fs.exists(file))) { const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
await fs.writeFile(file, JSON.stringify(content, undefined, 2)); let json = appJson[fieldName] || [];
socket.emitChanged('app-files-changed', { app: appFolder });
socket.emitChanged('used-apps-changed'); if (filterFunc) {
return true; 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;
// },
}; };

View File

@@ -3,7 +3,12 @@ const path = require('path');
const crypto = require('crypto'); const crypto = require('crypto');
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories'); const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport'); 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 socket = require('../utility/socket');
const scheduler = require('./scheduler'); const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport'); const getDiagramExport = require('../utility/getDiagramExport');

View File

@@ -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", "pureName": "role_connections",
"columns": [ "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", "pureName": "role_permissions",
"columns": [ "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", "pureName": "table_permission_roles",
"columns": [ "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", "pureName": "user_connections",
"columns": [ "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", "pureName": "user_permissions",
"columns": [ "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": [], "collections": [],

View File

@@ -85,6 +85,16 @@ async function loadTablePermissionsFromRequest(req) {
return tablePermissions; 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) { function matchDatabasePermissionRow(conid, database, permissionRow) {
if (permissionRow.connection_id) { if (permissionRow.connection_id) {
if (conid != permissionRow.connection_id) { if (conid != permissionRow.connection_id) {
@@ -135,6 +145,27 @@ function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissi
return true; 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 = { const DATABASE_ROLE_ID_NAMES = {
'-1': 'view', '-1': 'view',
'-2': 'read_content', '-2': 'read_content',
@@ -143,6 +174,11 @@ const DATABASE_ROLE_ID_NAMES = {
'-5': 'deny', '-5': 'deny',
}; };
const FILE_ROLE_ID_NAMES = {
'-1': 'allow',
'-2': 'deny',
};
function getDatabaseRoleLevelIndex(roleName) { function getDatabaseRoleLevelIndex(roleName) {
if (!roleName) { if (!roleName) {
return 6; return 6;
@@ -198,6 +234,17 @@ function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
return res; 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 = { const TABLE_ROLE_ID_NAMES = {
'-1': 'read', '-1': 'read',
'-2': 'update_only', '-2': 'update_only',
@@ -308,8 +355,10 @@ module.exports = {
loadPermissionsFromRequest, loadPermissionsFromRequest,
loadDatabasePermissionsFromRequest, loadDatabasePermissionsFromRequest,
loadTablePermissionsFromRequest, loadTablePermissionsFromRequest,
loadFilePermissionsFromRequest,
getDatabasePermissionRole, getDatabasePermissionRole,
getTablePermissionRole, getTablePermissionRole,
getFilePermissionRole,
testStandardPermission, testStandardPermission,
testDatabaseRolePermission, testDatabaseRolePermission,
getTablePermissionRoleLevelIndex, getTablePermissionRoleLevelIndex,

View File

@@ -111,3 +111,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
} }
return res; 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' },
];

View File

@@ -1,12 +1,12 @@
interface ApplicationCommand { // interface ApplicationCommand {
name: string; // name: string;
sql: string; // sql: string;
} // }
interface ApplicationQuery { // interface ApplicationQuery {
name: string; // name: string;
sql: string; // sql: string;
} // }
interface VirtualReferenceDefinition { interface VirtualReferenceDefinition {
pureName: string; pureName: string;
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
delimiter: string; delimiter: string;
} }
export interface ApplicationDefinition { interface ApplicationUsageRule {
name: string; conditionGroup?: string;
serverHostsRegex?: string;
queries: ApplicationQuery[]; serverHostsList?: string[];
commands: ApplicationCommand[]; databaseNamesRegex?: string;
virtualReferences: VirtualReferenceDefinition[]; databaseNamesList?: string[];
dictionaryDescriptions: DictionaryDescriptionDefinition[]; 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[];
} }

View File

@@ -20,7 +20,7 @@
installNewVolatileConnectionListener, installNewVolatileConnectionListener,
refreshPublicCloudFiles, refreshPublicCloudFiles,
} from './utility/api'; } from './utility/api';
import { getConfig, getSettings, getUsedApps } from './utility/metadataLoaders'; import { getAllApps, getConfig, getSettings } from './utility/metadataLoaders';
import AppTitleProvider from './utility/AppTitleProvider.svelte'; import AppTitleProvider from './utility/AppTitleProvider.svelte';
import getElectron from './utility/getElectron'; import getElectron from './utility/getElectron';
import AppStartInfo from './widgets/AppStartInfo.svelte'; import AppStartInfo from './widgets/AppStartInfo.svelte';
@@ -49,7 +49,7 @@
const connections = await apiCall('connections/list'); const connections = await apiCall('connections/list');
const settings = await getSettings(); const settings = await getSettings();
const apps = await getUsedApps(); const apps = await getAllApps();
const loadedApiValue = !!(settings && connections && config && apps); const loadedApiValue = !!(settings && connections && config && apps);
if (loadedApiValue) { if (loadedApiValue) {

View 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>

View File

@@ -36,6 +36,7 @@
export let filter = null; export let filter = null;
export let disableHover = false; export let disableHover = false;
export let divProps = {}; export let divProps = {};
export let additionalIcons = null;
$: isChecked = $: isChecked =
checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x)); checkedObjectsStore && $checkedObjectsStore.find(x => module?.extractKey(data) == module?.extractKey(x));
@@ -160,6 +161,11 @@
/> />
</span> </span>
{/if} {/if}
{#if additionalIcons}
{#each additionalIcons as ic}
<FontIcon icon={ic.icon} title={ic.title} colorClass={ic.colorClass} />
{/each}
{/if}
{#if extInfo} {#if extInfo}
<span class="ext-info"> <span class="ext-info">
<TokenizedFilteredText text={extInfo} {filter} /> <TokenizedFilteredText text={extInfo} {filter} />

View File

@@ -130,7 +130,7 @@
import openNewTab from '../utility/openNewTab'; import openNewTab from '../utility/openNewTab';
import { getDatabaseMenuItems } from './DatabaseAppObject.svelte'; import { getDatabaseMenuItems } from './DatabaseAppObject.svelte';
import getElectron from '../utility/getElectron'; import getElectron from '../utility/getElectron';
import { getDatabaseList, useUsedApps } from '../utility/metadataLoaders'; import { getDatabaseList, useAllApps } from '../utility/metadataLoaders';
import { getLocalStorage } from '../utility/storageCache'; import { getLocalStorage } from '../utility/storageCache';
import { apiCall, removeVolatileMapping } from '../utility/api'; import { apiCall, removeVolatileMapping } from '../utility/api';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte'; import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
@@ -383,7 +383,7 @@
$currentDatabase, $currentDatabase,
$apps, $apps,
$openedSingleDatabaseConnections, $openedSingleDatabaseConnections,
data.databasePermissionRole, data.databasePermissionRole
), ),
], ],
@@ -427,7 +427,7 @@
} }
} }
$: apps = useUsedApps(); $: apps = useAllApps();
</script> </script>
<AppObjectCore <AppObjectCore

View File

@@ -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 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 = const isSqlOrDoc =
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document'); driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
@@ -564,11 +580,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }), 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 }, { divider: true },
commands.length > 0 && [ commands.length > 0 && [
commands.map((cmd: any) => ({ commands.map((cmd: any) => ({
text: cmd.name, text: cmd.label,
onClick: () => { onClick: () => {
showModal(ConfirmSqlModal, { showModal(ConfirmSqlModal, {
sql: cmd.sql, sql: cmd.sql,
@@ -618,12 +649,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
getConnectionLabel, getConnectionLabel,
} from 'dbgate-tools'; } from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte'; 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 { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api'; import { apiCall } from '../utility/api';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte'; import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.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 newQuery from '../query/newQuery';
import ConfirmModal from '../modals/ConfirmModal.svelte'; import ConfirmModal from '../modals/ConfirmModal.svelte';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte'; import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
@@ -639,7 +670,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
import { getNumberIcon } from '../icons/FontIcon.svelte'; import { getNumberIcon } from '../icons/FontIcon.svelte';
import { getDatabaseClickActionSetting } from '../settings/settingsTools'; import { getDatabaseClickActionSetting } from '../settings/settingsTools';
import { _t } from '../translations'; import { _t } from '../translations';
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte'; import { tick } from 'svelte';
export let data; export let data;
export let passProps; 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); $: 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}`]; $: 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> </script>
<AppObjectCore <AppObjectCore
@@ -681,6 +717,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
switchCurrentDatabase(data); 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={() => { on:mousedown={() => {
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection }; $focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
}} }}

View File

@@ -45,16 +45,16 @@
schedulerEvents: 'icon scheduler-event', schedulerEvents: 'icon scheduler-event',
}; };
const defaultTabs = { // const defaultTabs = {
tables: 'TableDataTab', // tables: 'TableDataTab',
collections: 'CollectionDataTab', // collections: 'CollectionDataTab',
views: 'ViewDataTab', // views: 'ViewDataTab',
matviews: 'ViewDataTab', // matviews: 'ViewDataTab',
queries: 'QueryDataTab', // queries: 'QueryDataTab',
procedures: 'SqlObjectTab', // procedures: 'SqlObjectTab',
functions: 'SqlObjectTab', // functions: 'SqlObjectTab',
triggers: 'SqlObjectTab', // triggers: 'SqlObjectTab',
}; // };
function createScriptTemplatesSubmenu(objectTypeField) { function createScriptTemplatesSubmenu(objectTypeField) {
return { return {
@@ -741,7 +741,7 @@
export async function openDatabaseObjectDetail( export async function openDatabaseObjectDetail(
tabComponent, tabComponent,
scriptTemplate, scriptTemplate,
{ schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode }, { schemaName, pureName, conid, database, objectTypeField, defaultActionId, isRawMode, sql },
forceNewTab?, forceNewTab?,
initialData?, initialData?,
icon?, icon?,
@@ -776,6 +776,7 @@
initialArgs: scriptTemplate ? { scriptTemplate } : null, initialArgs: scriptTemplate ? { scriptTemplate } : null,
defaultActionId, defaultActionId,
isRawMode, isRawMode,
sql,
}, },
}, },
initialData, initialData,
@@ -797,7 +798,7 @@
data, data,
{ forceNewTab = false, tabPreviewMode = false, focusTab = false } = {} { 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 driver = findEngineDriver(data, getExtensions());
const activeTab = getActiveTab(); const activeTab = getActiveTab();
@@ -843,6 +844,7 @@
objectTypeField, objectTypeField,
defaultActionId: prefferedAction.defaultActionId, defaultActionId: prefferedAction.defaultActionId,
isRawMode: prefferedAction?.isRawMode ?? false, isRawMode: prefferedAction?.isRawMode ?? false,
sql,
}, },
forceNewTab, forceNewTab,
prefferedAction?.initialData, prefferedAction?.initialData,

View File

@@ -142,6 +142,18 @@
label: 'Model transform file', 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 = { export const SAVED_FILE_HANDLERS = {
sql, sql,
shell, shell,
@@ -154,6 +166,7 @@
modtrans, modtrans,
datadeploy, datadeploy,
dbcompare, dbcompare,
apps,
}; };
export const extractKey = data => data.file; export const extractKey = data => data.file;

View File

@@ -100,4 +100,12 @@ export const defaultDatabaseObjectAppObjectActions = {
icon: 'img sql-file', icon: 'img sql-file',
}, },
], ],
queries: [
{
label: 'Show query',
tab: 'QueryDataTab',
defaultActionId: 'showAppQuery',
icon: 'img app-query',
},
],
}; };

View File

@@ -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({ registerCommand({
id: 'new.diagram', id: 'new.diagram',
category: 'New', category: 'New',
@@ -297,22 +314,22 @@ registerCommand({
}, },
}); });
registerCommand({ // registerCommand({
id: 'new.application', // id: 'new.application',
category: 'New', // category: 'New',
icon: 'img app', // icon: 'img app',
name: 'Application', // name: 'Application',
onClick: () => { // onClick: () => {
showModal(InputTextModal, { // showModal(InputTextModal, {
value: '', // value: '',
label: 'New application name', // label: 'New application name',
header: 'Create application', // header: 'Create application',
onConfirm: async folder => { // onConfirm: async folder => {
apiCall('apps/create-folder', { folder }); // apiCall('apps/create-folder', { folder });
}, // },
}); // });
}, // },
}); // });
registerCommand({ registerCommand({
id: 'new.table', id: 'new.table',

View File

@@ -10,6 +10,8 @@
import { copyTextToClipboard } from '../utility/clipboard'; import { copyTextToClipboard } from '../utility/clipboard';
import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte'; import VirtualForeignKeyEditorModal from '../tableeditor/VirtualForeignKeyEditorModal.svelte';
import { showModal } from '../modals/modalTools'; import { showModal } from '../modals/modalTools';
import DefineDictionaryDescriptionModal from '../modals/DefineDictionaryDescriptionModal.svelte';
import { sleep } from '../utility/common';
export let column; export let column;
export let conid = undefined; export let conid = undefined;
@@ -24,6 +26,7 @@
export let allowDefineVirtualReferences = false; export let allowDefineVirtualReferences = false;
export let setGrouping; export let setGrouping;
export let seachInColumns = ''; export let seachInColumns = '';
export let onReload = undefined;
const openReferencedTable = () => { const openReferencedTable = () => {
openDatabaseObjectDetail('TableDataTab', null, { 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() { function getMenu() {
return [ return [
setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' }, setSort && { onClick: () => setSort('ASC'), text: 'Sort ascending' },
@@ -72,10 +88,13 @@
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' }, { onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
], ],
allowDefineVirtualReferences && [
{ divider: true }, { divider: true },
{ onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
], allowDefineVirtualReferences && { onClick: handleDefineVirtualForeignKey, text: 'Define virtual foreign key' },
column.foreignKey && {
onClick: handleCustomizeDescriptions,
text: 'Customize description',
},
]; ];
} }
</script> </script>

View File

@@ -2003,6 +2003,7 @@
grouping={display.getGrouping(col.uniqueName)} grouping={display.getGrouping(col.uniqueName)}
{allowDefineVirtualReferences} {allowDefineVirtualReferences}
seachInColumns={display.config?.searchInColumns} seachInColumns={display.config?.searchInColumns}
onReload={refresh}
/> />
</td> </td>
{/each} {/each}

View File

@@ -15,13 +15,13 @@
import stableStringify from 'json-stable-stringify'; import stableStringify from 'json-stable-stringify';
import { import {
useAllApps,
useConnectionInfo, useConnectionInfo,
useConnectionList, useConnectionList,
useDatabaseInfo, useDatabaseInfo,
useDatabaseServerVersion, useDatabaseServerVersion,
useServerVersion, useServerVersion,
useSettings, useSettings,
useUsedApps,
} from '../utility/metadataLoaders'; } from '../utility/metadataLoaders';
import DataGrid from './DataGrid.svelte'; import DataGrid from './DataGrid.svelte';
@@ -53,7 +53,7 @@
$: connection = useConnectionInfo({ conid }); $: connection = useConnectionInfo({ conid });
$: dbinfo = useDatabaseInfo({ conid, database }); $: dbinfo = useDatabaseInfo({ conid, database });
$: serverVersion = useDatabaseServerVersion({ conid, database }); $: serverVersion = useDatabaseServerVersion({ conid, database });
$: apps = useUsedApps(); $: apps = useAllApps();
$: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps); $: extendedDbInfo = extendDatabaseInfoFromApps($dbinfo, $apps);
$: connections = useConnectionList(); $: connections = useConnectionList();
const settingsValue = useSettings(); const settingsValue = useSettings();

View File

@@ -42,7 +42,7 @@
import DesignerTable from './DesignerTable.svelte'; import DesignerTable from './DesignerTable.svelte';
import { isConnectedByReference } from './designerTools'; import { isConnectedByReference } from './designerTools';
import uuidv1 from 'uuid/v1'; import uuidv1 from 'uuid/v1';
import { getTableInfo, useDatabaseInfo, useUsedApps } from '../utility/metadataLoaders'; import { getTableInfo, useAllApps, useDatabaseInfo } from '../utility/metadataLoaders';
import cleanupDesignColumns from './cleanupDesignColumns'; import cleanupDesignColumns from './cleanupDesignColumns';
import _ from 'lodash'; import _ from 'lodash';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@@ -108,7 +108,7 @@
ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId) ref => tables.find(x => x.designerId == ref.sourceId) && tables.find(x => x.designerId == ref.targetId)
) as any[]; ) as any[];
$: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1; $: zoomKoef = settings?.customizeStyle && value?.style?.zoomKoef ? value?.style?.zoomKoef : 1;
$: apps = useUsedApps(); $: apps = useAllApps();
$: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2; $: isMultipleTableSelection = tables.filter(x => x.isSelectedTable).length >= 2;

View 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>

View File

@@ -11,7 +11,7 @@
<TextField <TextField
{...$$restProps} {...$$restProps}
value={$values[name] ?? defaultValue} value={$values?.[name] ?? defaultValue}
on:input={e => setFieldValue(name, e.target['value'])} on:input={e => setFieldValue(name, e.target['value'])}
on:input={e => { on:input={e => {
if (saveOnInput) { if (saveOnInput) {

View File

@@ -1,35 +1,57 @@
<script lang="ts"> <script lang="ts">
import _ from 'lodash'; import _ from 'lodash';
import { createEventDispatcher } from 'svelte'; import { createEventDispatcher, tick } from 'svelte';
import SelectField from '../forms/SelectField.svelte'; import SelectField from '../forms/SelectField.svelte';
import { currentDatabase } from '../stores'; import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools'; 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 value = '';
export let disableInitialize = false; export let conid;
export let database;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let selectFieldKey = 0;
$: appFolders = useAppFolders(); $: dbInfo = useDatabaseInfo({ conid, database });
$: usedApps = useUsedApps(); $: connectionInfo = useConnectionInfo({ conid });
$: { $: allApps = useAllApps();
if (!disableInitialize && value == '#new' && $currentDatabase) { $: apps = filterAppsForDatabase($connectionInfo, database, $allApps || [], $dbInfo);
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
const common = _.intersection( $: if (apps?.length == 1) {
($appFolders || []).map(x => x.name), value = apps[0].appid;
filtered.map(x => x.name) selectFieldKey++;
);
if (common.length > 0) {
value = common[0] as string;
dispatch('change', value); 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> </script>
<div class="flex">
{#key selectFieldKey}
<SelectField <SelectField
isNative isNative
{...$$restProps} {...$$restProps}
@@ -39,10 +61,18 @@
dispatch('change', value); dispatch('change', value);
}} }}
options={[ options={[
{ label: '(New application linked to current DB)', value: '#new' }, {
...($appFolders || []).map(app => ({ label: '(not selected)',
label: app.name, value: '',
value: app.name, },
...(apps || []).map(app => ({
label: app.applicationName,
value: app.appid,
})), })),
]} ]}
/> />
{/key}
<InlineButton on:click={handleAddNewApplication} square>
<FontIcon icon="icon plus-thick" padLeft padRight />
</InlineButton>
</div>

View File

@@ -76,6 +76,7 @@
'icon send': 'mdi mdi-send', 'icon send': 'mdi mdi-send',
'icon regex': 'mdi mdi-regex', 'icon regex': 'mdi mdi-regex',
'icon list': 'mdi mdi-format-list-bulleted-triangle', 'icon list': 'mdi mdi-format-list-bulleted-triangle',
'icon help': 'mdi mdi-help',
'icon window-restore': 'mdi mdi-window-restore', 'icon window-restore': 'mdi mdi-window-restore',
'icon window-maximize': 'mdi mdi-window-maximize', 'icon window-maximize': 'mdi mdi-window-maximize',

View File

@@ -1,11 +1,10 @@
<script lang="ts"> <script lang="ts">
import FormProvider from '../forms/FormProvider.svelte';
import _ from 'lodash'; import _ from 'lodash';
import FormSubmit from '../forms/FormSubmit.svelte'; import FormSubmit from '../forms/FormSubmit.svelte';
import FormStyledButton from '../buttons/FormStyledButton.svelte'; import FormStyledButton from '../buttons/FormStyledButton.svelte';
import ModalBase from './ModalBase.svelte'; import ModalBase from './ModalBase.svelte';
import { closeCurrentModal } from './modalTools'; 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 TableControl from '../elements/TableControl.svelte';
import TextField from '../forms/TextField.svelte'; import TextField from '../forms/TextField.svelte';
import FormTextField from '../forms/FormTextField.svelte'; import FormTextField from '../forms/FormTextField.svelte';
@@ -16,14 +15,12 @@
checkDescriptionExpression, checkDescriptionExpression,
getDictionaryDescription, getDictionaryDescription,
parseDelimitedColumnList, parseDelimitedColumnList,
saveDictionaryDescription,
} from '../utility/dictionaryDescriptionTools'; } from '../utility/dictionaryDescriptionTools';
import { includes } from 'lodash';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import FormSelectField from '../forms/FormSelectField.svelte'; import FormSelectField from '../forms/FormSelectField.svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte'; import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { currentDatabase } from '../stores'; import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools'; import { filterAppsForDatabase } from '../utility/appTools';
import { apiCall } from '../utility/api';
export let conid; export let conid;
export let database; export let database;
@@ -33,13 +30,12 @@
$: tableInfo = useTableInfo({ conid, database, schemaName, pureName }); $: tableInfo = useTableInfo({ conid, database, schemaName, pureName });
$: apps = useUsedApps(); $: apps = useAllApps();
$: appFolders = useAppFolders();
$: connections = useConnectionList(); $: connections = useConnectionList();
$: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true); $: descriptionInfo = getDictionaryDescription($tableInfo, conid, database, $apps, $connections, true);
const values = writable({ targetApplication: '#new' } as any); const values = writable({ targetApplication: '' } as any);
function initValues(descriptionInfo) { function initValues(descriptionInfo) {
$values = { $values = {
@@ -52,28 +48,21 @@
$: { $: {
if (descriptionInfo) initValues(descriptionInfo); if (descriptionInfo) initValues(descriptionInfo);
} }
$: {
if ($values.targetApplication == '#new' && $currentDatabase) {
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $apps || []);
const common = _.intersection(
($appFolders || []).map(x => x.name),
filtered.map(x => x.name)
);
if (common.length > 0) {
$values = {
...$values,
targetApplication: common[0],
};
}
}
}
</script> </script>
<FormProviderCore {values}> <FormProviderCore {values}>
<ModalBase {...$$restProps}> <ModalBase {...$$restProps}>
<svelte:fragment slot="header">Define description</svelte:fragment> <svelte:fragment slot="header">Define description</svelte:fragment>
<FormSelectField
label="Target application (mandatory)"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
{conid}
{database}
/>
<div class="wrapper"> <div class="wrapper">
<TableControl <TableControl
rows={$tableInfo?.columns || []} rows={$tableInfo?.columns || []}
@@ -103,30 +92,34 @@
<FormTextField name="delimiter" label="Delimiter" /> <FormTextField name="delimiter" label="Delimiter" />
<FormSelectField
label="Target application"
name="targetApplication"
disableInitialize
selectFieldComponent={TargetApplicationSelect}
/>
<!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> --> <!-- <FormCheckboxField name="useForAllDatabases" label="Use for all databases" /> -->
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<FormSubmit <FormSubmit
value="OK" value="OK"
disabled={!checkDescriptionExpression($values?.columns, $tableInfo)} disabled={!checkDescriptionExpression($values?.columns, $tableInfo) || !$values.targetApplication}
on:click={() => { on:click={async () => {
closeCurrentModal(); closeCurrentModal();
saveDictionaryDescription(
$tableInfo, const expression = $values.columns;
conid, await apiCall('apps/save-dictionary-description', {
database, appid: $values.targetApplication,
$values.columns, schemaName: $tableInfo.schemaName,
$values.delimiter, pureName: $tableInfo.pureName,
$values.targetApplication columns: parseDelimitedColumnList(expression),
); expression,
onConfirm(); delimiter: $values.delimiter,
});
// saveDictionaryDescription(
// $tableInfo,
// conid,
// database,
// $values.columns,
// $values.delimiter,
// $values.targetApplication
// );
onConfirm?.();
}} }}
/> />
<FormStyledButton type="button" value="Close" on:click={closeCurrentModal} /> <FormStyledButton type="button" value="Close" on:click={closeCurrentModal} />

View File

@@ -6,7 +6,7 @@
import { closeCurrentModal, showModal } from './modalTools'; import { closeCurrentModal, showModal } from './modalTools';
import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte'; import DefineDictionaryDescriptionModal from './DefineDictionaryDescriptionModal.svelte';
import ScrollableTableControl from '../elements/ScrollableTableControl.svelte'; import ScrollableTableControl from '../elements/ScrollableTableControl.svelte';
import { getTableInfo, useConnectionList, useUsedApps } from '../utility/metadataLoaders'; import { getTableInfo, useAllApps, useConnectionList } from '../utility/metadataLoaders';
import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools'; import { getDictionaryDescription } from '../utility/dictionaryDescriptionTools';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { dumpSqlSelect } from 'dbgate-sqltree'; import { dumpSqlSelect } from 'dbgate-sqltree';
@@ -34,7 +34,7 @@
let checkedKeys = []; let checkedKeys = [];
$: apps = useUsedApps(); $: apps = useAllApps();
$: connections = useConnectionList(); $: connections = useConnectionList();
function defineDescription() { function defineDescription() {

View File

@@ -12,7 +12,8 @@
import { onMount, tick } from 'svelte'; import { onMount, tick } from 'svelte';
import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte'; import TargetApplicationSelect from '../forms/TargetApplicationSelect.svelte';
import { apiCall } from '../utility/api'; 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 conid;
export let database; export let database;
@@ -173,7 +174,7 @@
<div class="row"> <div class="row">
<div class="label col-3">Target application</div> <div class="label col-3">Target application</div>
<div class="col-9"> <div class="col-9">
<TargetApplicationSelect bind:value={dstApp} /> <TargetApplicationSelect bind:value={dstApp} {conid} {database} />
</div> </div>
</div> </div>
</div> </div>
@@ -181,10 +182,10 @@
<svelte:fragment slot="footer"> <svelte:fragment slot="footer">
<FormSubmit <FormSubmit
value={'Save'} value={'Save'}
disabled={!dstApp}
on:click={async () => { on:click={async () => {
const appFolder = await saveDbToApp(conid, database, dstApp);
await apiCall('apps/save-virtual-reference', { await apiCall('apps/save-virtual-reference', {
appFolder, appid: dstApp,
schemaName, schemaName,
pureName, pureName,
refSchemaName, refSchemaName,

View File

@@ -28,6 +28,10 @@
import { apiCall, apiOff, apiOn } from '../utility/api'; import { apiCall, apiOff, apiOn } from '../utility/api';
import createActivator, { getActiveComponent } from '../utility/createActivator'; import createActivator, { getActiveComponent } from '../utility/createActivator';
import useEffect from '../utility/useEffect'; 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); export const activator = createActivator('QueryDataTab', true);
@@ -40,6 +44,8 @@
let jslid; let jslid;
let loading = false; let loading = false;
$: frontMatter = getSqlFrontMatter(sql, yaml);
async function loadData(conid, database, sql) { async function loadData(conid, database, sql) {
const resp = await apiCall('sessions/execute-reader', { const resp = await apiCall('sessions/execute-reader', {
conid, conid,
@@ -96,17 +102,30 @@
} }
} }
$: $effect; $: $effect;
$: selectedChart = frontMatter?.['selected-chart'];
$: fixedChartDefinition = selectedChart && frontMatter ? frontMatter?.[`chart-${selectedChart}`] : null;
</script> </script>
<ToolStripContainer> <ToolStripContainer>
{#if jslid} {#if loading}
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
{:else}
<LoadingInfo message="Loading data..." /> <LoadingInfo message="Loading data..." />
{:else if jslid}
{#if fixedChartDefinition}
<JslChart {jslid} fixedDefinition={fixedChartDefinition} />
{:else}
<JslDataGrid {jslid} listenInitializeFile onCustomGridRefresh={handleRefresh} focusOnVisible />
{/if}
{/if} {/if}
<svelte:fragment slot="toolstrip"> <svelte:fragment slot="toolstrip">
{#if fixedChartDefinition}
<ToolStripButton on:click={handleRefresh} icon="icon refresh">Refresh</ToolStripButton>
{:else}
<ToolStripCommandButton command="dataGrid.refresh" /> <ToolStripCommandButton command="dataGrid.refresh" />
{/if}
<ToolStripCommandButton command="queryData.stopLoading" /> <ToolStripCommandButton command="queryData.stopLoading" />
{#if !fixedChartDefinition}
<ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} /> <ToolStripExportButton command="jslTableGrid.export" {quickExportHandlerRef} />
{/if}
</svelte:fragment> </svelte:fragment>
</ToolStripContainer> </ToolStripContainer>

View File

@@ -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 { 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) { // export async function saveDbToApp(conid: string, database: string, app: string) {
if (app == '#new') { // const connection = await getConnectionInfo({ conid });
const folder = await apiCall('apps/create-folder', { folder: database });
await apiCall('connections/update-database', { // if (app == '#new') {
conid, // const appJson = {
database, // applicationName: _.startCase(database),
values: { // usageRules: [
[`useApp:${folder}`]: true, // {
}, // serverHostsList: connection?.server ? [connection.server] : undefined,
// databaseNamesList: [database],
// conditionGroup: '1',
// },
// ],
// };
// 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 [];
}
// 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;
}); });
// const db = (connection?.databases || []).find(x => x.name == database);
return folder; // return apps?.filter(app => db && db[`useApp:${app.name}`]);
} }
await apiCall('connections/update-database', { export async function openApplicationEditor(appid) {
conid, const dataContent = await apiCall('files/load', { folder: 'apps', file: appid, format: 'json' });
database, openNewTab(
values: { {
[`useApp:${app}`]: true, title: appid,
icon: 'img app',
tabComponent: 'AppEditorTab',
props: {
savedFile: appid,
savedFolder: 'apps',
savedFormat: 'json',
}, },
}); },
{ editor: dataContent }
return app; );
}
export function filterAppsForDatabase(connection, database: string, $apps): ApplicationDefinition[] {
const db = (connection?.databases || []).find(x => x.name == database);
return $apps?.filter(app => db && db[`useApp:${app.name}`]);
} }

View File

@@ -6,6 +6,7 @@ const cachedByKey = {};
const cachedPromisesByKey = {}; const cachedPromisesByKey = {};
const cachedKeysByReloadTrigger = {}; const cachedKeysByReloadTrigger = {};
const subscriptionsByReloadTrigger = {}; const subscriptionsByReloadTrigger = {};
const subscriptionsByByCacheKeyPeek = {};
const cacheGenerationByKey = {}; const cacheGenerationByKey = {};
let cacheGeneration = 0; let cacheGeneration = 0;
@@ -29,6 +30,7 @@ function cacheSet(cacheKey, value, reloadTrigger, generation) {
addCacheKeyToReloadTrigger(cacheKey, reloadTrigger); addCacheKeyToReloadTrigger(cacheKey, reloadTrigger);
delete cachedPromisesByKey[cacheKey]; delete cachedPromisesByKey[cacheKey];
cacheGenerationByKey[cacheKey] = generation; cacheGenerationByKey[cacheKey] = generation;
dispatchCacheChangePeek(cacheKey);
} }
function cacheClean(reloadTrigger) { function cacheClean(reloadTrigger) {
@@ -64,6 +66,10 @@ function getCacheGenerationForKey(cacheKey) {
return cacheGenerationByKey[cacheKey] || 0; return cacheGenerationByKey[cacheKey] || 0;
} }
export function getCachedValue(cacheKey) {
return cacheGet(cacheKey);
}
export async function loadCachedValue(reloadTrigger, cacheKey, func) { export async function loadCachedValue(reloadTrigger, cacheKey, func) {
const fromCache = cacheGet(cacheKey); const fromCache = cacheGet(cacheKey);
if (fromCache) { if (fromCache) {
@@ -107,12 +113,36 @@ export async function unsubscribeCacheChange(reloadTrigger, cacheKey, reloadHand
x => x != reloadHandler x => x != reloadHandler
); );
} }
if (subscriptionsByReloadTrigger[itemString].length == 0) { if (subscriptionsByReloadTrigger[itemString]?.length == 0) {
delete subscriptionsByReloadTrigger[itemString]; 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) { export function dispatchCacheChange(reloadTrigger) {
cacheClean(reloadTrigger); cacheClean(reloadTrigger);

View File

@@ -1,8 +1,9 @@
import type { DictionaryDescription } from 'dbgate-datalib'; 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 _ from 'lodash';
import { apiCall } from './api'; import { apiCall } from './api';
import { filterAppsForDatabase, saveDbToApp } from './appTools'; import { filterAppsForDatabase } from './appTools';
// import { filterAppsForDatabase, saveDbToApp } from './appTools';
function checkDescriptionColumns(columns: string[], table: TableInfo) { function checkDescriptionColumns(columns: string[], table: TableInfo) {
if (!columns?.length) return false; if (!columns?.length) return false;
@@ -17,7 +18,8 @@ export function getDictionaryDescription(
database: string, database: string,
apps: ApplicationDefinition[], apps: ApplicationDefinition[],
connections, connections,
skipCheckSaved: boolean = false skipCheckSaved: boolean = false,
dbInfo: DatabaseInfo = null
): DictionaryDescription { ): DictionaryDescription {
const conn = connections?.find(x => x._id == conid); const conn = connections?.find(x => x._id == conid);
@@ -25,7 +27,7 @@ export function getDictionaryDescription(
return null; return null;
} }
const dbApps = filterAppsForDatabase(conn, database, apps); const dbApps = filterAppsForDatabase(conn, database, apps, dbInfo);
if (!dbApps) { if (!dbApps) {
return null; return null;
@@ -70,22 +72,20 @@ export function changeDelimitedColumnList(columns, columnName, isChecked) {
return parsed.join(','); return parsed.join(',');
} }
export async function saveDictionaryDescription( // export async function saveDictionaryDescription(
table: TableInfo, // table: TableInfo,
conid: string, // conid: string,
database: string, // database: string,
expression: string, // expression: string,
delimiter: string, // delimiter: string,
targetApplication: string // targetApplication: string
) { // ) {
const appFolder = await saveDbToApp(conid, database, targetApplication); // await apiCall('apps/save-dictionary-description', {
// appFolder,
await apiCall('apps/save-dictionary-description', { // schemaName: table.schemaName,
appFolder, // pureName: table.pureName,
schemaName: table.schemaName, // columns: parseDelimitedColumnList(expression),
pureName: table.pureName, // expression,
columns: parseDelimitedColumnList(expression), // delimiter,
expression, // });
delimiter, // }
});
}

View File

@@ -1,5 +1,12 @@
import _ from 'lodash'; 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 stableStringify from 'json-stable-stringify';
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
import { extendDatabaseInfo } from 'dbgate-tools'; import { extendDatabaseInfo } from 'dbgate-tools';
@@ -107,17 +114,17 @@ const archiveFilesLoader = ({ folder }) => ({
reloadTrigger: { key: `archive-files-changed`, folder }, reloadTrigger: { key: `archive-files-changed`, folder },
}); });
const appFoldersLoader = () => ({ // const appFoldersLoader = () => ({
url: 'apps/folders', // url: 'apps/folders',
params: {}, // params: {},
reloadTrigger: { key: `app-folders-changed` }, // reloadTrigger: { key: `app-folders-changed` },
}); // });
const appFilesLoader = ({ folder }) => ({ // const appFilesLoader = ({ folder }) => ({
url: 'apps/files', // url: 'apps/files',
params: { folder }, // params: { folder },
reloadTrigger: { key: `app-files-changed`, app: folder }, // reloadTrigger: { key: `app-files-changed`, app: folder },
}); // });
// const dbAppsLoader = ({ conid, database }) => ({ // const dbAppsLoader = ({ conid, database }) => ({
// url: 'apps/get-apps-for-db', // url: 'apps/get-apps-for-db',
@@ -125,10 +132,10 @@ const appFilesLoader = ({ folder }) => ({
// reloadTrigger: `db-apps-changed-${conid}-${database}`, // reloadTrigger: `db-apps-changed-${conid}-${database}`,
// }); // });
const usedAppsLoader = ({ conid, database }) => ({ const allAppsLoader = () => ({
url: 'apps/get-used-apps', url: 'apps/get-all-apps',
params: {}, params: {},
reloadTrigger: { key: `used-apps-changed` }, reloadTrigger: { key: `files-changed`, folder: 'apps' },
}); });
const serverStatusLoader = () => ({ 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>} */ /** @returns {Promise<import('dbgate-types').DatabaseInfo>} */
export function getDatabaseInfo(args) { export function getDatabaseInfo(args) {
return getCore(databaseInfoLoader, args); return getCore(databaseInfoLoader, args);
@@ -237,6 +275,10 @@ export function useDatabaseInfo(args) {
return useCore(databaseInfoLoader, args); return useCore(databaseInfoLoader, args);
} }
export function useDatabaseInfoPeek(args) {
return useCorePeek(databaseInfoLoader, args);
}
export async function getDbCore(args, objectTypeField = undefined) { export async function getDbCore(args, objectTypeField = undefined) {
const db = await getDatabaseInfo(args); const db = await getDatabaseInfo(args);
if (!db) return null; if (!db) return null;
@@ -392,25 +434,25 @@ export function useArchiveFolders(args = {}) {
return useCore(archiveFoldersLoader, args); return useCore(archiveFoldersLoader, args);
} }
export function getAppFiles(args) { // export function getAppFiles(args) {
return getCore(appFilesLoader, args); // return getCore(appFilesLoader, args);
} // }
export function useAppFiles(args) { // export function useAppFiles(args) {
return useCore(appFilesLoader, args); // return useCore(appFilesLoader, args);
} // }
export function getAppFolders(args = {}) { // export function getAppFolders(args = {}) {
return getCore(appFoldersLoader, args); // return getCore(appFoldersLoader, args);
} // }
export function useAppFolders(args = {}) { // export function useAppFolders(args = {}) {
return useCore(appFoldersLoader, args); // return useCore(appFoldersLoader, args);
} // }
export function getUsedApps(args = {}) { export function getAllApps(args = {}) {
return getCore(usedAppsLoader, args); return getCore(allAppsLoader, args);
} }
export function useUsedApps(args = {}) { export function useAllApps(args = {}) {
return useCore(usedAppsLoader, args); return useCore(allAppsLoader, args);
} }
// export function getDbApps(args = {}) { // export function getDbApps(args = {}) {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -12,6 +12,7 @@
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { isProApp } from '../utility/proTools'; import { isProApp } from '../utility/proTools';
import InlineUploadButton from '../buttons/InlineUploadButton.svelte'; import InlineUploadButton from '../buttons/InlineUploadButton.svelte';
import { DATA_FOLDER_NAMES } from 'dbgate-tools';
let filter = ''; let filter = '';
@@ -27,6 +28,7 @@
const dbCompareJobFiles = useFiles({ folder: 'dbcompare' }); const dbCompareJobFiles = useFiles({ folder: 'dbcompare' });
const perspectiveFiles = useFiles({ folder: 'perspectives' }); const perspectiveFiles = useFiles({ folder: 'perspectives' });
const modelTransformFiles = useFiles({ folder: 'modtrans' }); const modelTransformFiles = useFiles({ folder: 'modtrans' });
const appFiles = useFiles({ folder: 'apps' });
$: files = [ $: files = [
...($sqlFiles || []), ...($sqlFiles || []),
@@ -41,32 +43,18 @@
...($modelTransformFiles || []), ...($modelTransformFiles || []),
...((isProApp() && $dataDeployJobFiles) || []), ...((isProApp() && $dataDeployJobFiles) || []),
...((isProApp() && $dbCompareJobFiles) || []), ...((isProApp() && $dbCompareJobFiles) || []),
...((isProApp() && $appFiles) || []),
]; ];
function handleRefreshFiles() { function handleRefreshFiles() {
apiCall('files/refresh', { apiCall('files/refresh', {
folders: [ folders: DATA_FOLDER_NAMES.map(folder => folder.name),
'sql',
'shell',
'markdown',
'charts',
'query',
'sqlite',
'diagrams',
'perspectives',
'impexp',
'modtrans',
'datadeploy',
'dbcompare',
],
}); });
} }
function dataFolderTitle(folder) { function dataFolderTitle(folder) {
if (folder == 'modtrans') return 'Model transforms'; const foundFolder = DATA_FOLDER_NAMES.find(f => f.name === folder);
if (folder == 'datadeploy') return 'Data deploy jobs'; return foundFolder ? foundFolder.label : _.startCase(folder);
if (folder == 'dbcompare') return 'Database compare jobs';
return _.startCase(folder);
} }
async function handleUploadedFile(filePath, fileName) { async function handleUploadedFile(filePath, fileName) {

View File

@@ -17,11 +17,11 @@
import SearchInput from '../elements/SearchInput.svelte'; import SearchInput from '../elements/SearchInput.svelte';
import WidgetsInnerContainer from './WidgetsInnerContainer.svelte'; import WidgetsInnerContainer from './WidgetsInnerContainer.svelte';
import { import {
useAllApps,
useConnectionInfo, useConnectionInfo,
useDatabaseInfo, useDatabaseInfo,
useDatabaseStatus, useDatabaseStatus,
useSchemaList, useSchemaList,
useUsedApps,
} from '../utility/metadataLoaders'; } from '../utility/metadataLoaders';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte'; import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import AppObjectList from '../appobj/AppObjectList.svelte'; import AppObjectList from '../appobj/AppObjectList.svelte';
@@ -73,9 +73,8 @@
$: connection = useConnectionInfo({ conid }); $: connection = useConnectionInfo({ conid });
$: driver = findEngineDriver($connection, $extensions); $: driver = findEngineDriver($connection, $extensions);
$: apps = useUsedApps(); $: apps = useAllApps();
$: appsForDb = filterAppsForDatabase($connection, database, $apps || [], $objects);
$: dbApps = filterAppsForDatabase($currentDatabase?.connection, $currentDatabase?.name, $apps || []);
// $: console.log('OBJECTS', $objects); // $: console.log('OBJECTS', $objects);
@@ -87,11 +86,12 @@
['schemaName', 'pureName'] ['schemaName', 'pureName']
) )
), ),
...dbApps.map(app => ...appsForDb.map(app =>
app.queries.map(query => ({ Object.values(app.files || {})
.filter(x => x.type == 'query')
.map(query => ({
objectTypeField: 'queries', objectTypeField: 'queries',
pureName: query.name, pureName: query.label,
schemaName: app.name,
sql: query.sql, sql: query.sql,
})) }))
), ),
@@ -281,7 +281,7 @@
> >
<AppObjectList <AppObjectList
list={objectList list={objectList
.filter(x => ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true)) .filter(x => x.schemaName == null || ($appliedCurrentSchema ? x.schemaName == $appliedCurrentSchema : true))
.map(x => ({ ...x, conid, database }))} .map(x => ({ ...x, conid, database }))}
module={databaseObjectAppObject} module={databaseObjectAppObject}
groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)} groupFunc={data => getObjectTypeFieldLabel(data.objectTypeField, driver)}

View File

@@ -6,7 +6,6 @@
import PluginsWidget from './PluginsWidget.svelte'; import PluginsWidget from './PluginsWidget.svelte';
import CellDataWidget from './CellDataWidget.svelte'; import CellDataWidget from './CellDataWidget.svelte';
import HistoryWidget from './HistoryWidget.svelte'; import HistoryWidget from './HistoryWidget.svelte';
import AppWidget from './AppWidget.svelte';
import AdminMenuWidget from './AdminMenuWidget.svelte'; import AdminMenuWidget from './AdminMenuWidget.svelte';
import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte'; import AdminPremiumPromoWidget from './AdminPremiumPromoWidget.svelte';
import PublicCloudWidget from './PublicCloudWidget.svelte'; import PublicCloudWidget from './PublicCloudWidget.svelte';
@@ -14,8 +13,9 @@
import hasPermission from '../utility/hasPermission'; import hasPermission from '../utility/hasPermission';
</script> </script>
{#if hasPermission('widgets/database')}
<DatabaseWidget hidden={$visibleSelectedWidget != 'database'} /> <DatabaseWidget hidden={$visibleSelectedWidget != 'database'} />
{/if}
{#if $visibleSelectedWidget == 'file' && hasPermission('widgets/file')} {#if $visibleSelectedWidget == 'file' && hasPermission('widgets/file')}
<FilesWidget /> <FilesWidget />
{/if} {/if}
@@ -31,9 +31,6 @@
{#if $visibleSelectedWidget == 'cell-data' && hasPermission('widgets/cell-data')} {#if $visibleSelectedWidget == 'cell-data' && hasPermission('widgets/cell-data')}
<CellDataWidget /> <CellDataWidget />
{/if} {/if}
{#if $visibleSelectedWidget == 'app' && hasPermission('widgets/app')}
<AppWidget />
{/if}
{#if $visibleSelectedWidget == 'admin' && hasPermission('widgets/admin')} {#if $visibleSelectedWidget == 'admin' && hasPermission('widgets/admin')}
<AdminMenuWidget /> <AdminMenuWidget />
{/if} {/if}

View File

@@ -110,13 +110,6 @@
hasPermission('settings/change') && { command: 'settings.show' }, hasPermission('settings/change') && { command: 'settings.show' },
{ command: 'theme.changeTheme' }, { command: 'theme.changeTheme' },
hasPermission('settings/change') && { command: 'settings.commands' }, hasPermission('settings/change') && { command: 'settings.commands' },
hasPermission('widgets/app') && {
text: 'View applications',
onClick: () => {
$selectedWidget = 'app';
$visibleWidgetSideBar = true;
},
},
hasPermission('widgets/plugins') && { hasPermission('widgets/plugins') && {
text: 'Manage plugins', text: 'Manage plugins',
onClick: () => { onClick: () => {