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.diagram', hideDisabled: true },
{ command: 'new.perspective', hideDisabled: true },
{ command: 'new.application', hideDisabled: true },
{ command: 'new.shell', hideDisabled: true },
{ command: 'new.jsonl', hideDisabled: true },
{ command: 'new.modelTransform', hideDisabled: true },

View File

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

View File

@@ -1,233 +1,98 @@
const fs = require('fs-extra');
const _ = require('lodash');
const path = require('path');
const { appdir } = require('../utility/directories');
const { appdir, filesdir } = require('../utility/directories');
const socket = require('../utility/socket');
const connections = require('./connections');
const {
loadPermissionsFromRequest,
loadFilePermissionsFromRequest,
hasPermission,
getFilePermissionRole,
} = require('../utility/hasPermission');
module.exports = {
folders_meta: true,
async folders() {
const folders = await fs.readdir(appdir());
return [
...folders.map(name => ({
name,
})),
];
},
createFolder_meta: true,
async createFolder({ folder }) {
const name = await this.getNewAppFolder({ name: folder });
await fs.mkdir(path.join(appdir(), name));
socket.emitChanged('app-folders-changed');
this.emitChangedDbApp(folder);
return name;
},
files_meta: true,
async files({ folder }) {
if (!folder) return [];
const dir = path.join(appdir(), folder);
if (!(await fs.exists(dir))) return [];
const files = await fs.readdir(dir);
function fileType(ext, type) {
return files
.filter(name => name.endsWith(ext))
.map(name => ({
name: name.slice(0, -ext.length),
label: path.parse(name.slice(0, -ext.length)).base,
type,
}));
}
return [
...fileType('.command.sql', 'command.sql'),
...fileType('.query.sql', 'query.sql'),
...fileType('.config.json', 'config.json'),
];
},
async emitChangedDbApp(folder) {
const used = await this.getUsedAppFolders();
if (used.includes(folder)) {
socket.emitChanged('used-apps-changed');
}
},
refreshFiles_meta: true,
async refreshFiles({ folder }) {
socket.emitChanged('app-files-changed', { app: folder });
},
refreshFolders_meta: true,
async refreshFolders() {
socket.emitChanged(`app-folders-changed`);
},
deleteFile_meta: true,
async deleteFile({ folder, file, fileType }) {
await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
socket.emitChanged('app-files-changed', { app: folder });
this.emitChangedDbApp(folder);
},
renameFile_meta: true,
async renameFile({ folder, file, newFile, fileType }) {
await fs.rename(
path.join(path.join(appdir(), folder), `${file}.${fileType}`),
path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
);
socket.emitChanged('app-files-changed', { app: folder });
this.emitChangedDbApp(folder);
},
renameFolder_meta: true,
async renameFolder({ folder, newFolder }) {
const uniqueName = await this.getNewAppFolder({ name: newFolder });
await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
socket.emitChanged(`app-folders-changed`);
},
deleteFolder_meta: true,
async deleteFolder({ folder }) {
if (!folder) throw new Error('Missing folder parameter');
await fs.rmdir(path.join(appdir(), folder), { recursive: true });
socket.emitChanged(`app-folders-changed`);
socket.emitChanged('app-files-changed', { app: folder });
socket.emitChanged('used-apps-changed');
},
async getNewAppFolder({ name }) {
if (!(await fs.exists(path.join(appdir(), name)))) return name;
let index = 2;
while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
index += 1;
}
return `${name}${index}`;
},
getUsedAppFolders_meta: true,
async getUsedAppFolders() {
const list = await connections.list();
const apps = [];
for (const connection of list) {
for (const db of connection.databases || []) {
for (const key of _.keys(db || {})) {
if (key.startsWith('useApp:') && db[key]) {
apps.push(key.substring('useApp:'.length));
}
}
}
}
return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
},
getUsedApps_meta: true,
async getUsedApps() {
const apps = await this.getUsedAppFolders();
getAllApps_meta: true,
async getAllApps({}, req) {
const dir = path.join(filesdir(), 'apps');
const res = [];
const loadedPermissions = await loadPermissionsFromRequest(req);
const filePermissions = await loadFilePermissionsFromRequest(req);
for (const folder of apps) {
res.push(await this.loadApp({ folder }));
}
return res;
},
// getAppsForDb_meta: true,
// async getAppsForDb({ conid, database }) {
// const connection = await connections.get({ conid });
// if (!connection) return [];
// const db = (connection.databases || []).find(x => x.name == database);
// const apps = [];
// const res = [];
// if (db) {
// for (const key of _.keys(db || {})) {
// if (key.startsWith('useApp:') && db[key]) {
// apps.push(key.substring('useApp:'.length));
// }
// }
// }
// for (const folder of apps) {
// res.push(await this.loadApp({ folder }));
// }
// return res;
// },
loadApp_meta: true,
async loadApp({ folder }) {
const res = {
queries: [],
commands: [],
name: folder,
};
const dir = path.join(appdir(), folder);
if (await fs.exists(dir)) {
const files = await fs.readdir(dir);
async function processType(ext, field) {
for (const file of files) {
if (file.endsWith(ext)) {
res[field].push({
name: file.slice(0, -ext.length),
sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
});
}
}
for (const file of await fs.readdir(dir)) {
if (!hasPermission(`all-files`, loadedPermissions)) {
const role = getFilePermissionRole('apps', file, filePermissions);
if (role == 'deny') continue;
}
const content = await fs.readFile(path.join(dir, file), { encoding: 'utf-8' });
const appJson = JSON.parse(content);
// const app = {
// appid: file,
// name: appJson.applicationName,
// usageRules: appJson.usageRules || [],
// icon: appJson.applicationIcon || 'img app',
// color: appJson.applicationColor,
// queries: Object.values(appJson.files || {})
// .filter(x => x.type == 'query')
// .map(x => ({
// name: x.label,
// sql: x.sql,
// })),
// commands: Object.values(appJson.files || {})
// .filter(x => x.type == 'command')
// .map(x => ({
// name: x.label,
// sql: x.sql,
// })),
// virtualReferences: appJson.virtualReferences,
// dictionaryDescriptions: appJson.dictionaryDescriptions,
// };
const app = {
...appJson,
appid: file,
};
await processType('.command.sql', 'commands');
await processType('.query.sql', 'queries');
res.push(app);
}
try {
res.virtualReferences = JSON.parse(
await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
);
} catch (err) {
res.virtualReferences = [];
}
try {
res.dictionaryDescriptions = JSON.parse(
await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
);
} catch (err) {
res.dictionaryDescriptions = [];
}
return res;
},
async saveConfigFile(appFolder, filename, filterFunc, newItem) {
const file = path.join(appdir(), appFolder, filename);
let json;
try {
json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
} catch (err) {
json = [];
createAppFromDb_meta: true,
async createAppFromDb({ appName, server, database }, req) {
const appdir = path.join(filesdir(), 'apps');
if (!fs.existsSync(appdir)) {
await fs.mkdir(appdir);
}
if (filterFunc) {
json = json.filter(filterFunc);
const appId = _.kebabCase(appName);
let suffix = undefined;
while (fs.existsSync(path.join(appdir, `${appId}${suffix || ''}`))) {
if (!suffix) suffix = 2;
else suffix++;
}
const finalAppId = `${appId}${suffix || ''}`;
json = [...json, newItem];
const appJson = {
applicationName: appName,
usageRules: [
{
serverHostsList: server,
databaseNamesList: database,
},
],
};
await fs.writeFile(file, JSON.stringify(json, undefined, 2));
await fs.writeFile(path.join(appdir, `${finalAppId}`), JSON.stringify(appJson, undefined, 2));
socket.emitChanged('app-files-changed', { app: appFolder });
socket.emitChanged('used-apps-changed');
socket.emitChanged(`files-changed`, { folder: 'apps' });
return finalAppId;
},
saveVirtualReference_meta: true,
async saveVirtualReference({ appFolder, schemaName, pureName, refSchemaName, refTableName, columns }) {
await this.saveConfigFile(
appFolder,
'virtual-references.config.json',
async saveVirtualReference({ appid, schemaName, pureName, refSchemaName, refTableName, columns }) {
await this.saveConfigItem(
appid,
'virtualReferences',
columns.length == 1
? x =>
!(
@@ -245,14 +110,17 @@ module.exports = {
columns,
}
);
socket.emitChanged(`files-changed`, { folder: 'apps' });
return true;
},
saveDictionaryDescription_meta: true,
async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
await this.saveConfigFile(
appFolder,
'dictionary-descriptions.config.json',
async saveDictionaryDescription({ appid, pureName, schemaName, expression, columns, delimiter }) {
await this.saveConfigItem(
appid,
'dictionaryDescriptions',
x => !(x.schemaName == schemaName && x.pureName == pureName),
{
schemaName,
@@ -263,18 +131,271 @@ module.exports = {
}
);
socket.emitChanged(`files-changed`, { folder: 'apps' });
return true;
},
createConfigFile_meta: true,
async createConfigFile({ appFolder, fileName, content }) {
const file = path.join(appdir(), appFolder, fileName);
if (!(await fs.exists(file))) {
await fs.writeFile(file, JSON.stringify(content, undefined, 2));
socket.emitChanged('app-files-changed', { app: appFolder });
socket.emitChanged('used-apps-changed');
return true;
async saveConfigItem(appid, fieldName, filterFunc, newItem) {
const file = path.join(filesdir(), 'apps', appid);
const appJson = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
let json = appJson[fieldName] || [];
if (filterFunc) {
json = json.filter(filterFunc);
}
return false;
json = [...json, newItem];
await fs.writeFile(
file,
JSON.stringify(
{
...appJson,
[fieldName]: json,
},
undefined,
2
)
);
socket.emitChanged('files-changed', { folder: 'apps' });
},
// folders_meta: true,
// async folders() {
// const folders = await fs.readdir(appdir());
// return [
// ...folders.map(name => ({
// name,
// })),
// ];
// },
// createFolder_meta: true,
// async createFolder({ folder }) {
// const name = await this.getNewAppFolder({ name: folder });
// await fs.mkdir(path.join(appdir(), name));
// socket.emitChanged('app-folders-changed');
// this.emitChangedDbApp(folder);
// return name;
// },
// files_meta: true,
// async files({ folder }) {
// if (!folder) return [];
// const dir = path.join(appdir(), folder);
// if (!(await fs.exists(dir))) return [];
// const files = await fs.readdir(dir);
// function fileType(ext, type) {
// return files
// .filter(name => name.endsWith(ext))
// .map(name => ({
// name: name.slice(0, -ext.length),
// label: path.parse(name.slice(0, -ext.length)).base,
// type,
// }));
// }
// return [
// ...fileType('.command.sql', 'command.sql'),
// ...fileType('.query.sql', 'query.sql'),
// ...fileType('.config.json', 'config.json'),
// ];
// },
// async emitChangedDbApp(folder) {
// const used = await this.getUsedAppFolders();
// if (used.includes(folder)) {
// socket.emitChanged('used-apps-changed');
// }
// },
// refreshFiles_meta: true,
// async refreshFiles({ folder }) {
// socket.emitChanged('app-files-changed', { app: folder });
// },
// refreshFolders_meta: true,
// async refreshFolders() {
// socket.emitChanged(`app-folders-changed`);
// },
// deleteFile_meta: true,
// async deleteFile({ folder, file, fileType }) {
// await fs.unlink(path.join(appdir(), folder, `${file}.${fileType}`));
// socket.emitChanged('app-files-changed', { app: folder });
// this.emitChangedDbApp(folder);
// },
// renameFile_meta: true,
// async renameFile({ folder, file, newFile, fileType }) {
// await fs.rename(
// path.join(path.join(appdir(), folder), `${file}.${fileType}`),
// path.join(path.join(appdir(), folder), `${newFile}.${fileType}`)
// );
// socket.emitChanged('app-files-changed', { app: folder });
// this.emitChangedDbApp(folder);
// },
// renameFolder_meta: true,
// async renameFolder({ folder, newFolder }) {
// const uniqueName = await this.getNewAppFolder({ name: newFolder });
// await fs.rename(path.join(appdir(), folder), path.join(appdir(), uniqueName));
// socket.emitChanged(`app-folders-changed`);
// },
// deleteFolder_meta: true,
// async deleteFolder({ folder }) {
// if (!folder) throw new Error('Missing folder parameter');
// await fs.rmdir(path.join(appdir(), folder), { recursive: true });
// socket.emitChanged(`app-folders-changed`);
// socket.emitChanged('app-files-changed', { app: folder });
// socket.emitChanged('used-apps-changed');
// },
// async getNewAppFolder({ name }) {
// if (!(await fs.exists(path.join(appdir(), name)))) return name;
// let index = 2;
// while (await fs.exists(path.join(appdir(), `${name}${index}`))) {
// index += 1;
// }
// return `${name}${index}`;
// },
// getUsedAppFolders_meta: true,
// async getUsedAppFolders() {
// const list = await connections.list();
// const apps = [];
// for (const connection of list) {
// for (const db of connection.databases || []) {
// for (const key of _.keys(db || {})) {
// if (key.startsWith('useApp:') && db[key]) {
// apps.push(key.substring('useApp:'.length));
// }
// }
// }
// }
// return _.intersection(_.uniq(apps), await fs.readdir(appdir()));
// },
// // getAppsForDb_meta: true,
// // async getAppsForDb({ conid, database }) {
// // const connection = await connections.get({ conid });
// // if (!connection) return [];
// // const db = (connection.databases || []).find(x => x.name == database);
// // const apps = [];
// // const res = [];
// // if (db) {
// // for (const key of _.keys(db || {})) {
// // if (key.startsWith('useApp:') && db[key]) {
// // apps.push(key.substring('useApp:'.length));
// // }
// // }
// // }
// // for (const folder of apps) {
// // res.push(await this.loadApp({ folder }));
// // }
// // return res;
// // },
// loadApp_meta: true,
// async loadApp({ folder }) {
// const res = {
// queries: [],
// commands: [],
// name: folder,
// };
// const dir = path.join(appdir(), folder);
// if (await fs.exists(dir)) {
// const files = await fs.readdir(dir);
// async function processType(ext, field) {
// for (const file of files) {
// if (file.endsWith(ext)) {
// res[field].push({
// name: file.slice(0, -ext.length),
// sql: await fs.readFile(path.join(dir, file), { encoding: 'utf-8' }),
// });
// }
// }
// }
// await processType('.command.sql', 'commands');
// await processType('.query.sql', 'queries');
// }
// try {
// res.virtualReferences = JSON.parse(
// await fs.readFile(path.join(dir, 'virtual-references.config.json'), { encoding: 'utf-8' })
// );
// } catch (err) {
// res.virtualReferences = [];
// }
// try {
// res.dictionaryDescriptions = JSON.parse(
// await fs.readFile(path.join(dir, 'dictionary-descriptions.config.json'), { encoding: 'utf-8' })
// );
// } catch (err) {
// res.dictionaryDescriptions = [];
// }
// return res;
// },
// async saveConfigFile(appFolder, filename, filterFunc, newItem) {
// const file = path.join(appdir(), appFolder, filename);
// let json;
// try {
// json = JSON.parse(await fs.readFile(file, { encoding: 'utf-8' }));
// } catch (err) {
// json = [];
// }
// if (filterFunc) {
// json = json.filter(filterFunc);
// }
// json = [...json, newItem];
// await fs.writeFile(file, JSON.stringify(json, undefined, 2));
// socket.emitChanged('app-files-changed', { app: appFolder });
// socket.emitChanged('used-apps-changed');
// },
// saveDictionaryDescription_meta: true,
// async saveDictionaryDescription({ appFolder, pureName, schemaName, expression, columns, delimiter }) {
// await this.saveConfigFile(
// appFolder,
// 'dictionary-descriptions.config.json',
// x => !(x.schemaName == schemaName && x.pureName == pureName),
// {
// schemaName,
// pureName,
// expression,
// columns,
// delimiter,
// }
// );
// return true;
// },
// createConfigFile_meta: true,
// async createConfigFile({ appFolder, fileName, content }) {
// const file = path.join(appdir(), appFolder, fileName);
// if (!(await fs.exists(file))) {
// await fs.writeFile(file, JSON.stringify(content, undefined, 2));
// socket.emitChanged('app-files-changed', { app: appFolder });
// socket.emitChanged('used-apps-changed');
// return true;
// }
// return false;
// },
};

View File

@@ -3,7 +3,12 @@ const path = require('path');
const crypto = require('crypto');
const { filesdir, archivedir, resolveArchiveFolder, uploadsdir, appdir, jsldir } = require('../utility/directories');
const getChartExport = require('../utility/getChartExport');
const { hasPermission, loadPermissionsFromRequest } = require('../utility/hasPermission');
const {
hasPermission,
loadPermissionsFromRequest,
loadFilePermissionsFromRequest,
getFilePermissionRole,
} = require('../utility/hasPermission');
const socket = require('../utility/socket');
const scheduler = require('./scheduler');
const getDiagramExport = require('../utility/getDiagramExport');

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",
"columns": [
@@ -899,6 +981,85 @@ module.exports = {
]
}
},
{
"pureName": "role_files",
"columns": [
{
"pureName": "role_files",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "role_files",
"columnName": "role_id",
"dataType": "int",
"notNull": true
},
{
"pureName": "role_files",
"columnName": "folder_name",
"dataType": "varchar(100)",
"notNull": false
},
{
"pureName": "role_files",
"columnName": "file_names_list",
"dataType": "varchar(1000)",
"notNull": false
},
{
"pureName": "role_files",
"columnName": "file_names_regex",
"dataType": "varchar(1000)",
"notNull": false
},
{
"pureName": "role_files",
"columnName": "file_permission_role_id",
"dataType": "int",
"notNull": true
}
],
"foreignKeys": [
{
"constraintType": "foreignKey",
"constraintName": "FK_role_files_role_id",
"pureName": "role_files",
"refTableName": "roles",
"deleteAction": "CASCADE",
"columns": [
{
"columnName": "role_id",
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_role_files_file_permission_role_id",
"pureName": "role_files",
"refTableName": "file_permission_roles",
"columns": [
{
"columnName": "file_permission_role_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
"pureName": "role_files",
"constraintType": "primaryKey",
"constraintName": "PK_role_files",
"columns": [
{
"columnName": "id"
}
]
}
},
{
"pureName": "role_permissions",
"columns": [
@@ -1082,49 +1243,6 @@ module.exports = {
]
}
},
{
"pureName": "roles",
"columns": [
{
"pureName": "roles",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "roles",
"columnName": "name",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "roles",
"constraintType": "primaryKey",
"constraintName": "PK_roles",
"columns": [
{
"columnName": "id"
}
]
},
"preloadedRows": [
{
"id": -1,
"name": "anonymous-user"
},
{
"id": -2,
"name": "logged-user"
},
{
"id": -3,
"name": "superadmin"
}
]
},
{
"pureName": "table_permission_roles",
"columns": [
@@ -1243,6 +1361,47 @@ module.exports = {
}
]
},
{
"pureName": "users",
"columns": [
{
"pureName": "users",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "users",
"columnName": "login",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "users",
"columnName": "password",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "users",
"columnName": "email",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "users",
"constraintType": "primaryKey",
"constraintName": "PK_users",
"columns": [
{
"columnName": "id"
}
]
}
},
{
"pureName": "user_connections",
"columns": [
@@ -1397,6 +1556,85 @@ module.exports = {
]
}
},
{
"pureName": "user_files",
"columns": [
{
"pureName": "user_files",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "user_files",
"columnName": "user_id",
"dataType": "int",
"notNull": true
},
{
"pureName": "user_files",
"columnName": "folder_name",
"dataType": "varchar(100)",
"notNull": false
},
{
"pureName": "user_files",
"columnName": "file_names_list",
"dataType": "varchar(1000)",
"notNull": false
},
{
"pureName": "user_files",
"columnName": "file_names_regex",
"dataType": "varchar(1000)",
"notNull": false
},
{
"pureName": "user_files",
"columnName": "file_permission_role_id",
"dataType": "int",
"notNull": true
}
],
"foreignKeys": [
{
"constraintType": "foreignKey",
"constraintName": "FK_user_files_user_id",
"pureName": "user_files",
"refTableName": "users",
"deleteAction": "CASCADE",
"columns": [
{
"columnName": "user_id",
"refColumnName": "id"
}
]
},
{
"constraintType": "foreignKey",
"constraintName": "FK_user_files_file_permission_role_id",
"pureName": "user_files",
"refTableName": "file_permission_roles",
"columns": [
{
"columnName": "file_permission_role_id",
"refColumnName": "id"
}
]
}
],
"primaryKey": {
"pureName": "user_files",
"constraintType": "primaryKey",
"constraintName": "PK_user_files",
"columns": [
{
"columnName": "id"
}
]
}
},
{
"pureName": "user_permissions",
"columns": [
@@ -1641,47 +1879,6 @@ module.exports = {
}
]
}
},
{
"pureName": "users",
"columns": [
{
"pureName": "users",
"columnName": "id",
"dataType": "int",
"autoIncrement": true,
"notNull": true
},
{
"pureName": "users",
"columnName": "login",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "users",
"columnName": "password",
"dataType": "varchar(250)",
"notNull": false
},
{
"pureName": "users",
"columnName": "email",
"dataType": "varchar(250)",
"notNull": false
}
],
"foreignKeys": [],
"primaryKey": {
"pureName": "users",
"constraintType": "primaryKey",
"constraintName": "PK_users",
"columns": [
{
"columnName": "id"
}
]
}
}
],
"collections": [],

View File

@@ -85,6 +85,16 @@ async function loadTablePermissionsFromRequest(req) {
return tablePermissions;
}
async function loadFilePermissionsFromRequest(req) {
const authProvider = getAuthProviderFromReq(req);
if (!req) {
return null;
}
const filePermissions = await authProvider.getCurrentFilePermissions(req);
return filePermissions;
}
function matchDatabasePermissionRow(conid, database, permissionRow) {
if (permissionRow.connection_id) {
if (conid != permissionRow.connection_id) {
@@ -135,6 +145,27 @@ function matchTablePermissionRow(objectTypeField, schemaName, pureName, permissi
return true;
}
function matchFilePermissionRow(folder, file, permissionRow) {
if (permissionRow.folder_name) {
if (folder != permissionRow.folder_name) {
return false;
}
}
if (permissionRow.file_names_list) {
const items = permissionRow.file_names_list.split('\n');
if (!items.find(item => item.trim()?.toLowerCase() === file?.toLowerCase())) {
return false;
}
}
if (permissionRow.file_names_regex) {
const regex = new RegExp(permissionRow.file_names_regex, 'i');
if (!regex.test(file)) {
return false;
}
}
return true;
}
const DATABASE_ROLE_ID_NAMES = {
'-1': 'view',
'-2': 'read_content',
@@ -143,6 +174,11 @@ const DATABASE_ROLE_ID_NAMES = {
'-5': 'deny',
};
const FILE_ROLE_ID_NAMES = {
'-1': 'allow',
'-2': 'deny',
};
function getDatabaseRoleLevelIndex(roleName) {
if (!roleName) {
return 6;
@@ -198,6 +234,17 @@ function getDatabasePermissionRole(conid, database, loadedDatabasePermissions) {
return res;
}
function getFilePermissionRole(folder, file, loadedFilePermissions) {
let res = 'deny';
for (const permissionRow of loadedFilePermissions) {
if (!matchFilePermissionRow(folder, file, permissionRow)) {
continue;
}
res = FILE_ROLE_ID_NAMES[permissionRow.file_permission_role_id];
}
return res;
}
const TABLE_ROLE_ID_NAMES = {
'-1': 'read',
'-2': 'update_only',
@@ -308,8 +355,10 @@ module.exports = {
loadPermissionsFromRequest,
loadDatabasePermissionsFromRequest,
loadTablePermissionsFromRequest,
loadFilePermissionsFromRequest,
getDatabasePermissionRole,
getTablePermissionRole,
getFilePermissionRole,
testStandardPermission,
testDatabaseRolePermission,
getTablePermissionRoleLevelIndex,

View File

@@ -111,3 +111,20 @@ export function fillConstraintNames(table: TableInfo, dialect: SqlDialect) {
}
return res;
}
export const DATA_FOLDER_NAMES = [
{ name: 'sql', label: 'SQL scripts' },
{ name: 'shell', label: 'Shell scripts' },
{ name: 'markdown', label: 'Markdown files' },
{ name: 'charts', label: 'Charts' },
{ name: 'query', label: 'Query designs' },
{ name: 'sqlite', label: 'SQLite files' },
{ name: 'duckdb', label: 'DuckDB files' },
{ name: 'diagrams', label: 'Diagrams' },
{ name: 'perspectives', label: 'Perspectives' },
{ name: 'impexp', label: 'Import/Export jobs' },
{ name: 'modtrans', label: 'Model transforms' },
{ name: 'datadeploy', label: 'Data deploy jobs' },
{ name: 'dbcompare', label: 'Database compare jobs' },
{ name: 'apps', label: 'Applications' },
];

View File

@@ -1,12 +1,12 @@
interface ApplicationCommand {
name: string;
sql: string;
}
// interface ApplicationCommand {
// name: string;
// sql: string;
// }
interface ApplicationQuery {
name: string;
sql: string;
}
// interface ApplicationQuery {
// name: string;
// sql: string;
// }
interface VirtualReferenceDefinition {
pureName: string;
@@ -27,11 +27,31 @@ interface DictionaryDescriptionDefinition {
delimiter: string;
}
export interface ApplicationDefinition {
name: string;
queries: ApplicationQuery[];
commands: ApplicationCommand[];
virtualReferences: VirtualReferenceDefinition[];
dictionaryDescriptions: DictionaryDescriptionDefinition[];
interface ApplicationUsageRule {
conditionGroup?: string;
serverHostsRegex?: string;
serverHostsList?: string[];
databaseNamesRegex?: string;
databaseNamesList?: string[];
tableNamesRegex?: string;
tableNamesList?: string[];
columnNamesRegex?: string;
columnNamesList?: string[];
}
export interface ApplicationDefinition {
appid: string;
applicationName: string;
applicationIcon?: string;
applicationColor?: string;
usageRules?: ApplicationUsageRule[];
files?: {
[key: string]: {
label: string;
sql: string;
type: 'query' | 'command';
};
};
virtualReferences?: VirtualReferenceDefinition[];
dictionaryDescriptions?: DictionaryDescriptionDefinition[];
}

View File

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

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

View File

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

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 commands = _.flatten((apps || []).map(x => x.commands || []));
const commands = _.flatten((apps || []).map(x => Object.values(x.files || {}).filter(x => x.type == 'command')));
const isSqlOrDoc =
driver?.databaseEngineTypes?.includes('sql') || driver?.databaseEngineTypes?.includes('document');
@@ -564,11 +580,26 @@ await dbgateApi.executeQuery(${JSON.stringify(
text: _t('database.dataDeployer', { defaultMessage: 'Data deployer' }),
},
isProApp() &&
hasPermission(`files/apps/write`) && {
onClick: handleCreateNewApp,
text: _t('database.createNewApplication', { defaultMessage: 'Create new application' }),
},
isProApp() &&
apps?.length > 0 && {
text: _t('database.editApplications', { defaultMessage: 'Edit application' }),
submenu: apps.map((app: any) => ({
text: app.applicationName,
onClick: () => openApplicationEditor(app.appid),
})),
},
{ divider: true },
commands.length > 0 && [
commands.map((cmd: any) => ({
text: cmd.name,
text: cmd.label,
onClick: () => {
showModal(ConfirmSqlModal, {
sql: cmd.sql,
@@ -618,12 +649,12 @@ await dbgateApi.executeQuery(${JSON.stringify(
getConnectionLabel,
} from 'dbgate-tools';
import InputTextModal from '../modals/InputTextModal.svelte';
import { getDatabaseInfo, useUsedApps } from '../utility/metadataLoaders';
import { getDatabaseInfo, useAllApps, useDatabaseInfoPeek } from '../utility/metadataLoaders';
import { openJsonDocument } from '../tabs/JsonTab.svelte';
import { apiCall } from '../utility/api';
import ErrorMessageModal from '../modals/ErrorMessageModal.svelte';
import ConfirmSqlModal, { runOperationOnDatabase, saveScriptToDatabase } from '../modals/ConfirmSqlModal.svelte';
import { filterAppsForDatabase } from '../utility/appTools';
import { filterAppsForDatabase, openApplicationEditor } from '../utility/appTools';
import newQuery from '../query/newQuery';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import { closeMultipleTabs } from '../tabpanel/TabsPanel.svelte';
@@ -639,7 +670,7 @@ await dbgateApi.executeQuery(${JSON.stringify(
import { getNumberIcon } from '../icons/FontIcon.svelte';
import { getDatabaseClickActionSetting } from '../settings/settingsTools';
import { _t } from '../translations';
import { dataGridRowHeight } from '../datagrid/DataGridRowHeightMeter.svelte';
import { tick } from 'svelte';
export let data;
export let passProps;
@@ -657,8 +688,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
}
$: isPinned = !!$pinnedDatabases.find(x => x?.name == data.name && x?.connection?._id == data.connection?._id);
$: apps = useUsedApps();
$: apps = useAllApps();
$: isLoadingSchemas = $loadingSchemaLists[`${data?.connection?._id}::${data?.name}`];
$: dbInfo = useDatabaseInfoPeek({ conid: data?.connection?._id, database: data?.name });
$: appsForDb = filterAppsForDatabase(data?.connection, data?.name, $apps, $dbInfo);
// $: console.log('AppsForDB:', data?.name, appsForDb);
</script>
<AppObjectCore
@@ -681,6 +717,13 @@ await dbgateApi.executeQuery(${JSON.stringify(
switchCurrentDatabase(data);
}
}}
additionalIcons={appsForDb?.length > 0
? appsForDb.map(ic => ({
icon: ic.applicationIcon || 'img app',
title: ic.applicationName,
colorClass: ic.applicationColor ? `color-icon-${ic.applicationColor}` : undefined,
}))
: null}
on:mousedown={() => {
$focusedConnectionOrDatabase = { conid: data.connection?._id, database: data.name, connection: data.connection };
}}

View File

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

View File

@@ -142,6 +142,18 @@
label: 'Model transform file',
};
const apps: FileTypeHandler = isProApp()
? {
icon: 'img app',
format: 'json',
tabComponent: 'AppEditorTab',
folder: 'apps',
currentConnection: false,
extension: 'json',
label: 'Application file',
}
: undefined;
export const SAVED_FILE_HANDLERS = {
sql,
shell,
@@ -154,6 +166,7 @@
modtrans,
datadeploy,
dbcompare,
apps,
};
export const extractKey = data => data.file;

View File

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

View File

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

View File

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

View File

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

View File

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

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
{...$$restProps}
value={$values[name] ?? defaultValue}
value={$values?.[name] ?? defaultValue}
on:input={e => setFieldValue(name, e.target['value'])}
on:input={e => {
if (saveOnInput) {

View File

@@ -1,48 +1,78 @@
<script lang="ts">
import _ from 'lodash';
import { createEventDispatcher } from 'svelte';
import { createEventDispatcher, tick } from 'svelte';
import SelectField from '../forms/SelectField.svelte';
import { currentDatabase } from '../stores';
import { filterAppsForDatabase } from '../utility/appTools';
import { useAppFolders, useUsedApps } from '../utility/metadataLoaders';
import { getConnectionInfo, useAllApps, useConnectionInfo, useDatabaseInfo } from '../utility/metadataLoaders';
import InlineButton from '../buttons/InlineButton.svelte';
import FontIcon from '../icons/FontIcon.svelte';
import { showModal } from '../modals/modalTools';
import InputTextModal from '../modals/InputTextModal.svelte';
import { apiCall } from '../utility/api';
export let value = '#new';
export let disableInitialize = false;
export let value = '';
export let conid;
export let database;
const dispatch = createEventDispatcher();
let selectFieldKey = 0;
$: appFolders = useAppFolders();
$: usedApps = useUsedApps();
$: dbInfo = useDatabaseInfo({ conid, database });
$: connectionInfo = useConnectionInfo({ conid });
$: {
if (!disableInitialize && value == '#new' && $currentDatabase) {
const filtered = filterAppsForDatabase($currentDatabase.connection, $currentDatabase.name, $usedApps || []);
const common = _.intersection(
($appFolders || []).map(x => x.name),
filtered.map(x => x.name)
);
if (common.length > 0) {
value = common[0] as string;
$: allApps = useAllApps();
$: apps = filterAppsForDatabase($connectionInfo, database, $allApps || [], $dbInfo);
$: if (apps?.length == 1) {
value = apps[0].appid;
selectFieldKey++;
dispatch('change', value);
}
async function handleAddNewApplication() {
showModal(InputTextModal, {
header: 'New application',
label: 'Application name',
value: _.startCase(database),
onConfirm: async appName => {
const newAppId = await apiCall('apps/create-app-from-db', {
appName,
server: $connectionInfo?.server,
database,
});
await tick();
value = newAppId;
dispatch('change', value);
}
}
},
});
}
</script>
<SelectField
isNative
{...$$restProps}
{value}
on:change={e => {
value = e.detail;
dispatch('change', value);
}}
options={[
{ label: '(New application linked to current DB)', value: '#new' },
...($appFolders || []).map(app => ({
label: app.name,
value: app.name,
})),
]}
/>
<div class="flex">
{#key selectFieldKey}
<SelectField
isNative
{...$$restProps}
{value}
on:change={e => {
value = e.detail;
dispatch('change', value);
}}
options={[
{
label: '(not selected)',
value: '',
},
...(apps || []).map(app => ({
label: app.applicationName,
value: app.appid,
})),
]}
/>
{/key}
<InlineButton on:click={handleAddNewApplication} square>
<FontIcon icon="icon plus-thick" padLeft padRight />
</InlineButton>
</div>

View File

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

View File

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

View File

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

View File

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

View File

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

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 _ from 'lodash';
import { match } from 'fuzzy';
import { getConnectionInfo } from './metadataLoaders';
import openNewTab from './openNewTab';
export async function saveDbToApp(conid: string, database: string, app: string) {
if (app == '#new') {
const folder = await apiCall('apps/create-folder', { folder: database });
// export async function saveDbToApp(conid: string, database: string, app: string) {
// const connection = await getConnectionInfo({ conid });
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${folder}`]: true,
},
});
// if (app == '#new') {
// const appJson = {
// applicationName: _.startCase(database),
// usageRules: [
// {
// serverHostsList: connection?.server ? [connection.server] : undefined,
// databaseNamesList: [database],
// conditionGroup: '1',
// },
// ],
// };
return folder;
// const file =
// const folder = await apiCall('apps/create-folder', { folder: database });
// await apiCall('connections/update-database', {
// conid,
// database,
// values: {
// [`useApp:${folder}`]: true,
// },
// });
// return folder;
// }
// await apiCall('connections/update-database', {
// conid,
// database,
// values: {
// [`useApp:${app}`]: true,
// },
// });
// return app;
// }
export function filterAppsForDatabase(
connection,
database: string,
apps: ApplicationDefinition[],
dbinfo: DatabaseInfo = null
): ApplicationDefinition[] {
if (!apps) {
return [];
}
await apiCall('connections/update-database', {
conid,
database,
values: {
[`useApp:${app}`]: true,
},
// console.log('ALL APPS:', apps);
// console.log('DB INFO:', dbinfo);
// console.log('CONNECTION:', connection);
// console.log('DATABASE:', database);
return apps.filter(app => {
const groupedConditions = _.groupBy(app.usageRules, rule => rule.conditionGroup || '1');
for (const group of Object.values(groupedConditions)) {
let groupMatch = true;
for (const rule of group) {
let ruleMatch = true;
if (rule.serverHostsRegex) {
const re = new RegExp(rule.serverHostsRegex);
ruleMatch = ruleMatch && !!connection?.server && re.test(connection.server);
}
if (rule.serverHostsList) {
ruleMatch = ruleMatch && !!connection?.server && rule.serverHostsList.includes(connection.server);
}
if (rule.databaseNamesRegex) {
const re = new RegExp(rule.databaseNamesRegex);
ruleMatch = ruleMatch && !!database && re.test(database);
}
if (rule.databaseNamesList) {
ruleMatch = ruleMatch && !!database && rule.databaseNamesList.includes(database);
}
let matchedTables = dbinfo?.tables;
if (rule.tableNamesRegex) {
const re = new RegExp(rule.tableNamesRegex);
matchedTables = dbinfo?.tables?.filter(table => !!table && re.test(table.pureName)) || [];
ruleMatch = ruleMatch && matchedTables.length > 0;
}
if (rule.tableNamesList) {
matchedTables =
dbinfo?.tables?.filter(table => !!table && rule.tableNamesList.includes(table.pureName)) || [];
ruleMatch = ruleMatch && matchedTables.length > 0;
}
if (rule.columnNamesRegex) {
const re = new RegExp(rule.columnNamesRegex);
ruleMatch =
ruleMatch &&
matchedTables.some(table => !!table?.columns?.some(column => !!column && re.test(column.columnName)));
}
if (rule.columnNamesList) {
ruleMatch =
ruleMatch &&
matchedTables.some(
table => !!table?.columns?.some(column => !!column && rule.columnNamesList.includes(column.columnName))
);
}
groupMatch = groupMatch && ruleMatch;
}
if (groupMatch) return true;
}
return false;
});
return app;
// const db = (connection?.databases || []).find(x => x.name == database);
// return apps?.filter(app => db && db[`useApp:${app.name}`]);
}
export function filterAppsForDatabase(connection, database: string, $apps): ApplicationDefinition[] {
const db = (connection?.databases || []).find(x => x.name == database);
return $apps?.filter(app => db && db[`useApp:${app.name}`]);
export async function openApplicationEditor(appid) {
const dataContent = await apiCall('files/load', { folder: 'apps', file: appid, format: 'json' });
openNewTab(
{
title: appid,
icon: 'img app',
tabComponent: 'AppEditorTab',
props: {
savedFile: appid,
savedFolder: 'apps',
savedFormat: 'json',
},
},
{ editor: dataContent }
);
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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