From 11a4f0ef329347478bac2bf51e49fa16e3305d99 Mon Sep 17 00:00:00 2001 From: Jan Prochazka Date: Thu, 11 Sep 2025 13:10:36 +0200 Subject: [PATCH] SYNC: Merge pull request #9 from dbgate/feature/apps --- app/src/mainMenuDefinition.js | 1 + packages/api/src/auth/authProvider.js | 4 + packages/api/src/controllers/apps.js | 561 +++++++++++------- packages/api/src/controllers/files.js | 7 +- packages/api/src/storageModel.js | 365 +++++++++--- packages/api/src/utility/hasPermission.js | 49 ++ packages/tools/src/nameTools.ts | 17 + packages/types/appdefs.d.ts | 50 +- packages/web/src/App.svelte | 4 +- .../src/admin/FolderPermissionChooser.svelte | 35 ++ packages/web/src/appobj/AppObjectCore.svelte | 6 + .../web/src/appobj/ConnectionAppObject.svelte | 6 +- .../web/src/appobj/DatabaseAppObject.svelte | 55 +- .../src/appobj/DatabaseObjectAppObject.svelte | 28 +- .../web/src/appobj/SavedFileAppObject.svelte | 13 + packages/web/src/appobj/appObjectTools.ts | 8 + packages/web/src/commands/stdCommands.ts | 49 +- .../src/datagrid/ColumnHeaderControl.svelte | 27 +- packages/web/src/datagrid/DataGridCore.svelte | 1 + .../web/src/datagrid/TableDataGrid.svelte | 4 +- packages/web/src/designer/Designer.svelte | 4 +- packages/web/src/forms/FormIconField.svelte | 444 ++++++++++++++ .../web/src/forms/FormTextFieldRaw.svelte | 2 +- .../src/forms/TargetApplicationSelect.svelte | 96 +-- packages/web/src/icons/FontIcon.svelte | 1 + .../DefineDictionaryDescriptionModal.svelte | 77 ++- .../src/modals/DictionaryLookupModal.svelte | 4 +- .../VirtualForeignKeyEditorModal.svelte | 9 +- packages/web/src/tabs/QueryDataTab.svelte | 29 +- packages/web/src/utility/appTools.ts | 146 ++++- packages/web/src/utility/cache.ts | 32 +- .../src/utility/dictionaryDescriptionTools.ts | 46 +- packages/web/src/utility/metadataLoaders.ts | 102 +++- packages/web/src/widgets/AppFilesList.svelte | 120 ---- packages/web/src/widgets/AppFolderList.svelte | 39 -- packages/web/src/widgets/AppWidget.svelte | 19 - .../web/src/widgets/SavedFilesList.svelte | 24 +- packages/web/src/widgets/SqlObjectList.svelte | 24 +- .../web/src/widgets/WidgetContainer.svelte | 9 +- .../web/src/widgets/WidgetIconPanel.svelte | 7 - 40 files changed, 1770 insertions(+), 754 deletions(-) create mode 100644 packages/web/src/admin/FolderPermissionChooser.svelte create mode 100644 packages/web/src/forms/FormIconField.svelte delete mode 100644 packages/web/src/widgets/AppFilesList.svelte delete mode 100644 packages/web/src/widgets/AppFolderList.svelte delete mode 100644 packages/web/src/widgets/AppWidget.svelte diff --git a/app/src/mainMenuDefinition.js b/app/src/mainMenuDefinition.js index c4de505d0..466999fc2 100644 --- a/app/src/mainMenuDefinition.js +++ b/app/src/mainMenuDefinition.js @@ -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 }, diff --git a/packages/api/src/auth/authProvider.js b/packages/api/src/auth/authProvider.js index 153782ae8..af57d0cd4 100644 --- a/packages/api/src/auth/authProvider.js +++ b/packages/api/src/auth/authProvider.js @@ -55,6 +55,10 @@ class AuthProviderBase { return []; } + async getCurrentFilePermissions(req) { + return []; + } + getLoginPageConnections() { return null; } diff --git a/packages/api/src/controllers/apps.js b/packages/api/src/controllers/apps.js index 43c2d6e28..156848d51 100644 --- a/packages/api/src/controllers/apps.js +++ b/packages/api/src/controllers/apps.js @@ -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; + // }, }; diff --git a/packages/api/src/controllers/files.js b/packages/api/src/controllers/files.js index 4d776c73c..57c1b68c2 100644 --- a/packages/api/src/controllers/files.js +++ b/packages/api/src/controllers/files.js @@ -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'); diff --git a/packages/api/src/storageModel.js b/packages/api/src/storageModel.js index 88c1e8346..3c8ee800f 100644 --- a/packages/api/src/storageModel.js +++ b/packages/api/src/storageModel.js @@ -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": [], diff --git a/packages/api/src/utility/hasPermission.js b/packages/api/src/utility/hasPermission.js index c34bd410c..2e9902c2f 100644 --- a/packages/api/src/utility/hasPermission.js +++ b/packages/api/src/utility/hasPermission.js @@ -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, diff --git a/packages/tools/src/nameTools.ts b/packages/tools/src/nameTools.ts index 617481a3e..b63922ac9 100644 --- a/packages/tools/src/nameTools.ts +++ b/packages/tools/src/nameTools.ts @@ -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' }, +]; diff --git a/packages/types/appdefs.d.ts b/packages/types/appdefs.d.ts index b9826bebf..549b9cd5c 100644 --- a/packages/types/appdefs.d.ts +++ b/packages/types/appdefs.d.ts @@ -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[]; } diff --git a/packages/web/src/App.svelte b/packages/web/src/App.svelte index 081f2a90f..4c08d2554 100644 --- a/packages/web/src/App.svelte +++ b/packages/web/src/App.svelte @@ -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) { diff --git a/packages/web/src/admin/FolderPermissionChooser.svelte b/packages/web/src/admin/FolderPermissionChooser.svelte new file mode 100644 index 000000000..860896fa3 --- /dev/null +++ b/packages/web/src/admin/FolderPermissionChooser.svelte @@ -0,0 +1,35 @@ + + + + +
+ + +
diff --git a/packages/web/src/appobj/AppObjectCore.svelte b/packages/web/src/appobj/AppObjectCore.svelte index a7884af35..6f37db654 100644 --- a/packages/web/src/appobj/AppObjectCore.svelte +++ b/packages/web/src/appobj/AppObjectCore.svelte @@ -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 @@ /> {/if} + {#if additionalIcons} + {#each additionalIcons as ic} + + {/each} + {/if} {#if extInfo} diff --git a/packages/web/src/appobj/ConnectionAppObject.svelte b/packages/web/src/appobj/ConnectionAppObject.svelte index c9df74b45..6a2c99c6f 100644 --- a/packages/web/src/appobj/ConnectionAppObject.svelte +++ b/packages/web/src/appobj/ConnectionAppObject.svelte @@ -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(); { + 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); 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 }; }} diff --git a/packages/web/src/appobj/DatabaseObjectAppObject.svelte b/packages/web/src/appobj/DatabaseObjectAppObject.svelte index 4486dee02..7f0941854 100644 --- a/packages/web/src/appobj/DatabaseObjectAppObject.svelte +++ b/packages/web/src/appobj/DatabaseObjectAppObject.svelte @@ -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, diff --git a/packages/web/src/appobj/SavedFileAppObject.svelte b/packages/web/src/appobj/SavedFileAppObject.svelte index 8132f9da2..01060e41a 100644 --- a/packages/web/src/appobj/SavedFileAppObject.svelte +++ b/packages/web/src/appobj/SavedFileAppObject.svelte @@ -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; diff --git a/packages/web/src/appobj/appObjectTools.ts b/packages/web/src/appobj/appObjectTools.ts index 3a7456a61..3aa047357 100644 --- a/packages/web/src/appobj/appObjectTools.ts +++ b/packages/web/src/appobj/appObjectTools.ts @@ -100,4 +100,12 @@ export const defaultDatabaseObjectAppObjectActions = { icon: 'img sql-file', }, ], + queries: [ + { + label: 'Show query', + tab: 'QueryDataTab', + defaultActionId: 'showAppQuery', + icon: 'img app-query', + }, + ], }; diff --git a/packages/web/src/commands/stdCommands.ts b/packages/web/src/commands/stdCommands.ts index 18a87182e..659a154f8 100644 --- a/packages/web/src/commands/stdCommands.ts +++ b/packages/web/src/commands/stdCommands.ts @@ -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', diff --git a/packages/web/src/datagrid/ColumnHeaderControl.svelte b/packages/web/src/datagrid/ColumnHeaderControl.svelte index 183d9d346..669e4e44e 100644 --- a/packages/web/src/datagrid/ColumnHeaderControl.svelte +++ b/packages/web/src/datagrid/ColumnHeaderControl.svelte @@ -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', + }, ]; } diff --git a/packages/web/src/datagrid/DataGridCore.svelte b/packages/web/src/datagrid/DataGridCore.svelte index 9e898a46c..83ff0dd9f 100644 --- a/packages/web/src/datagrid/DataGridCore.svelte +++ b/packages/web/src/datagrid/DataGridCore.svelte @@ -2003,6 +2003,7 @@ grouping={display.getGrouping(col.uniqueName)} {allowDefineVirtualReferences} seachInColumns={display.config?.searchInColumns} + onReload={refresh} /> {/each} diff --git a/packages/web/src/datagrid/TableDataGrid.svelte b/packages/web/src/datagrid/TableDataGrid.svelte index 9f4488222..46d0b434b 100644 --- a/packages/web/src/datagrid/TableDataGrid.svelte +++ b/packages/web/src/datagrid/TableDataGrid.svelte @@ -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(); diff --git a/packages/web/src/designer/Designer.svelte b/packages/web/src/designer/Designer.svelte index 37abea18e..c2feb55e1 100644 --- a/packages/web/src/designer/Designer.svelte +++ b/packages/web/src/designer/Designer.svelte @@ -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; diff --git a/packages/web/src/forms/FormIconField.svelte b/packages/web/src/forms/FormIconField.svelte new file mode 100644 index 000000000..6f5d53303 --- /dev/null +++ b/packages/web/src/forms/FormIconField.svelte @@ -0,0 +1,444 @@ + + + +
+
handleKeydown(e, togglePicker)} + role="button" + tabindex="0" + > + + {ICONS.find(icon => icon.icon === iconValue)?.name || '(Default icon)'} + +
+ + {#if showPicker} +
+
+ Choose an icon + + + +
+ +
+ {#each ICONS as { icon, name: iconDisplayName }} +
selectIcon(icon)} + on:keydown={e => handleKeydown(e, () => selectIcon(icon))} + role="button" + tabindex="0" + title={iconDisplayName} + > + + {iconDisplayName} +
+ {/each} +
+
+ {/if} +
+
+ + diff --git a/packages/web/src/forms/FormTextFieldRaw.svelte b/packages/web/src/forms/FormTextFieldRaw.svelte index 320dd7b20..6d9dee15b 100644 --- a/packages/web/src/forms/FormTextFieldRaw.svelte +++ b/packages/web/src/forms/FormTextFieldRaw.svelte @@ -11,7 +11,7 @@ setFieldValue(name, e.target['value'])} on:input={e => { if (saveOnInput) { diff --git a/packages/web/src/forms/TargetApplicationSelect.svelte b/packages/web/src/forms/TargetApplicationSelect.svelte index 44eb8aba7..7773e4727 100644 --- a/packages/web/src/forms/TargetApplicationSelect.svelte +++ b/packages/web/src/forms/TargetApplicationSelect.svelte @@ -1,48 +1,78 @@ - { - 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, - })), - ]} -/> +
+ {#key selectFieldKey} + { + value = e.detail; + dispatch('change', value); + }} + options={[ + { + label: '(not selected)', + value: '', + }, + ...(apps || []).map(app => ({ + label: app.applicationName, + value: app.appid, + })), + ]} + /> + {/key} + + + +
diff --git a/packages/web/src/icons/FontIcon.svelte b/packages/web/src/icons/FontIcon.svelte index 107fe1314..41a562df8 100644 --- a/packages/web/src/icons/FontIcon.svelte +++ b/packages/web/src/icons/FontIcon.svelte @@ -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', diff --git a/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte b/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte index 04057d340..16cc96c9c 100644 --- a/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte +++ b/packages/web/src/modals/DefineDictionaryDescriptionModal.svelte @@ -1,11 +1,10 @@ Define description + +
- - { + 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?.(); }} /> diff --git a/packages/web/src/modals/DictionaryLookupModal.svelte b/packages/web/src/modals/DictionaryLookupModal.svelte index 2333e5b39..ac27b5c73 100644 --- a/packages/web/src/modals/DictionaryLookupModal.svelte +++ b/packages/web/src/modals/DictionaryLookupModal.svelte @@ -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() { diff --git a/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte b/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte index 1eeee3a50..94d794039 100644 --- a/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte +++ b/packages/web/src/tableeditor/VirtualForeignKeyEditorModal.svelte @@ -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 @@
Target application
- +
@@ -181,10 +182,10 @@ { - const appFolder = await saveDbToApp(conid, database, dstApp); await apiCall('apps/save-virtual-reference', { - appFolder, + appid: dstApp, schemaName, pureName, refSchemaName, diff --git a/packages/web/src/tabs/QueryDataTab.svelte b/packages/web/src/tabs/QueryDataTab.svelte index 227414ec6..f188f1d83 100644 --- a/packages/web/src/tabs/QueryDataTab.svelte +++ b/packages/web/src/tabs/QueryDataTab.svelte @@ -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; - {#if jslid} - - {:else} + {#if loading} + {:else if jslid} + {#if fixedChartDefinition} + + {:else} + + {/if} {/if} - + {#if fixedChartDefinition} + Refresh + {:else} + + {/if} - + {#if !fixedChartDefinition} + + {/if} diff --git a/packages/web/src/utility/appTools.ts b/packages/web/src/utility/appTools.ts index 88a886601..9210048a5 100644 --- a/packages/web/src/utility/appTools.ts +++ b/packages/web/src/utility/appTools.ts @@ -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 } + ); } diff --git a/packages/web/src/utility/cache.ts b/packages/web/src/utility/cache.ts index 19a100d08..c32610691 100644 --- a/packages/web/src/utility/cache.ts +++ b/packages/web/src/utility/cache.ts @@ -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); diff --git a/packages/web/src/utility/dictionaryDescriptionTools.ts b/packages/web/src/utility/dictionaryDescriptionTools.ts index ec4f74380..d6c3129c6 100644 --- a/packages/web/src/utility/dictionaryDescriptionTools.ts +++ b/packages/web/src/utility/dictionaryDescriptionTools.ts @@ -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, +// }); +// } diff --git a/packages/web/src/utility/metadataLoaders.ts b/packages/web/src/utility/metadataLoaders.ts index d334efc08..29bed5957 100644 --- a/packages/web/src/utility/metadataLoaders.ts +++ b/packages/web/src/utility/metadataLoaders.ts @@ -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} */ 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 = {}) { diff --git a/packages/web/src/widgets/AppFilesList.svelte b/packages/web/src/widgets/AppFilesList.svelte deleted file mode 100644 index f1535af7e..000000000 --- a/packages/web/src/widgets/AppFilesList.svelte +++ /dev/null @@ -1,120 +0,0 @@ - - - - - - - - - - - - - - - ({ - fileName: file.name, - folderName: folder, - fileType: file.type, - fileLabel: file.label, - }))} - groupFunc={data => APP_LABELS[data.fileType] || 'App config'} - module={appFileAppObject} - {filter} - /> - diff --git a/packages/web/src/widgets/AppFolderList.svelte b/packages/web/src/widgets/AppFolderList.svelte deleted file mode 100644 index 4db2d38c5..000000000 --- a/packages/web/src/widgets/AppFolderList.svelte +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - runCommand('new.application')} title="Create new application"> - - - - - - - - - diff --git a/packages/web/src/widgets/AppWidget.svelte b/packages/web/src/widgets/AppWidget.svelte deleted file mode 100644 index 13074843e..000000000 --- a/packages/web/src/widgets/AppWidget.svelte +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - diff --git a/packages/web/src/widgets/SavedFilesList.svelte b/packages/web/src/widgets/SavedFilesList.svelte index 73f83a244..4dd592638 100644 --- a/packages/web/src/widgets/SavedFilesList.svelte +++ b/packages/web/src/widgets/SavedFilesList.svelte @@ -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) { diff --git a/packages/web/src/widgets/SqlObjectList.svelte b/packages/web/src/widgets/SqlObjectList.svelte index 33f070b2c..d33d8da97 100644 --- a/packages/web/src/widgets/SqlObjectList.svelte +++ b/packages/web/src/widgets/SqlObjectList.svelte @@ -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 @@ > ($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)} diff --git a/packages/web/src/widgets/WidgetContainer.svelte b/packages/web/src/widgets/WidgetContainer.svelte index bf970a8dc..24968bf58 100644 --- a/packages/web/src/widgets/WidgetContainer.svelte +++ b/packages/web/src/widgets/WidgetContainer.svelte @@ -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'; -