Merge branch 'develop'

This commit is contained in:
Jan Prochazka
2021-03-22 17:56:40 +01:00
464 changed files with 18032 additions and 25050 deletions

3
.gitignore vendored
View File

@@ -12,6 +12,8 @@ node_modules
build
dist
app/packages/web/public
# misc
.DS_Store
.env.local
@@ -24,3 +26,4 @@ yarn-debug.log*
yarn-error.log*
app/src/nativeModulesContent.js
packages/api/src/nativeModulesContent.js
.VSCodeCounter

View File

@@ -68,10 +68,10 @@
"start": "cross-env ELECTRON_START_URL=http://localhost:5000 electron .",
"start:local": "cross-env electron .",
"dist": "electron-builder",
"build": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn dist",
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build:app && cd ../../app && yarn predist",
"build": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn dist",
"build:local": "cd ../packages/api && yarn build && cd ../web && yarn build && cd ../../app && yarn predist",
"postinstall": "electron-builder install-app-deps",
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/build/*\" packages && copyfiles \"../packages/web/build/**/*\" packages"
"predist": "copyfiles ../packages/api/dist/* packages && copyfiles \"../packages/web/public/*\" packages && copyfiles \"../packages/web/public/**/*\" packages"
},
"main": "src/electron.js",
"devDependencies": {

View File

@@ -27,6 +27,8 @@ autoUpdater.logger = log;
// TODO - create settings for this
// appUpdater.channel = 'beta';
let commands = {};
function hideSplash() {
if (splashWindow) {
splashWindow.destroy();
@@ -35,61 +37,35 @@ function hideSplash() {
mainWindow.show();
}
function commandItem(id) {
const command = commands[id];
return {
id,
label: command ? command.menuName || command.toolbarName || command.name : id,
accelerator: command ? command.keyText : undefined,
enabled: command ? command.enabled : false,
click() {
mainWindow.webContents.executeJavaScript(`dbgate_runCommand('${id}')`);
},
};
}
function buildMenu() {
const template = [
{
label: 'File',
submenu: [
{
label: 'Connect to database',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_createNewConnection()`);
},
},
{
label: 'Open file',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_openFile()`);
},
},
{
label: 'Save',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('save')`);
},
accelerator: 'Ctrl+S',
id: 'save',
},
{
label: 'Save As',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_tabCommand('saveAs')`);
},
accelerator: 'Ctrl+Shift+S',
id: 'saveAs',
},
commandItem('new.connection'),
commandItem('file.open'),
commandItem('group.save'),
commandItem('group.saveAs'),
{ type: 'separator' },
{ role: 'close' },
],
},
{
label: 'Window',
submenu: [
{
label: 'New query',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_newQuery()`);
},
},
{ type: 'separator' },
{
label: 'Close all tabs',
click() {
mainWindow.webContents.executeJavaScript('dbgate_closeAll()');
},
},
{ role: 'minimize' },
],
submenu: [commandItem('new.query'), { type: 'separator' }, commandItem('tabs.closeAll'), { role: 'minimize' }],
},
// {
@@ -144,12 +120,7 @@ function buildMenu() {
require('electron').shell.openExternal('https://github.com/dbgate/dbgate/issues/new');
},
},
{
label: 'About',
click() {
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
},
},
commandItem('about.show'),
],
},
];
@@ -157,10 +128,22 @@ function buildMenu() {
return Menu.buildFromTemplate(template);
}
ipcMain.on('update-menu', async (event, arg) => {
const commands = await mainWindow.webContents.executeJavaScript(`dbgate_getCurrentTabCommands()`);
mainMenu.getMenuItemById('save').enabled = !!commands.save;
mainMenu.getMenuItemById('saveAs').enabled = !!commands.saveAs;
ipcMain.on('update-commands', async (event, arg) => {
commands = JSON.parse(arg);
for (const key of Object.keys(commands)) {
const menu = mainMenu.getMenuItemById(key);
if (!menu) continue;
const command = commands[key];
// rebuild menu
if (menu.label != command.text || menu.accelerator != command.keyText) {
mainMenu = buildMenu();
mainWindow.setMenu(mainMenu);
return;
}
menu.enabled = command.enabled;
}
});
function createWindow() {
@@ -186,7 +169,7 @@ function createWindow() {
const startUrl =
process.env.ELECTRON_START_URL ||
url.format({
pathname: path.join(__dirname, '../packages/web/build/index.html'),
pathname: path.join(__dirname, '../packages/web/public/index.html'),
protocol: 'file:',
slashes: true,
});

View File

@@ -9,7 +9,7 @@
"start:api": "yarn workspace dbgate-api start",
"start:api:portal": "yarn workspace dbgate-api start:portal",
"start:api:covid": "yarn workspace dbgate-api start:covid",
"start:web": "yarn workspace dbgate-web start",
"start:web": "yarn workspace dbgate-web dev",
"start:sqltree": "yarn workspace dbgate-sqltree start",
"start:tools": "yarn workspace dbgate-tools start",
"start:datalib": "yarn workspace dbgate-datalib start",
@@ -21,7 +21,7 @@
"build:lib": "yarn build:tools && yarn build:sqltree && yarn build:filterparser && yarn build:datalib",
"build:app": "cd app && yarn install && yarn build",
"build:api": "yarn workspace dbgate-api build",
"build:web:docker": "yarn workspace dbgate-web build:docker",
"build:web:docker": "yarn workspace dbgate-web build",
"build:app:local": "cd app && yarn build:local",
"start:app:local": "cd app && yarn start:local",
"setCurrentVersion": "node setCurrentVersion",
@@ -29,7 +29,7 @@
"fillNativeModules": "node fillNativeModules",
"fillNativeModulesElectron": "node fillNativeModules --eletron",
"prettier": "prettier --write packages/api/src && prettier --write packages/datalib/src && prettier --write packages/filterparser/src && prettier --write packages/sqltree/src && prettier --write packages/tools/src && prettier --write packages/types && prettier --write packages/web/src && prettier --write app/src",
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/public/* docker -u 2 && copyfiles \"packages/web/public/**/*\" docker -u 2",
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
"prepare": "yarn build:lib",
"start": "concurrently --kill-others-on-fail \"yarn start:api\" \"yarn start:web\"",

View File

@@ -121,7 +121,13 @@ module.exports = {
getStats_meta: 'get',
getStats({ jslid }) {
const file = `${getJslFileName(jslid)}.stats`;
if (fs.existsSync(file)) return JSON.parse(fs.readFileSync(file, 'utf-8'));
if (fs.existsSync(file)) {
try {
return JSON.parse(fs.readFileSync(file, 'utf-8'));
} catch (e) {
return {};
}
}
return {};
},

View File

@@ -29,7 +29,7 @@ const hasPermission = require('../utility/hasPermission');
const preinstallPluginMinimalVersions = {
'dbgate-plugin-mssql': '1.1.0',
'dbgate-plugin-mysql': '1.1.0',
'dbgate-plugin-mysql': '1.1.1',
'dbgate-plugin-postgres': '1.1.0',
'dbgate-plugin-csv': '1.0.8',
'dbgate-plugin-excel': '1.0.6',
@@ -149,7 +149,7 @@ module.exports = {
return content.commands[command](args);
},
authTypes_meta: 'post',
authTypes_meta: 'get',
async authTypes({ engine }) {
const packageName = extractPackageName(engine);
const content = requirePlugin(packageName);

View File

@@ -347,5 +347,6 @@ export function changeSetInsertNewRow(changeSet: ChangeSet, name?: NamedObjectIn
}
export function changeSetContainsChanges(changeSet: ChangeSet) {
if (!changeSet) return false;
return changeSet.deletes.length > 0 || changeSet.updates.length > 0 || changeSet.inserts.length > 0;
}

View File

@@ -27,6 +27,7 @@ export interface EngineAuthType {
export interface EngineDriver {
engine: string;
title: string;
defaultPort?: number;
connect({ server, port, user, password, database }): any;
query(pool: any, sql: string): Promise<QueryResult>;
stream(pool: any, sql: string, options: StreamOptions);

View File

@@ -21,6 +21,11 @@ export interface FileFormatDefinition {
getOutputParams?: (sourceName, values) => any;
}
export interface ThemeDefinition {
className: string;
themeName: string;
}
export interface PluginDefinition {
packageName: string;
manifest: any;
@@ -31,4 +36,5 @@ export interface ExtensionsDirectory {
plugins: PluginDefinition[];
fileFormats: FileFormatDefinition[];
drivers: EngineDriver[];
themes: ThemeDefinition[];
}

View File

@@ -1,28 +0,0 @@
module.exports = {
"env": {
"browser": true,
"es6": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react"
],
"rules": {
"react/prop-types": "off",
"no-unused-vars": "warn"
}
};

105
packages/web/README.md Normal file
View File

@@ -0,0 +1,105 @@
*Looking for a shareable component template? Go here --> [sveltejs/component-template](https://github.com/sveltejs/component-template)*
---
# svelte app
This is a project template for [Svelte](https://svelte.dev) apps. It lives at https://github.com/sveltejs/template.
To create a new project based on this template using [degit](https://github.com/Rich-Harris/degit):
```bash
npx degit sveltejs/template svelte-app
cd svelte-app
```
*Note that you will need to have [Node.js](https://nodejs.org) installed.*
## Get started
Install the dependencies...
```bash
cd svelte-app
npm install
```
...then start [Rollup](https://rollupjs.org):
```bash
npm run dev
```
Navigate to [localhost:5000](http://localhost:5000). You should see your app running. Edit a component file in `src`, save it, and reload the page to see your changes.
By default, the server will only respond to requests from localhost. To allow connections from other computers, edit the `sirv` commands in package.json to include the option `--host 0.0.0.0`.
If you're using [Visual Studio Code](https://code.visualstudio.com/) we recommend installing the official extension [Svelte for VS Code](https://marketplace.visualstudio.com/items?itemName=svelte.svelte-vscode). If you are using other editors you may need to install a plugin in order to get syntax highlighting and intellisense.
## Building and running in production mode
To create an optimised version of the app:
```bash
npm run build
```
You can run the newly built app with `npm run start`. This uses [sirv](https://github.com/lukeed/sirv), which is included in your package.json's `dependencies` so that the app will work when you deploy to platforms like [Heroku](https://heroku.com).
## Single-page app mode
By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.
If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:
```js
"start": "sirv public --single"
```
## Using TypeScript
This template comes with a script to set up a TypeScript development environment, you can run it immediately after cloning the template with:
```bash
node scripts/setupTypeScript.js
```
Or remove the script via:
```bash
rm scripts/setupTypeScript.js
```
## Deploying to the web
### With [Vercel](https://vercel.com)
Install `vercel` if you haven't already:
```bash
npm install -g vercel
```
Then, from within your project folder:
```bash
cd public
vercel deploy --name my-project
```
### With [surge](https://surge.sh/)
Install `surge` if you haven't already:
```bash
npm install -g surge
```
Then, from within your project folder:
```bash
npm run build
surge public my-project.surge.sh
```

View File

@@ -1,69 +1,51 @@
{
"name": "dbgate-web",
"version": "3.9.5",
"files": [
"build"
],
"version": "1.0.0",
"scripts": {
"start": "cross-env BROWSER=none PORT=5000 react-scripts start",
"build:docker": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
"build:app": "cross-env PUBLIC_URL=. CI=false react-scripts build",
"build": "cross-env CI=false REACT_APP_API_URL=ORIGIN react-scripts build",
"prepublishOnly": "yarn build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"ts": "tsc"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"build": "cross-env API_URL=ORIGIN rollup -c",
"dev": "cross-env API_URL=http://localhost:3000 rollup -c -w",
"start": "sirv public",
"validate": "svelte-check"
},
"devDependencies": {
"@types/react": "^16.9.17",
"@types/styled-components": "^4.4.2",
"dbgate-types": "^3.9.5",
"typescript": "^3.7.4",
"@ant-design/colors": "^5.0.0",
"@mdi/font": "^5.8.55",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@rollup/plugin-commonjs": "^17.0.0",
"@rollup/plugin-node-resolve": "^11.0.0",
"@rollup/plugin-replace": "^2.4.1",
"@rollup/plugin-typescript": "^6.0.0",
"@tsconfig/svelte": "^1.0.0",
"ace-builds": "^1.4.8",
"axios": "^0.19.0",
"chart.js": "^2.9.4",
"compare-versions": "^3.6.0",
"cross-env": "^6.0.3",
"cross-env": "^7.0.3",
"dbgate-datalib": "^3.9.5",
"dbgate-sqltree": "^3.9.5",
"dbgate-tools": "^3.9.5",
"eslint": "^6.8.0",
"eslint-plugin-react": "^7.17.0",
"dbgate-types": "^3.9.5",
"json-stable-stringify": "^1.0.1",
"localforage": "^1.9.0",
"markdown-to-jsx": "^7.1.0",
"lodash": "^4.17.15",
"randomcolor": "^0.6.2",
"react": "^16.12.0",
"react-ace": "^8.0.0",
"react-chartjs-2": "^2.11.1",
"react-dom": "^16.12.0",
"react-dropzone": "^11.2.3",
"react-helmet": "^6.1.0",
"react-json-view": "^1.19.1",
"react-modal": "^3.11.1",
"react-scripts": "3.3.0",
"react-select": "^3.1.0",
"resize-observer-polyfill": "^1.5.1",
"rollup": "^2.3.4",
"rollup-plugin-copy": "^3.3.0",
"rollup-plugin-css-only": "^3.1.0",
"rollup-plugin-livereload": "^2.0.0",
"rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0",
"socket.io-client": "^2.3.0",
"sql-formatter": "^2.3.3",
"styled-components": "^4.4.1",
"svelte": "^3.35.0",
"svelte-check": "^1.0.0",
"svelte-preprocess": "^4.0.0",
"tslib": "^2.0.0",
"typescript": "^3.9.3",
"uuid": "^3.4.0"
},
"dependencies": {
"@mdi/font": "^5.9.55",
"file-selector": "^0.2.4",
"resize-observer-polyfill": "^1.5.1",
"sirv-cli": "^1.0.0",
"svelte-markdown": "^0.1.4",
"svelte-select": "^3.17.0"
}
}
}

419
packages/web/public/bulma.css vendored Normal file
View File

@@ -0,0 +1,419 @@
.m-0 {
margin: 0 !important;
}
.mt-0 {
margin-top: 0 !important;
}
.mr-0 {
margin-right: 0 !important;
}
.mb-0 {
margin-bottom: 0 !important;
}
.ml-0 {
margin-left: 0 !important;
}
.mx-0 {
margin-left: 0 !important;
margin-right: 0 !important;
}
.my-0 {
margin-top: 0 !important;
margin-bottom: 0 !important;
}
.m-1 {
margin: 0.25rem !important;
}
.mt-1 {
margin-top: 0.25rem !important;
}
.mr-1 {
margin-right: 0.25rem !important;
}
.mb-1 {
margin-bottom: 0.25rem !important;
}
.ml-1 {
margin-left: 0.25rem !important;
}
.mx-1 {
margin-left: 0.25rem !important;
margin-right: 0.25rem !important;
}
.my-1 {
margin-top: 0.25rem !important;
margin-bottom: 0.25rem !important;
}
.m-2 {
margin: 0.5rem !important;
}
.mt-2 {
margin-top: 0.5rem !important;
}
.mr-2 {
margin-right: 0.5rem !important;
}
.mb-2 {
margin-bottom: 0.5rem !important;
}
.ml-2 {
margin-left: 0.5rem !important;
}
.mx-2 {
margin-left: 0.5rem !important;
margin-right: 0.5rem !important;
}
.my-2 {
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.m-3 {
margin: 0.75rem !important;
}
.mt-3 {
margin-top: 0.75rem !important;
}
.mr-3 {
margin-right: 0.75rem !important;
}
.mb-3 {
margin-bottom: 0.75rem !important;
}
.ml-3 {
margin-left: 0.75rem !important;
}
.mx-3 {
margin-left: 0.75rem !important;
margin-right: 0.75rem !important;
}
.my-3 {
margin-top: 0.75rem !important;
margin-bottom: 0.75rem !important;
}
.m-4 {
margin: 1rem !important;
}
.mt-4 {
margin-top: 1rem !important;
}
.mr-4 {
margin-right: 1rem !important;
}
.mb-4 {
margin-bottom: 1rem !important;
}
.ml-4 {
margin-left: 1rem !important;
}
.mx-4 {
margin-left: 1rem !important;
margin-right: 1rem !important;
}
.my-4 {
margin-top: 1rem !important;
margin-bottom: 1rem !important;
}
.m-5 {
margin: 1.5rem !important;
}
.mt-5 {
margin-top: 1.5rem !important;
}
.mr-5 {
margin-right: 1.5rem !important;
}
.mb-5 {
margin-bottom: 1.5rem !important;
}
.ml-5 {
margin-left: 1.5rem !important;
}
.mx-5 {
margin-left: 1.5rem !important;
margin-right: 1.5rem !important;
}
.my-5 {
margin-top: 1.5rem !important;
margin-bottom: 1.5rem !important;
}
.m-6 {
margin: 3rem !important;
}
.mt-6 {
margin-top: 3rem !important;
}
.mr-6 {
margin-right: 3rem !important;
}
.mb-6 {
margin-bottom: 3rem !important;
}
.ml-6 {
margin-left: 3rem !important;
}
.mx-6 {
margin-left: 3rem !important;
margin-right: 3rem !important;
}
.my-6 {
margin-top: 3rem !important;
margin-bottom: 3rem !important;
}
.p-0 {
padding: 0 !important;
}
.pt-0 {
padding-top: 0 !important;
}
.pr-0 {
padding-right: 0 !important;
}
.pb-0 {
padding-bottom: 0 !important;
}
.pl-0 {
padding-left: 0 !important;
}
.px-0 {
padding-left: 0 !important;
padding-right: 0 !important;
}
.py-0 {
padding-top: 0 !important;
padding-bottom: 0 !important;
}
.p-1 {
padding: 0.25rem !important;
}
.pt-1 {
padding-top: 0.25rem !important;
}
.pr-1 {
padding-right: 0.25rem !important;
}
.pb-1 {
padding-bottom: 0.25rem !important;
}
.pl-1 {
padding-left: 0.25rem !important;
}
.px-1 {
padding-left: 0.25rem !important;
padding-right: 0.25rem !important;
}
.py-1 {
padding-top: 0.25rem !important;
padding-bottom: 0.25rem !important;
}
.p-2 {
padding: 0.5rem !important;
}
.pt-2 {
padding-top: 0.5rem !important;
}
.pr-2 {
padding-right: 0.5rem !important;
}
.pb-2 {
padding-bottom: 0.5rem !important;
}
.pl-2 {
padding-left: 0.5rem !important;
}
.px-2 {
padding-left: 0.5rem !important;
padding-right: 0.5rem !important;
}
.py-2 {
padding-top: 0.5rem !important;
padding-bottom: 0.5rem !important;
}
.p-3 {
padding: 0.75rem !important;
}
.pt-3 {
padding-top: 0.75rem !important;
}
.pr-3 {
padding-right: 0.75rem !important;
}
.pb-3 {
padding-bottom: 0.75rem !important;
}
.pl-3 {
padding-left: 0.75rem !important;
}
.px-3 {
padding-left: 0.75rem !important;
padding-right: 0.75rem !important;
}
.py-3 {
padding-top: 0.75rem !important;
padding-bottom: 0.75rem !important;
}
.p-4 {
padding: 1rem !important;
}
.pt-4 {
padding-top: 1rem !important;
}
.pr-4 {
padding-right: 1rem !important;
}
.pb-4 {
padding-bottom: 1rem !important;
}
.pl-4 {
padding-left: 1rem !important;
}
.px-4 {
padding-left: 1rem !important;
padding-right: 1rem !important;
}
.py-4 {
padding-top: 1rem !important;
padding-bottom: 1rem !important;
}
.p-5 {
padding: 1.5rem !important;
}
.pt-5 {
padding-top: 1.5rem !important;
}
.pr-5 {
padding-right: 1.5rem !important;
}
.pb-5 {
padding-bottom: 1.5rem !important;
}
.pl-5 {
padding-left: 1.5rem !important;
}
.px-5 {
padding-left: 1.5rem !important;
padding-right: 1.5rem !important;
}
.py-5 {
padding-top: 1.5rem !important;
padding-bottom: 1.5rem !important;
}
.p-6 {
padding: 3rem !important;
}
.pt-6 {
padding-top: 3rem !important;
}
.pr-6 {
padding-right: 3rem !important;
}
.pb-6 {
padding-bottom: 3rem !important;
}
.pl-6 {
padding-left: 3rem !important;
}
.px-6 {
padding-left: 3rem !important;
padding-right: 3rem !important;
}
.py-6 {
padding-top: 3rem !important;
padding-bottom: 3rem !important;
}

View File

@@ -0,0 +1,22 @@
:root {
--dim-widget-icon-size: 60px;
--dim-statusbar-height: 20px;
--dim-left-panel-width: 300px;
--dim-tabs-panel-height: 53px;
--dim-tabs-height: 33px;
--dim-splitter-thickness: 3px;
--dim-visible-left-panel: 1; /* set from JS */
--dim-content-left: calc(
var(--dim-widget-icon-size) + var(--dim-visible-left-panel) *
(var(--dim-left-panel-width) + var(--dim-splitter-thickness))
);
--dim-visible-toolbar: 1; /* set from JS */
--dim-toolbar-height: 30px;
--dim-header-top: calc(var(--dim-toolbar-height) * var(--dim-visible-toolbar));
--dim-content-top: calc(var(--dim-header-top) + var(--dim-tabs-panel-height));
--dim-large-form-margin: 20px;
}

View File

@@ -0,0 +1,142 @@
body {
font-family: -apple-system, BlinkMacSystemFont, Segoe WPC, Segoe UI, HelveticaNeue-Light, Ubuntu, Droid Sans,
sans-serif;
font-size: 14px;
/* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
*/
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.horizontal-split-handle {
background-color: var(--theme-border);
width: var(--dim-splitter-thickness);
cursor: col-resize;
}
.horizontal-split-handle:hover {
background-color: var(--theme-bg-2);
}
.vertical-split-handle {
background-color: var(--theme-border);
height: var(--dim-splitter-thickness);
cursor: row-resize;
}
.vertical-split-handle:hover {
background-color: var(--theme-bg-2);
}
.icon-invisible {
visibility: hidden;
}
.space-between {
display: flex;
justify-content: space-between;
}
.flex {
display: flex;
}
.nowrap {
white-space: nowrap;
}
.bold {
font-weight: bold;
}
.flex1 {
flex: 1;
}
.col-9 {
flex-basis: 75%;
max-width: 75%;
}
.col-8 {
flex-basis: 66.6667%;
max-width: 66.6667%;
}
.col-6 {
flex-basis: 50%;
max-width: 50%;
}
.col-4 {
flex-basis: 33.3333%;
max-width: 33.3333%;
}
.col-3 {
flex-basis: 25%;
max-width: 25%;
}
.largeFormMarker input[type='text'] {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}
.largeFormMarker input[type='password'] {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}
.largeFormMarker select {
width: 100%;
padding: 10px 10px;
font-size: 14px;
box-sizing: border-box;
border-radius: 4px;
}
body *::-webkit-scrollbar {
height: 0.8em;
width: 0.8em;
}
body *::-webkit-scrollbar-track {
border-radius: 1px;
background-color: var(--theme-bg-1);
}
body *::-webkit-scrollbar-corner {
border-radius: 1px;
background-color: var(--theme-bg-2);
}
body *::-webkit-scrollbar-thumb {
border-radius: 1px;
background-color: var(--theme-bg-3);
}
body *::-webkit-scrollbar-thumb:hover {
background-color: var(--theme-bg-4);
}
input {
background-color: var(--theme-bg-0);
color: var(--theme-font-1);
border: 1px solid var(--theme-border);
}
input[disabled] {
background-color: var(--theme-bg-1);
}
select {
background-color: var(--theme-bg-0);
color: var(--theme-font-1);
border: 1px solid var(--theme-border);
}
select[disabled] {
background-color: var(--theme-bg-1);
}
textarea {
background-color: var(--theme-bg-0);
color: var(--theme-font-1);
border: 1px solid var(--theme-border);
}

View File

@@ -0,0 +1,32 @@
.color-icon-blue {
color: var(--theme-icon-blue);
}
.color-icon-green {
color: var(--theme-icon-green);
}
.color-icon-red {
color: var(--theme-icon-red);
}
.color-icon-gold {
color: var(--theme-icon-gold);
}
.color-icon-yellow {
color: var(--theme-icon-yellow);
}
.color-icon-magenta {
color: var(--theme-icon-magenta);
}
.color-icon-inv-green {
color: var(--theme-icon-inv-green);
}
.color-icon-inv-red {
color: var(--theme-icon-inv-red);
}

View File

@@ -2,43 +2,28 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description"
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>DbGate</title>
<title>DbGate</title>
<meta name="theme-color" content="#000000" />
<meta name="description"
content="DbGate - web based opensource database administration tool for MS SQL, MySQL, Postgre SQL" />
<link rel='icon' type='image/png' href='favicon.ico'>
<link rel="manifest" href="manifest.json" />
<link rel='stylesheet' href='global.css'>
<link rel='stylesheet' href='dimensions.css'>
<link rel='stylesheet' href='bulma.css'>
<link rel='stylesheet' href='icon-colors.css'>
<link rel='stylesheet' href='build/bundle.css'>
<link rel='stylesheet' href='build/fonts/materialdesignicons.css'>
<script defer src='build/bundle.js'></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root">Loading DbGate...</div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -1,2 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *

View File

@@ -1,16 +0,0 @@
body {
background: #666;
}
div {
color: white;
font-size: 25pt;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
text-align: center;
font-family: Arial, Helvetica, sans-serif;
margin-top: 40px;
}

View File

@@ -1,12 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="splash.css">
</head>
<body>
<div>Starting DbGate...</div>
</body>
</html>

View File

@@ -0,0 +1,102 @@
import svelte from 'rollup-plugin-svelte';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import livereload from 'rollup-plugin-livereload';
import copy from 'rollup-plugin-copy';
import { terser } from 'rollup-plugin-terser';
import sveltePreprocess from 'svelte-preprocess';
import typescript from '@rollup/plugin-typescript';
import replace from '@rollup/plugin-replace';
import css from 'rollup-plugin-css-only';
const production = !process.env.ROLLUP_WATCH;
function serve() {
let server;
function toExit() {
if (server) server.kill(0);
}
return {
writeBundle() {
if (server) return;
server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true,
});
process.on('SIGTERM', toExit);
process.on('exit', toExit);
},
};
}
export default {
input: 'src/main.ts',
output: {
sourcemap: true,
format: 'iife',
name: 'app',
file: 'public/build/bundle.js',
},
plugins: [
copy({
targets: [
{
src: '../../node_modules/@mdi/font/css/materialdesignicons.css',
dest: 'public/build/fonts/',
},
{
src: '../../node_modules/@mdi/font/fonts/*',
dest: 'public/build/fonts/',
},
],
}),
replace({
'process.env.API_URL': JSON.stringify(process.env.API_URL),
}),
svelte({
preprocess: sveltePreprocess({ sourceMap: !production }),
compilerOptions: {
// enable run-time checks when not in production
dev: !production,
},
}),
// we'll extract any component CSS out into
// a separate file - better for performance
css({ output: 'bundle.css' }),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte'],
}),
commonjs(),
typescript({
sourceMap: !production,
inlineSources: !production,
}),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser(),
],
watch: {
clearScreen: false,
},
};

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,57 +0,0 @@
import React from 'react';
import './index.css';
import Screen from './Screen';
import {
CurrentWidgetProvider,
CurrentDatabaseProvider,
OpenedTabsProvider,
OpenedConnectionsProvider,
LeftPanelWidthProvider,
CurrentArchiveProvider,
CurrentThemeProvider,
} from './utility/globalState';
import { SocketProvider } from './utility/SocketProvider';
import ConnectionsPinger from './utility/ConnectionsPinger';
import { ModalLayerProvider } from './modals/showModal';
import UploadsProvider from './utility/UploadsProvider';
import ThemeHelmet from './themes/ThemeHelmet';
import PluginsProvider from './plugins/PluginsProvider';
import { ExtensionsProvider } from './utility/useExtensions';
import { MenuLayerProvider } from './modals/showMenu';
function App() {
return (
<CurrentWidgetProvider>
<CurrentDatabaseProvider>
<SocketProvider>
<OpenedTabsProvider>
<OpenedConnectionsProvider>
<LeftPanelWidthProvider>
<ConnectionsPinger>
<PluginsProvider>
<ExtensionsProvider>
<CurrentArchiveProvider>
<CurrentThemeProvider>
<UploadsProvider>
<ModalLayerProvider>
<MenuLayerProvider>
<ThemeHelmet />
<Screen />
</MenuLayerProvider>
</ModalLayerProvider>
</UploadsProvider>
</CurrentThemeProvider>
</CurrentArchiveProvider>
</ExtensionsProvider>
</PluginsProvider>
</ConnectionsPinger>
</LeftPanelWidthProvider>
</OpenedConnectionsProvider>
</OpenedTabsProvider>
</SocketProvider>
</CurrentDatabaseProvider>
</CurrentWidgetProvider>
);
}
export default App;

View File

@@ -0,0 +1,10 @@
<script lang="ts">
import CommandListener from './commands/CommandListener.svelte';
import PluginsProvider from './plugins/PluginsProvider.svelte';
import Screen from './Screen.svelte';
</script>
<PluginsProvider />
<CommandListener />
<Screen />

View File

@@ -1,11 +0,0 @@
// @ts-nocheck
import React from 'react';
import { render } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -1,62 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import getElectron from './utility/getElectron';
import useExtensions from './utility/useExtensions';
const TargetStyled = styled.div`
position: fixed;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: ${props => props.theme.main_background_blue[3]};
align-items: center;
justify-content: space-around;
z-index: 1000;
`;
const InfoBox = styled.div``;
const IconWrapper = styled.div`
display: flex;
justify-content: space-around;
font-size: 50px;
margin-bottom: 20px;
`;
const InfoWrapper = styled.div`
display: flex;
justify-content: space-around;
margin-top: 10px;
`;
const TitleWrapper = styled.div`
font-size: 30px;
display: flex;
justify-content: space-around;
`;
export default function DragAndDropFileTarget({ isDragActive, inputProps }) {
const theme = useTheme();
const { fileFormats } = useExtensions();
const electron = getElectron();
const fileTypeNames = fileFormats.filter(x => x.readerFunc).map(x => x.name);
if (electron) fileTypeNames.push('SQL');
return (
!!isDragActive && (
<TargetStyled theme={theme}>
<InfoBox>
<IconWrapper>
<FontIcon icon="icon cloud-upload" />
</IconWrapper>
<TitleWrapper>Drop the files to upload to DbGate</TitleWrapper>
<InfoWrapper>Supported file types: {fileTypeNames.join(', ')}</InfoWrapper>
</InfoBox>
<input {...inputProps} />
</TargetStyled>
)
);
}

View File

@@ -0,0 +1,55 @@
<script lang="ts">
import _ from 'lodash';
import FontIcon from './icons/FontIcon.svelte';
import { extensions } from './stores';
import getElectron from './utility/getElectron';
const electron = getElectron();
$: fileTypeNames = _.compact([
...$extensions.fileFormats.filter(x => x.readerFunc).map(x => x.name),
electron ? 'SQL' : null,
]);
</script>
<div class="target">
<div>
<div class="icon">
<FontIcon icon="icon cloud-upload" />
</div>
<div class="title">Drop the files to upload to DbGate</div>
<div class="info">Supported file types: {fileTypeNames.join(', ')}</div>
</div>
</div>
<style>
.target {
position: fixed;
display: flex;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--theme-bg-selected);
align-items: center;
justify-content: space-around;
z-index: 1000;
}
.icon {
display: flex;
justify-content: space-around;
font-size: 50px;
margin-bottom: 20px;
}
.info {
display: flex;
justify-content: space-around;
margin-top: 10px;
}
.title {
font-size: 30px;
display: flex;
justify-content: space-around;
}
</style>

View File

@@ -1,147 +0,0 @@
// @ts-nocheck
import React from 'react';
import dimensions from './theme/dimensions';
import styled from 'styled-components';
import TabsPanel from './TabsPanel';
import TabContent from './TabContent';
import WidgetIconPanel from './widgets/WidgetIconPanel';
import { useCurrentWidget, useLeftPanelWidth, useSetLeftPanelWidth } from './utility/globalState';
import WidgetContainer from './widgets/WidgetContainer';
import ToolBar from './widgets/Toolbar';
import StatusBar from './widgets/StatusBar';
import { useSplitterDrag, HorizontalSplitHandle } from './widgets/Splitter';
import { ModalLayer } from './modals/showModal';
import DragAndDropFileTarget from './DragAndDropFileTarget';
import { useUploadsZone } from './utility/UploadsProvider';
import useTheme from './theme/useTheme';
import { MenuLayer } from './modals/showMenu';
import ErrorBoundary, { ErrorBoundaryTest } from './utility/ErrorBoundary';
const BodyDiv = styled.div`
position: fixed;
top: ${dimensions.tabsPanel.height + dimensions.toolBar.height}px;
left: ${props => props.contentLeft}px;
bottom: ${dimensions.statusBar.height}px;
right: 0;
background-color: ${props => props.theme.content_background};
`;
const ToolBarDiv = styled.div`
position: fixed;
top: 0;
left: 0;
right: 0;
background-color: ${props => props.theme.toolbar_background};
height: ${dimensions.toolBar.height}px;
`;
const IconBar = styled.div`
position: fixed;
top: ${dimensions.toolBar.height}px;
left: 0;
bottom: ${dimensions.statusBar.height}px;
width: ${dimensions.widgetMenu.iconSize}px;
background-color: ${props => props.theme.widget_background};
`;
const LeftPanel = styled.div`
position: fixed;
top: ${dimensions.toolBar.height}px;
left: ${dimensions.widgetMenu.iconSize}px;
bottom: ${dimensions.statusBar.height}px;
background-color: ${props => props.theme.left_background};
display: flex;
`;
const TabsPanelContainer = styled.div`
display: flex;
position: fixed;
top: ${dimensions.toolBar.height}px;
left: ${props => props.contentLeft}px;
height: ${dimensions.tabsPanel.height}px;
right: 0;
background-color: ${props => props.theme.tabs_background2};
border-top: 1px solid ${props => props.theme.border};
overflow-x: auto;
::-webkit-scrollbar {
height: 7px;
}
}
`;
const StausBarContainer = styled.div`
position: fixed;
height: ${dimensions.statusBar.height}px;
left: 0;
right: 0;
bottom: 0;
background-color: ${props => props.theme.statusbar_background};
`;
const ScreenHorizontalSplitHandle = styled(HorizontalSplitHandle)`
position: absolute;
top: ${dimensions.toolBar.height}px;
bottom: ${dimensions.statusBar.height}px;
`;
// const StyledRoot = styled.div`
// // color: ${(props) => props.theme.fontColor};
// `;
export default function Screen() {
const theme = useTheme();
const currentWidget = useCurrentWidget();
const leftPanelWidth = useLeftPanelWidth();
const setLeftPanelWidth = useSetLeftPanelWidth();
const contentLeft = currentWidget
? dimensions.widgetMenu.iconSize + leftPanelWidth + dimensions.splitter.thickness
: dimensions.widgetMenu.iconSize;
const toolbarPortalRef = React.useRef();
const statusbarPortalRef = React.useRef();
const onSplitDown = useSplitterDrag('clientX', diff => setLeftPanelWidth(v => v + diff));
const { getRootProps, getInputProps, isDragActive } = useUploadsZone();
return (
<div {...getRootProps()}>
<ErrorBoundary>
<ToolBarDiv theme={theme}>
<ToolBar toolbarPortalRef={toolbarPortalRef} />
</ToolBarDiv>
<IconBar theme={theme}>
<WidgetIconPanel />
</IconBar>
{!!currentWidget && (
<LeftPanel theme={theme}>
<ErrorBoundary>
<WidgetContainer />
</ErrorBoundary>
</LeftPanel>
)}
{!!currentWidget && (
<ScreenHorizontalSplitHandle
onMouseDown={onSplitDown}
theme={theme}
style={{ left: leftPanelWidth + dimensions.widgetMenu.iconSize }}
/>
)}
<TabsPanelContainer contentLeft={contentLeft} theme={theme}>
<TabsPanel></TabsPanel>
</TabsPanelContainer>
<BodyDiv contentLeft={contentLeft} theme={theme}>
<TabContent toolbarPortalRef={toolbarPortalRef} statusbarPortalRef={statusbarPortalRef} />
</BodyDiv>
<StausBarContainer theme={theme}>
<StatusBar statusbarPortalRef={statusbarPortalRef} />
</StausBarContainer>
<ModalLayer />
<MenuLayer />
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
</ErrorBoundary>
</div>
);
}

View File

@@ -0,0 +1,137 @@
<script>
import WidgetContainer from './widgets/WidgetContainer.svelte';
import WidgetIconPanel from './widgets/WidgetIconPanel.svelte';
import {
currentTheme,
isFileDragActive,
leftPanelWidth,
selectedWidget,
visibleCommandPalette,
visibleToolbar,
} from './stores';
import TabsPanel from './widgets/TabsPanel.svelte';
import TabRegister from './TabRegister.svelte';
import CommandPalette from './commands/CommandPalette.svelte';
import Toolbar from './widgets/Toolbar.svelte';
import splitterDrag from './utility/splitterDrag';
import CurrentDropDownMenu from './modals/CurrentDropDownMenu.svelte';
import StatusBar from './widgets/StatusBar.svelte';
import ModalLayer from './modals/ModalLayer.svelte';
import DragAndDropFileTarget from './DragAndDropFileTarget.svelte';
import dragDropFileTarget from './utility/dragDropFileTarget';
</script>
<div class={`${$currentTheme} root`} use:dragDropFileTarget>
<div class="iconbar">
<WidgetIconPanel />
</div>
<div class="statusbar">
<StatusBar />
</div>
{#if $selectedWidget}
<div class="leftpanel">
<WidgetContainer />
</div>
{/if}
<div class="tabs">
<TabsPanel />
</div>
<div class="content">
<TabRegister />
</div>
{#if $selectedWidget}
<div
class="horizontal-split-handle splitter"
use:splitterDrag={'clientX'}
on:resizeSplitter={e => leftPanelWidth.update(x => x + e.detail)}
/>
{/if}
{#if $visibleCommandPalette}
<div class="commads">
<CommandPalette />
</div>
{/if}
{#if $visibleToolbar}
<div class="toolbar">
<Toolbar />
</div>
{/if}
<CurrentDropDownMenu />
<ModalLayer />
{#if $isFileDragActive}
<DragAndDropFileTarget />
{/if}
</div>
<style>
.root {
color: var(--theme-font-1);
}
.iconbar {
position: fixed;
left: 0;
top: var(--dim-header-top);
bottom: var(--dim-statusbar-height);
width: var(--dim-widget-icon-size);
background: var(--theme-bg-inv-1);
}
.statusbar {
position: fixed;
background: var(--theme-bg-statusbar-inv);
height: var(--dim-statusbar-height);
left: 0;
right: 0;
bottom: 0;
}
.leftpanel {
position: fixed;
top: var(--dim-header-top);
left: var(--dim-widget-icon-size);
bottom: var(--dim-statusbar-height);
width: var(--dim-left-panel-width);
background-color: var(--theme-bg-1);
display: flex;
}
.tabs {
display: flex;
position: fixed;
top: var(--dim-header-top);
left: var(--dim-content-left);
height: var(--dim-tabs-panel-height);
right: 0;
background-color: var(--theme-bg-2);
border-top: 1px solid var(--theme-border);
overflow-x: auto;
}
.tabs::-webkit-scrollbar {
height: 7px;
}
.content {
position: fixed;
top: var(--dim-content-top);
left: var(--dim-content-left);
bottom: var(--dim-statusbar-height);
right: 0;
background-color: var(--theme-bg-1);
}
.commads {
position: fixed;
top: var(--dim-header-top);
left: var(--dim-widget-icon-size);
}
.toolbar {
position: fixed;
top: 0;
height: var(--dim-toolbar-height);
left: 0;
right: 0;
}
.splitter {
position: absolute;
top: var(--dim-header-top);
bottom: var(--dim-statusbar-height);
left: calc(var(--dim-widget-icon-size) + var(--dim-left-panel-width));
}
</style>

View File

@@ -1,98 +0,0 @@
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
import tabs from './tabs';
import { useOpenedTabs } from './utility/globalState';
import ErrorBoundary from './utility/ErrorBoundary';
const TabContainerStyled = styled.div`
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
visibility: ${props =>
// @ts-ignore
props.tabVisible ? 'visible' : 'hidden'};
`;
function TabContainer({ TabComponent, ...props }) {
const { tabVisible, tabid, toolbarPortalRef, statusbarPortalRef } = props;
return (
// @ts-ignore
<TabContainerStyled tabVisible={tabVisible}>
<ErrorBoundary>
<TabComponent
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
/>
</ErrorBoundary>
</TabContainerStyled>
);
}
const TabContainerMemo = React.memo(TabContainer);
function createTabComponent(selectedTab) {
const TabComponent = tabs[selectedTab.tabComponent];
if (TabComponent) {
return {
TabComponent,
props: selectedTab.props,
};
}
return null;
}
export default function TabContent({ toolbarPortalRef, statusbarPortalRef }) {
const files = useOpenedTabs();
const [mountedTabs, setMountedTabs] = React.useState({});
const selectedTab = files.find(x => x.selected && x.closedTime == null);
React.useEffect(() => {
// cleanup closed tabs
if (
_.difference(
_.keys(mountedTabs),
_.map(
files.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k && x.closedTime == null)));
}
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid])
setMountedTabs({
...mountedTabs,
[tabid]: createTabComponent(selectedTab),
});
}
}, [mountedTabs, files]);
return _.keys(mountedTabs).map(tabid => {
const { TabComponent, props } = mountedTabs[tabid];
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
return (
<TabContainerMemo
key={tabid}
{...props}
tabid={tabid}
tabVisible={tabVisible}
toolbarPortalRef={toolbarPortalRef}
statusbarPortalRef={statusbarPortalRef}
TabComponent={TabComponent}
/>
);
});
}

View File

@@ -0,0 +1,35 @@
<script lang="ts">
import { setContext } from 'svelte';
import { writable } from 'svelte/store';
export let tabid;
export let tabVisible;
export let tabComponent;
const tabVisibleStore = writable(tabVisible);
setContext('tabid', tabid);
setContext('tabVisible', tabVisibleStore);
$: tabVisibleStore.set(tabVisible);
</script>
<div class:tabVisible>
<svelte:component this={tabComponent} {...$$restProps} {tabid} {tabVisible} />
</div>
<style>
div {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
display: flex;
}
.tabVisible {
visibility: visible;
}
:not(.tabVisible) {
visibility: hidden;
}
</style>

View File

@@ -0,0 +1,62 @@
<script context="module" lang="ts">
function createTabComponent(selectedTab) {
const tabComponent = tabs[selectedTab.tabComponent]?.default;
if (tabComponent) {
return {
tabComponent,
props: selectedTab && selectedTab.props,
};
}
return null;
}
</script>
<script lang="ts">
import _ from 'lodash';
import { openedTabs } from './stores';
import TabContent from './TabContent.svelte';
import tabs from './tabs';
let mountedTabs = {};
$: selectedTab = $openedTabs.find(x => x.selected && x.closedTime == null);
// cleanup closed tabs
$: {
if (
_.difference(
_.keys(mountedTabs),
_.map(
$openedTabs.filter(x => x.closedTime == null),
'tabid'
)
).length > 0
) {
mountedTabs = _.pickBy(mountedTabs, (v, k) => $openedTabs.find(x => x.tabid == k && x.closedTime == null));
}
}
// open missing tabs
$: {
if (selectedTab) {
const { tabid } = selectedTab;
if (tabid && !mountedTabs[tabid]) {
const newTab = createTabComponent(selectedTab);
if (newTab) {
mountedTabs = {
...mountedTabs,
[tabid]: newTab,
};
}
}
}
}
</script>
{#each _.keys(mountedTabs) as tabid (tabid)}
<TabContent
tabComponent={mountedTabs[tabid].tabComponent}
{...mountedTabs[tabid].props}
{tabid}
tabVisible={tabid == (selectedTab && selectedTab.tabid)}
/>
{/each}

View File

@@ -1,300 +0,0 @@
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
import { getConnectionInfo } from './utility/metadataLoaders';
import { FontIcon } from './icons';
import useTheme from './theme/useTheme';
import usePropsCompare from './utility/usePropsCompare';
import { useShowMenu } from './modals/showMenu';
import { setSelectedTabFunc } from './utility/common';
import getElectron from './utility/getElectron';
// const files = [
// { name: 'app.js' },
// { name: 'BranchCategory', type: 'table', selected: true },
// { name: 'ApplicationList' },
// ];
const DbGroupHandler = styled.div`
display: flex;
flex: 1;
align-content: stretch;
`;
const DbWrapperHandler = styled.div`
display: flex;
flex-direction: column;
align-items: stretch;
`;
const DbNameWrapper = styled.div`
text-align: center;
font-size: 8pt;
border-bottom: 1px solid ${props => props.theme.border};
border-right: 1px solid ${props => props.theme.border};
cursor: pointer;
user-select: none;
padding: 1px;
position: relative;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
// height: 15px;
&:hover {
background-color: ${props => props.theme.tabs_background3};
}
background-color: ${props =>
// @ts-ignore
props.selected ? props.theme.tabs_background1 : 'inherit'};
`;
// const DbNameWrapperInner = styled.div`
// position: absolute;
// white-space: nowrap;
// `;
const FileTabItem = styled.div`
border-right: 1px solid ${props => props.theme.border};
padding-left: 15px;
padding-right: 15px;
flex-shrink: 1;
flex-grow: 1;
min-width: 10px;
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
&:hover {
color: ${props => props.theme.tabs_font_hover};
}
background-color: ${props =>
// @ts-ignore
props.selected ? props.theme.tabs_background1 : 'inherit'};
`;
const FileNameWrapper = styled.span`
margin-left: 5px;
`;
const CloseButton = styled(FontIcon)`
margin-left: 5px;
color: gray;
&:hover {
color: ${props => props.theme.tabs_font2};
}
`;
function TabContextMenu({ close, closeAll, closeOthers, closeWithSameDb, closeWithOtherDb, props }) {
const { database } = props || {};
const { conid } = props || {};
return (
<>
<DropDownMenuItem onClick={close}>Close</DropDownMenuItem>
<DropDownMenuItem onClick={closeAll}>Close all</DropDownMenuItem>
<DropDownMenuItem onClick={closeOthers}>Close others</DropDownMenuItem>
{conid && database && (
<DropDownMenuItem onClick={closeWithSameDb}>Close with same DB - {database}</DropDownMenuItem>
)}
{conid && database && (
<DropDownMenuItem onClick={closeWithOtherDb}>Close with other DB than {database}</DropDownMenuItem>
)}
</>
);
}
function getTabDbName(tab) {
if (tab.props && tab.props.conid && tab.props.database) return tab.props.database;
if (tab.props && tab.props.archiveFolder) return tab.props.archiveFolder;
return '(no DB)';
}
function getTabDbKey(tab) {
if (tab.props && tab.props.conid && tab.props.database) return `database://${tab.props.database}-${tab.props.conid}`;
if (tab.props && tab.props.archiveFolder) return `archive://${tab.props.archiveFolder}`;
return '_no';
}
function getDbIcon(key) {
if (key.startsWith('database://')) return 'icon database';
if (key.startsWith('archive://')) return 'icon archive';
return 'icon file';
}
function buildTooltip(tab) {
let res = tab.tooltip;
if (tab.props && tab.props.savedFilePath) {
if (res) res += '\n';
res += tab.props.savedFilePath;
}
return res;
}
export default function TabsPanel() {
// const formatDbKey = (conid, database) => `${database}-${conid}`;
const theme = useTheme();
const showMenu = useShowMenu();
const tabs = useOpenedTabs();
const setOpenedTabs = useSetOpenedTabs();
const currentDb = useCurrentDatabase();
const setCurrentDb = useSetCurrentDatabase();
const { name, connection } = currentDb || {};
const currentDbKey = name && connection ? `database://${name}-${connection._id}` : '_no';
const handleTabClick = (e, tabid) => {
if (e.target.closest('.tabCloseButton')) {
return;
}
setOpenedTabs(files => setSelectedTabFunc(files, tabid));
};
const closeTabFunc = closeCondition => tabid => {
setOpenedTabs(files => {
const active = files.find(x => x.tabid == tabid);
if (!active) return files;
const newFiles = files.map(x => ({
...x,
closedTime: x.closedTime || (closeCondition(x, active) ? new Date().getTime() : undefined),
}));
if (newFiles.find(x => x.selected && x.closedTime == null)) {
return newFiles;
}
const selectedIndex = _.findLastIndex(newFiles, x => x.closedTime == null);
return newFiles.map((x, index) => ({
...x,
selected: index == selectedIndex,
}));
});
};
const closeTab = closeTabFunc((x, active) => x.tabid == active.tabid);
const closeAll = () => {
const closedTime = new Date().getTime();
setOpenedTabs(tabs =>
tabs.map(tab => ({
...tab,
closedTime: tab.closedTime || closedTime,
selected: false,
}))
);
};
const closeWithSameDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') == _.get(active, 'props.conid') &&
_.get(x, 'props.database') == _.get(active, 'props.database')
);
const closeWithOtherDb = closeTabFunc(
(x, active) =>
_.get(x, 'props.conid') != _.get(active, 'props.conid') ||
_.get(x, 'props.database') != _.get(active, 'props.database')
);
const closeOthers = closeTabFunc((x, active) => x.tabid != active.tabid);
const handleMouseUp = (e, tabid) => {
if (e.button == 1) {
e.preventDefault();
closeTab(tabid);
}
};
const handleContextMenu = (event, tabid, props) => {
event.preventDefault();
showMenu(
event.pageX,
event.pageY,
<TabContextMenu
close={() => closeTab(tabid)}
closeAll={closeAll}
closeOthers={() => closeOthers(tabid)}
closeWithSameDb={() => closeWithSameDb(tabid)}
closeWithOtherDb={() => closeWithOtherDb(tabid)}
props={props}
/>
);
};
React.useEffect(() => {
const electron = getElectron();
if (electron) {
const { ipcRenderer } = electron;
const activeTab = tabs.find(x => x.selected);
window['dbgate_activeTabId'] = activeTab ? activeTab.tabid : null;
ipcRenderer.send('update-menu');
}
}, [tabs]);
// console.log(
// 't',
// tabs.map(x => x.tooltip)
// );
const tabsWithDb = tabs
.filter(x => !x.closedTime)
.map(tab => ({
...tab,
tabDbName: getTabDbName(tab),
tabDbKey: getTabDbKey(tab),
}));
const tabsByDb = _.groupBy(tabsWithDb, 'tabDbKey');
const dbKeys = _.keys(tabsByDb).sort();
const handleSetDb = async props => {
const { conid, database } = props || {};
if (conid) {
const connection = await getConnectionInfo({ conid, database });
if (connection) {
setCurrentDb({ connection, name: database });
return;
}
}
setCurrentDb(null);
};
return (
<>
{dbKeys.map(dbKey => (
<DbWrapperHandler key={dbKey}>
<DbNameWrapper
// @ts-ignore
selected={tabsByDb[dbKey][0].tabDbKey == currentDbKey}
onClick={() => handleSetDb(tabsByDb[dbKey][0].props)}
theme={theme}
>
<FontIcon icon={getDbIcon(dbKey)} /> {tabsByDb[dbKey][0].tabDbName}
</DbNameWrapper>
<DbGroupHandler>
{_.sortBy(tabsByDb[dbKey], ['title', 'tabid']).map(tab => (
<FileTabItem
{...tab}
title={buildTooltip(tab)}
key={tab.tabid}
theme={theme}
onClick={e => handleTabClick(e, tab.tabid)}
onMouseUp={e => handleMouseUp(e, tab.tabid)}
onContextMenu={e => handleContextMenu(e, tab.tabid, tab.props)}
>
{<FontIcon icon={tab.busy ? 'icon loading' : tab.icon} />}
<FileNameWrapper>{tab.title}</FileNameWrapper>
<CloseButton
icon="icon close"
className="tabCloseButton"
theme={theme}
onClick={e => {
e.preventDefault();
closeTab(tab.tabid);
}}
/>
</FileTabItem>
))}
</DbGroupHandler>
</DbWrapperHandler>
))}
</>
);
}

View File

@@ -1,97 +0,0 @@
// @ts-nocheck
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import { useShowMenu } from '../modals/showMenu';
import useTheme from '../theme/useTheme';
const AppObjectDiv = styled.div`
padding: 5px;
${props =>
!props.disableHover &&
`
&:hover {
background-color: ${props.theme.left_background_blue[1]};
}
`}
cursor: pointer;
white-space: nowrap;
font-weight: ${props => (props.isBold ? 'bold' : 'normal')};
`;
const IconWrap = styled.span`
margin-right: 5px;
`;
const StatusIconWrap = styled.span`
margin-left: 5px;
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
color: ${props => props.theme.left_font3};
`;
export function AppObjectCore({
title,
icon,
data,
onClick = undefined,
onClick2 = undefined,
onClick3 = undefined,
isBold = undefined,
isBusy = undefined,
prefix = undefined,
statusIcon = undefined,
extInfo = undefined,
statusTitle = undefined,
disableHover = false,
children = null,
Menu = undefined,
...other
}) {
const theme = useTheme();
const showMenu = useShowMenu();
const handleContextMenu = event => {
if (!Menu) return;
event.preventDefault();
showMenu(event.pageX, event.pageY, <Menu data={data} />);
};
return (
<>
<AppObjectDiv
onContextMenu={handleContextMenu}
onClick={() => {
if (onClick) onClick(data);
if (onClick2) onClick2(data);
if (onClick3) onClick3(data);
}}
theme={theme}
isBold={isBold}
draggable
onDragStart={e => {
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
}}
disableHover={disableHover}
{...other}
>
{prefix}
<IconWrap>{isBusy ? <FontIcon icon="icon loading" /> : <FontIcon icon={icon} />}</IconWrap>
{title}
{statusIcon && (
<StatusIconWrap>
<FontIcon icon={statusIcon} title={statusTitle} />
</StatusIconWrap>
)}
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
</AppObjectDiv>
{children}
</>
);
}

View File

@@ -0,0 +1,83 @@
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
import contextMenu from '../utility/contextMenu';
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let icon;
export let title;
export let data;
export let isBold = false;
export let isBusy = false;
export let statusIcon = undefined;
export let statusTitle = undefined;
export let extInfo = undefined;
export let menu = undefined;
export let expandIcon = undefined;
function handleExpand() {
dispatch('expand');
}
</script>
<div
class="main"
class:isBold
draggable={true}
on:click
use:contextMenu={menu}
on:dragstart={e => {
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
}}
>
{#if expandIcon}
<span class="expand-icon" on:click|stopPropagation={handleExpand}>
<FontIcon icon={expandIcon} />
</span>
{/if}
{#if isBusy}
<FontIcon icon="icon loading" />
{:else}
<FontIcon {icon} />
{/if}
{title}
{#if statusIcon}
<span class="status">
<FontIcon icon={statusIcon} title={statusTitle} />
</span>
{/if}
{#if extInfo}
<span class="ext-info">
{extInfo}
</span>
{/if}
</div>
<slot />
<style>
.main {
padding: 5px;
cursor: pointer;
white-space: nowrap;
font-weight: normal;
}
.main:hover {
background-color: var(--theme-bg-hover);
}
.isBold {
font-weight: bold;
}
.status {
margin-left: 5px;
}
.ext-info {
font-weight: normal;
margin-left: 5px;
color: var(--theme-font-3);
}
.expand-icon {
margin-right: 3px;
}
</style>

View File

@@ -0,0 +1,48 @@
<script>
import { plusExpandIcon } from '../icons/expandIcons';
import FontIcon from '../icons/FontIcon.svelte';
import AppObjectListItem from './AppObjectListItem.svelte';
export let group;
export let items;
export let module;
let isExpanded = true;
$: filtered = items.filter(x => x.isMatched);
$: countText = filtered.length < items.length ? `${filtered.length}/${items.length}` : `${items.length}`;
</script>
<div class="group" on:click={() => (isExpanded = !isExpanded)}>
<span class="expand-icon">
<FontIcon icon={plusExpandIcon(isExpanded)} />
</span>
{group}
{items && `(${countText})`}
</div>
{#if isExpanded}
{#each filtered as item (module.extractKey(item.data))}
<AppObjectListItem {...$$restProps} {module} data={item.data} on:objectClick />
{/each}
{/if}
<style>
.group {
user-select: none;
padding: 5px;
cursor: pointer;
white-space: nowrap;
font-weight: bold;
}
.group:hover {
background-color: var(--theme-bg-hover);
}
.expand-icon {
margin-right: 3px;
}
</style>

View File

@@ -1,164 +0,0 @@
import React from 'react';
import _ from 'lodash';
import styled from 'styled-components';
import { ExpandIcon } from '../icons';
import useTheme from '../theme/useTheme';
const SubItemsDiv = styled.div`
margin-left: 28px;
`;
const ExpandIconHolder2 = styled.span`
margin-right: 3px;
// position: relative;
// top: -3px;
`;
const ExpandIconHolder = styled.span`
margin-right: 5px;
`;
const GroupDiv = styled.div`
user-select: none;
padding: 5px;
&:hover {
background-color: ${props => props.theme.left_background_blue[1]};
}
cursor: pointer;
white-space: nowrap;
font-weight: bold;
`;
function AppObjectListItem({
AppObjectComponent,
data,
filter,
onObjectClick,
isExpandable,
SubItems,
getCommonProps,
expandOnClick,
ExpandIconComponent,
}) {
const [isExpanded, setIsExpanded] = React.useState(false);
const expandable = data && isExpandable && isExpandable(data);
React.useEffect(() => {
if (!expandable) {
setIsExpanded(false);
}
}, [expandable]);
let commonProps = {
prefix: SubItems ? (
<ExpandIconHolder2>
{expandable ? (
<ExpandIconComponent
isExpanded={isExpanded}
onClick={e => {
setIsExpanded(v => !v);
e.stopPropagation();
}}
/>
) : (
<ExpandIconComponent isBlank />
)}
</ExpandIconHolder2>
) : null,
};
if (SubItems && expandOnClick) {
commonProps.onClick2 = () => setIsExpanded(v => !v);
}
if (onObjectClick) {
commonProps.onClick3 = onObjectClick;
}
if (getCommonProps) {
commonProps = { ...commonProps, ...getCommonProps(data) };
}
let res = <AppObjectComponent data={data} commonProps={commonProps} />;
if (SubItems && isExpanded) {
res = (
<>
{res}
<SubItemsDiv>
<SubItems data={data} filter={filter} />
</SubItemsDiv>
</>
);
}
return res;
}
function AppObjectGroup({ group, items }) {
const [isExpanded, setIsExpanded] = React.useState(true);
const theme = useTheme();
const filtered = items.filter(x => x.component);
let countText = filtered.length.toString();
if (filtered.length < items.length) countText += `/${items.length}`;
return (
<>
<GroupDiv onClick={() => setIsExpanded(!isExpanded)} theme={theme}>
<ExpandIconHolder>
<ExpandIcon isExpanded={isExpanded} />
</ExpandIconHolder>
{group} {items && `(${countText})`}
</GroupDiv>
{isExpanded && filtered.map(x => x.component)}
</>
);
}
export function AppObjectList({
list,
AppObjectComponent,
SubItems = undefined,
onObjectClick = undefined,
filter = undefined,
groupFunc = undefined,
groupOrdered = undefined,
isExpandable = undefined,
getCommonProps = undefined,
expandOnClick = false,
ExpandIconComponent = ExpandIcon,
}) {
const createComponent = data => (
<AppObjectListItem
key={AppObjectComponent.extractKey(data)}
AppObjectComponent={AppObjectComponent}
data={data}
filter={filter}
onObjectClick={onObjectClick}
SubItems={SubItems}
isExpandable={isExpandable}
getCommonProps={getCommonProps}
expandOnClick={expandOnClick}
ExpandIconComponent={ExpandIconComponent}
/>
);
if (groupFunc) {
const listGrouped = _.compact(
(list || []).map(data => {
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
const component = matcher && !matcher(filter) ? null : createComponent(data);
const group = groupFunc(data);
return { group, data, component };
})
);
const groups = _.groupBy(listGrouped, 'group');
return (groupOrdered || _.keys(groups)).map(group => (
<AppObjectGroup key={group} group={group} items={groups[group]} />
));
}
return (list || []).map(data => {
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
if (matcher && !matcher(filter)) return null;
return createComponent(data);
});
}

View File

@@ -0,0 +1,53 @@
<script>
import _ from 'lodash';
import AppObjectGroup from './AppObjectGroup.svelte';
import AppObjectListItem from './AppObjectListItem.svelte';
export let list;
export let module;
export let subItemsComponent = undefined;
export let expandOnClick = false;
export let isExpandable = undefined;
export let filter;
export let expandIconFunc = undefined;
export let groupFunc = undefined;
$: filtered = list.filter(data => {
const matcher = module.createMatcher && module.createMatcher(data);
if (matcher && !matcher(filter)) return false;
return true;
});
$: listGrouped = groupFunc
? _.compact(
(list || []).map(data => {
const matcher = module.createMatcher && module.createMatcher(data);
const isMatched = matcher && !matcher(filter) ? false : true;
const group = groupFunc(data);
return { group, data, isMatched };
})
)
: null;
$: groups = groupFunc ? _.groupBy(listGrouped, 'group') : null;
</script>
{#if groupFunc}
{#each _.keys(groups) as group (group)}
<AppObjectGroup {group} {module} items={groups[group]} {expandIconFunc} {isExpandable} {subItemsComponent} />
{/each}
{:else}
{#each filtered as data (module.extractKey(data))}
<AppObjectListItem
{module}
{subItemsComponent}
{expandOnClick}
{data}
{isExpandable}
on:objectClick
{expandIconFunc}
/>
{/each}
{/if}

View File

@@ -0,0 +1,56 @@
<script lang="ts" context="module">
function getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc) {
if (!subItemsComponent) return null;
if (!expandable) return 'icon invisible-box';
return expandIconFunc(isExpanded);
}
</script>
<script lang="ts">
import { tick } from 'svelte';
import { plusExpandIcon } from '../icons/expandIcons';
export let module;
export let data;
export let subItemsComponent;
export let expandOnClick;
export let isExpandable = undefined;
export let expandIconFunc = plusExpandIcon;
let isExpanded = false;
async function handleExpand() {
if (subItemsComponent && expandOnClick) {
await tick();
isExpanded = !isExpanded;
}
}
function handleExpandButton() {
isExpanded = !isExpanded;
}
$: expandable = data && isExpandable && isExpandable(data);
$: if (!expandable && isExpanded) isExpanded = false;
</script>
<svelte:component
this={module.default}
{data}
on:click={handleExpand}
on:expand={handleExpandButton}
expandIcon={getExpandIcon(expandable, subItemsComponent, isExpanded, expandIconFunc)}
/>
{#if isExpanded && subItemsComponent}
<div class="subitems">
<svelte:component this={subItemsComponent} {data} />
</div>
{/if}
<style>
.subitems {
margin-left: 28px;
}
</style>

View File

@@ -1,75 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { filterName } from 'dbgate-datalib';
import axios from '../utility/axios';
import { AppObjectCore } from './AppObjectCore';
import useOpenNewTab from '../utility/useOpenNewTab';
function openArchive(openNewTab, fileName, folderName) {
openNewTab({
title: fileName,
icon: 'img archive',
tooltip: `${folderName}\n${fileName}`,
tabComponent: 'ArchiveFileTab',
props: {
archiveFile: fileName,
archiveFolder: folderName,
},
});
}
function Menu({ data }) {
const openNewTab = useOpenNewTab();
const handleDelete = () => {
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
};
const handleOpenRead = () => {
openArchive(openNewTab, data.fileName, data.folderName);
};
const handleOpenWrite = async () => {
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
openNewTab({
title: data.fileName,
icon: 'img archive',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'archiveReader',
props: {
fileName: data.fileName,
folderName: data.folderName,
},
},
archiveFile: data.fileName,
archiveFolder: data.folderName,
},
});
};
return (
<>
<DropDownMenuItem onClick={handleOpenRead}>Open (readonly)</DropDownMenuItem>
<DropDownMenuItem onClick={handleOpenWrite}>Open in free table editor</DropDownMenuItem>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
);
}
function ArchiveFileAppObject({ data, commonProps }) {
const { fileName, folderName } = data;
const openNewTab = useOpenNewTab();
const onClick = () => {
openArchive(openNewTab, fileName, folderName);
};
return (
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
);
}
ArchiveFileAppObject.extractKey = data => data.fileName;
ArchiveFileAppObject.createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
export default ArchiveFileAppObject;

View File

@@ -0,0 +1,74 @@
<script lang="ts" context="module">
function openArchive(fileName, folderName) {
openNewTab({
title: fileName,
icon: 'img archive',
tooltip: `${folderName}\n${fileName}`,
tabComponent: 'ArchiveFileTab',
props: {
archiveFile: fileName,
archiveFolder: folderName,
},
});
}
export const extractKey = data => data.fileName;
export const createMatcher = ({ fileName }) => filter => filterName(filter, fileName);
</script>
<script lang="ts">
import { filterName } from 'dbgate-datalib';
import { currentArchive } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
const handleDelete = () => {
axiosInstance.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
};
const handleOpenRead = () => {
openArchive(data.fileName, data.folderName);
};
const handleOpenWrite = () => {
openNewTab({
title: data.fileName,
icon: 'img archive',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'archiveReader',
props: {
fileName: data.fileName,
folderName: data.folderName,
},
},
archiveFile: data.fileName,
archiveFolder: data.folderName,
},
});
};
const handleClick = () => {
openArchive(data.fileName, data.folderName);
};
function createMenu() {
return [
{ text: 'Open (readonly)', onClick: handleOpenRead },
{ text: 'Open in free table editor', onClick: handleOpenWrite },
{ text: 'Delete', onClick: handleDelete },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.fileName}
icon="img archive"
menu={createMenu}
on:click={handleClick}
/>

View File

@@ -1,34 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import axios from '../utility/axios';
import { filterName } from 'dbgate-datalib';
import { AppObjectCore } from './AppObjectCore';
import { useCurrentArchive } from '../utility/globalState';
function Menu({ data }) {
const handleDelete = () => {
axios.post('archive/delete-folder', { folder: data.name });
};
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
}
function ArchiveFolderAppObject({ data, commonProps }) {
const { name } = data;
const currentArchive = useCurrentArchive();
return (
<AppObjectCore
{...commonProps}
data={data}
title={name}
icon="img archive-folder"
isBold={name == currentArchive}
Menu={Menu}
/>
);
}
ArchiveFolderAppObject.extractKey = data => data.name;
ArchiveFolderAppObject.createMatcher = data => filter => filterName(filter, data.name);
export default ArchiveFolderAppObject;

View File

@@ -0,0 +1,33 @@
<script lang="ts" context="module">
export const extractKey = data => data.name;
export const createMatcher = data => filter => filterName(filter, data.name);
</script>
<script lang="ts">
import { filterName } from 'dbgate-datalib';
import { currentArchive } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
const handleDelete = () => {
axiosInstance.post('archive/delete-folder', { folder: data.name });
};
function createMenu() {
return [data.name != 'default' && { text: 'Delete', onClick: handleDelete }];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.name}
icon="img archive-folder"
isBold={data.name == $currentArchive}
on:click={() => ($currentArchive = data.name)}
menu={createMenu}
/>

View File

@@ -1,73 +0,0 @@
import React from 'react';
import _ from 'lodash';
import moment from 'moment';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import { setSelectedTabFunc } from '../utility/common';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const InfoDiv = styled.div`
margin-left: 30px;
color: ${props => props.theme.left_font3};
`;
function Menu({ data }) {
const setOpenedTabs = useSetOpenedTabs();
const handleDelete = () => {
setOpenedTabs(tabs => tabs.filter(x => x.tabid != data.tabid));
};
const handleDeleteOlder = () => {
setOpenedTabs(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
};
return (
<>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
<DropDownMenuItem onClick={handleDeleteOlder}>Delete older</DropDownMenuItem>
</>
);
}
function ClosedTabAppObject({ data, commonProps }) {
const { tabid, props, selected, icon, title, closedTime, busy } = data;
const setOpenedTabs = useSetOpenedTabs();
const theme = useTheme();
const onClick = () => {
setOpenedTabs(files =>
setSelectedTabFunc(
files.map(x => ({
...x,
closedTime: x.tabid == tabid ? undefined : x.closedTime,
})),
tabid
)
);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={`${title} ${moment(closedTime).fromNow()}`}
icon={icon}
isBold={!!selected}
onClick={onClick}
isBusy={busy}
Menu={Menu}
>
{data.props && data.props.database && (
<InfoDiv theme={theme}>
<FontIcon icon="icon database" /> {data.props.database}
</InfoDiv>
)}
{data.contentPreview && <InfoDiv theme={theme}>{data.contentPreview}</InfoDiv>}
</AppObjectCore>
);
}
ClosedTabAppObject.extractKey = data => data.tabid;
export default ClosedTabAppObject;

View File

@@ -0,0 +1,70 @@
<script lang="ts" context="module">
export const extractKey = data => data.tabid;
</script>
<script lang="ts">
import FontIcon from '../icons/FontIcon.svelte';
import { openedTabs } from '../stores';
import { setSelectedTabFunc } from '../utility/common';
import moment from 'moment';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
const handleDelete = () => {
openedTabs.update(tabs => tabs.filter(x => x.tabid != data.tabid));
};
const handleDeleteOlder = () => {
openedTabs.update(tabs => tabs.filter(x => !x.closedTime || x.closedTime >= data.closedTime));
};
const onClick = () => {
openedTabs.update(files =>
setSelectedTabFunc(
files.map(x => ({
...x,
closedTime: x.tabid == data.tabid ? undefined : x.closedTime,
})),
data.tabid
)
);
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Delete older', onClick: handleDeleteOlder },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={`${data.title} ${moment(data.closedTime).fromNow()}`}
icon={data.icon}
isBold={!!data.selected}
on:click={onClick}
isBusy={data.busy}
menu={createMenu}
>
{#if data.props && data.props.database}
<div class="info">
<FontIcon icon="icon database" />
{data.props.database}
</div>
{/if}
{#if data.contentPreview}
<div class="info">
{data.contentPreview}
</div>
{/if}
</AppObjectCore>
<style>
.info {
margin-left: 30px;
color: var(--theme-font-3);
}
</style>

View File

@@ -0,0 +1,22 @@
<script lang="ts" context="module">
export const extractKey = ({ columnName }) => columnName;
</script>
<script lang="ts">
import { getColumnIcon } from '../elements/ColumnLabel.svelte';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
$: extInfo = data.foreignKey ? `${data.dataType} -> ${data.foreignKey.refTableName}` : data.dataType;
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.columnName}
{extInfo}
icon={getColumnIcon(data, true)}
disableHover
/>

View File

@@ -1,119 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import ConnectionModal from '../modals/ConnectionModal';
import axios from '../utility/axios';
import { filterName } from 'dbgate-datalib';
import ConfirmModal from '../modals/ConfirmModal';
import CreateDatabaseModal from '../modals/CreateDatabaseModal';
import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import { useConfig } from '../utility/metadataLoaders';
import useExtensions from '../utility/useExtensions';
function Menu({ data }) {
const openedConnections = useOpenedConnections();
const setOpenedConnections = useSetOpenedConnections();
const showModal = useShowModal();
const config = useConfig();
const handleEdit = () => {
showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
};
const handleDelete = () => {
showModal(modalState => (
<ConfirmModal
modalState={modalState}
message={`Really delete connection ${data.displayName || data.server}?`}
onConfirm={() => axios.post('connections/delete', data)}
/>
));
};
const handleCreateDatabase = () => {
showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
};
const handleRefresh = () => {
axios.post('server-connections/refresh', { conid: data._id });
};
const handleDisconnect = () => {
setOpenedConnections(list => list.filter(x => x != data._id));
};
const handleConnect = () => {
setOpenedConnections(list => _.uniq([...list, data._id]));
};
return (
<>
{config.runAsPortal == false && (
<>
<DropDownMenuItem onClick={handleEdit}>Edit</DropDownMenuItem>
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
</>
)}
{!openedConnections.includes(data._id) && <DropDownMenuItem onClick={handleConnect}>Connect</DropDownMenuItem>}
{openedConnections.includes(data._id) && data.status && (
<DropDownMenuItem onClick={handleRefresh}>Refresh</DropDownMenuItem>
)}
{openedConnections.includes(data._id) && (
<DropDownMenuItem onClick={handleDisconnect}>Disconnect</DropDownMenuItem>
)}
{openedConnections.includes(data._id) && (
<DropDownMenuItem onClick={handleCreateDatabase}>Create database</DropDownMenuItem>
)}
</>
);
}
function ConnectionAppObject({ data, commonProps }) {
const { _id, server, displayName, engine, status } = data;
const openedConnections = useOpenedConnections();
const setOpenedConnections = useSetOpenedConnections();
const currentDatabase = useCurrentDatabase();
const extensions = useExtensions();
const isBold = _.get(currentDatabase, 'connection._id') == _id;
const onClick = () => setOpenedConnections(c => _.uniq([...c, _id]));
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
if (extensions.drivers.find(x => x.engine == engine)) {
const match = (engine || '').match(/^([^@]*)@/);
extInfo = match ? match[1] : engine;
} else {
extInfo = engine;
statusIcon = 'img warn';
statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`;
}
if (openedConnections.includes(_id)) {
if (!status) statusIcon = 'icon loading';
else if (status.name == 'pending') statusIcon = 'icon loading';
else if (status.name == 'ok') statusIcon = 'img ok';
else statusIcon = 'img error';
if (status && status.name == 'error') {
statusTitle = status.message;
}
}
return (
<AppObjectCore
{...commonProps}
title={displayName || server}
icon="img server"
data={data}
statusIcon={statusIcon}
statusTitle={statusTitle}
extInfo={extInfo}
isBold={isBold}
onClick={onClick}
Menu={Menu}
/>
);
}
ConnectionAppObject.extractKey = data => data._id;
ConnectionAppObject.createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
export default ConnectionAppObject;

View File

@@ -0,0 +1,116 @@
<script context="module">
const getContextMenu = (data, $openedConnections) => () => {
const handleRefresh = () => {
axiosInstance.post('server-connections/refresh', { conid: data._id });
};
const handleDisconnect = () => {
openedConnections.update(list => list.filter(x => x != data._id));
};
const handleConnect = () => {
openedConnections.update(list => _.uniq([...list, data._id]));
};
const handleEdit = () => {
showModal(ConnectionModal, { connection: data });
};
return [
{
text: 'Edit',
onClick: handleEdit,
},
!$openedConnections.includes(data._id) && {
text: 'Connect',
onClick: handleConnect,
},
$openedConnections.includes(data._id) &&
data.status && {
text: 'Refresh',
onClick: handleRefresh,
},
$openedConnections.includes(data._id) && {
text: 'Disconnect',
onClick: handleDisconnect,
},
];
};
export const extractKey = data => data._id;
export const createMatcher = ({ displayName, server }) => filter => filterName(filter, displayName, server);
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, openedConnections } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import { filterName } from 'dbgate-datalib';
import { showModal } from '../modals/modalTools';
import ConnectionModal from '../modals/ConnectionModal.svelte';
export let data;
let statusIcon = null;
let statusTitle = null;
let extInfo = null;
let engineStatusIcon = null;
let engineStatusTitle = null;
$: {
if ($extensions.drivers.find(x => x.engine == data.engine)) {
const match = (data.engine || '').match(/^([^@]*)@/);
extInfo = match ? match[1] : data.engine;
engineStatusIcon = null;
engineStatusTitle = null;
} else {
extInfo = data.engine;
engineStatusIcon = 'img warn';
engineStatusTitle = `Engine driver ${data.engine} not found, review installed plugins and change engine in edit connection dialog`;
}
}
$: {
const { _id, status } = data;
if ($openedConnections.includes(_id)) {
if (!status) statusIcon = 'icon loading';
else if (status.name == 'pending') statusIcon = 'icon loading';
else if (status.name == 'ok') statusIcon = 'img ok';
else statusIcon = 'img error';
if (status && status.name == 'error') {
statusTitle = status.message;
}
} else {
statusIcon = null;
statusTitle = null;
}
}
// const handleEdit = () => {
// showModal(modalState => <ConnectionModal modalState={modalState} connection={data} />);
// };
// const handleDelete = () => {
// showModal(modalState => (
// <ConfirmModal
// modalState={modalState}
// message={`Really delete connection ${data.displayName || data.server}?`}
// onConfirm={() => axios.post('connections/delete', data)}
// />
// ));
// };
// const handleCreateDatabase = () => {
// showModal(modalState => <CreateDatabaseModal modalState={modalState} conid={data._id} />);
// };
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.displayName || data.server}
icon="img server"
isBold={_.get($currentDatabase, 'connection._id') == data._id}
statusIcon={statusIcon || engineStatusIcon}
statusTitle={statusTitle || engineStatusTitle}
{extInfo}
menu={getContextMenu(data, $openedConnections)}
on:click={() => ($openedConnections = _.uniq([...$openedConnections, data._id]))}
on:click
/>

View File

@@ -1,90 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import ImportExportModal from '../modals/ImportExportModal';
import { getDefaultFileFormat } from '../utility/fileformats';
import { useCurrentDatabase } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import useExtensions from '../utility/useExtensions';
import useOpenNewTab from '../utility/useOpenNewTab';
function Menu({ data }) {
const { connection, name } = data;
const openNewTab = useOpenNewTab();
const extensions = useExtensions();
const showModal = useShowModal();
const tooltip = `${connection.displayName || connection.server}\n${name}`;
const handleNewQuery = () => {
openNewTab({
title: 'Query #',
icon: 'img sql-file',
tooltip,
tabComponent: 'QueryTab',
props: {
conid: connection._id,
database: name,
},
});
};
const handleImport = () => {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
sourceStorageType: getDefaultFileFormat(extensions).storageType,
targetStorageType: 'database',
targetConnectionId: connection._id,
targetDatabaseName: name,
}}
/>
));
};
const handleExport = () => {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
targetStorageType: getDefaultFileFormat(extensions).storageType,
sourceStorageType: 'database',
sourceConnectionId: connection._id,
sourceDatabaseName: name,
}}
/>
));
};
return (
<>
<DropDownMenuItem onClick={handleNewQuery}>New query</DropDownMenuItem>
<DropDownMenuItem onClick={handleImport}>Import</DropDownMenuItem>
<DropDownMenuItem onClick={handleExport}>Export</DropDownMenuItem>
</>
);
}
function DatabaseAppObject({ data, commonProps }) {
const { name, connection } = data;
const currentDatabase = useCurrentDatabase();
return (
<AppObjectCore
{...commonProps}
data={data}
title={name}
icon="img database"
isBold={
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
}
Menu={Menu}
/>
);
}
DatabaseAppObject.extractKey = props => props.name;
export default DatabaseAppObject;

View File

@@ -0,0 +1,23 @@
<script lang="ts" context="module">
export const extractKey = props => props.name;
</script>
<script lang="ts">
import _ from 'lodash';
import { currentDatabase } from '../stores';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.name}
icon="img database"
isBold={_.get($currentDatabase, 'connection._id') == _.get(data.connection, '_id') &&
_.get($currentDatabase, 'name') == data.name}
on:click={() => ($currentDatabase = data)}
/>

View File

@@ -1,325 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { DropDownMenuDivider, DropDownMenuItem } from '../modals/DropDownMenu';
import { getConnectionInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import { filterName } from 'dbgate-datalib';
import ImportExportModal from '../modals/ImportExportModal';
import { useSetOpenedTabs } from '../utility/globalState';
import { AppObjectCore } from './AppObjectCore';
import useShowModal from '../modals/showModal';
import { findEngineDriver } from 'dbgate-tools';
import useExtensions from '../utility/useExtensions';
import useOpenNewTab from '../utility/useOpenNewTab';
import uuidv1 from 'uuid/v1';
import { AppObjectList } from './AppObjectList';
const icons = {
tables: 'img table',
views: 'img view',
procedures: 'img procedure',
functions: 'img function',
};
const menus = {
tables: [
{
label: 'Open data',
tab: 'TableDataTab',
forceNewTab: true,
},
{
label: 'Open form',
tab: 'TableDataTab',
forceNewTab: true,
initialData: {
grid: {
isFormView: true,
},
},
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
isDivider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
isDivider: true,
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
views: [
{
label: 'Open data',
tab: 'ViewDataTab',
forceNewTab: true,
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
isDivider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
isDivider: true,
},
{
label: 'SQL: CREATE VIEW',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
procedures: [
{
label: 'SQL: CREATE PROCEDURE',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: EXECUTE',
sqlTemplate: 'EXECUTE PROCEDURE',
},
],
functions: [
{
label: 'SQL: CREATE FUNCTION',
sqlTemplate: 'CREATE OBJECT',
},
],
};
const defaultTabs = {
tables: 'TableDataTab',
views: 'ViewDataTab',
};
export async function openDatabaseObjectDetail(
openNewTab,
tabComponent,
sqlTemplate,
{ schemaName, pureName, conid, database, objectTypeField },
forceNewTab,
initialData
) {
const connection = await getConnectionInfo({ conid });
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
schemaName,
pureName,
})}`;
openNewTab(
{
title: sqlTemplate ? 'Query #' : pureName,
tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
props: {
schemaName,
pureName,
conid,
database,
objectTypeField,
initialArgs: sqlTemplate ? { sqlTemplate } : null,
},
},
initialData,
{ forceNewTab }
);
}
function Menu({ data }) {
const showModal = useShowModal();
const openNewTab = useOpenNewTab();
const extensions = useExtensions();
const getDriver = async () => {
const conn = await getConnectionInfo(data);
if (!conn) return;
const driver = findEngineDriver(conn, extensions);
return driver;
};
return (
<>
{menus[data.objectTypeField].map(menu =>
menu.isDivider ? (
<DropDownMenuDivider />
) : (
<DropDownMenuItem
key={menu.label}
onClick={async () => {
if (menu.isExport) {
showModal(modalState => (
<ImportExportModal
modalState={modalState}
initialValues={{
sourceStorageType: 'database',
sourceConnectionId: data.conid,
sourceDatabaseName: data.database,
sourceSchemaName: data.schemaName,
sourceList: [data.pureName],
}}
/>
));
} else if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
...coninfo,
database: data.database,
},
schemaName: data.schemaName,
pureName: data.pureName,
},
},
},
});
} else if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: uuidv1(),
left: 50,
top: 50,
},
],
},
}
);
} else {
openDatabaseObjectDetail(
openNewTab,
menu.tab,
menu.sqlTemplate,
data,
menu.forceNewTab,
menu.initialData
);
}
}}
>
{menu.label}
</DropDownMenuItem>
)
)}
</>
);
}
function DatabaseObjectAppObject({ data, commonProps }) {
const { conid, database, pureName, schemaName, objectTypeField } = data;
const openNewTab = useOpenNewTab();
const onClick = ({ schemaName, pureName }) => {
openDatabaseObjectDetail(
openNewTab,
defaultTabs[objectTypeField],
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
{
schemaName,
pureName,
conid,
database,
objectTypeField,
},
false
);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={schemaName ? `${schemaName}.${pureName}` : pureName}
icon={icons[objectTypeField]}
onClick={onClick}
Menu={Menu}
/>
);
}
DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) =>
schemaName ? `${schemaName}.${pureName}` : pureName;
DatabaseObjectAppObject.createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
export default DatabaseObjectAppObject;

View File

@@ -0,0 +1,310 @@
<script lang="ts" context="module">
export const extractKey = ({ schemaName, pureName }) => (schemaName ? `${schemaName}.${pureName}` : pureName);
export const createMatcher = ({ pureName }) => filter => filterName(filter, pureName);
const icons = {
tables: 'img table',
views: 'img view',
procedures: 'img procedure',
functions: 'img function',
};
const defaultTabs = {
tables: 'TableDataTab',
views: 'ViewDataTab',
};
const menus = {
tables: [
{
label: 'Open data',
tab: 'TableDataTab',
forceNewTab: true,
},
{
label: 'Open form',
tab: 'TableDataTab',
forceNewTab: true,
initialData: {
grid: {
isFormView: true,
},
},
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
divider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
divider: true,
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
views: [
{
label: 'Open data',
tab: 'ViewDataTab',
forceNewTab: true,
},
{
label: 'Open structure',
tab: 'TableStructureTab',
},
{
label: 'Query designer',
isQueryDesigner: true,
},
{
divider: true,
},
{
label: 'Export',
isExport: true,
},
{
label: 'Open in free table editor',
isOpenFreeTable: true,
},
{
label: 'Open active chart',
isActiveChart: true,
},
{
divider: true,
},
{
label: 'SQL: CREATE VIEW',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: CREATE TABLE',
sqlTemplate: 'CREATE TABLE',
},
],
procedures: [
{
label: 'SQL: CREATE PROCEDURE',
sqlTemplate: 'CREATE OBJECT',
},
{
label: 'SQL: EXECUTE',
sqlTemplate: 'EXECUTE PROCEDURE',
},
],
functions: [
{
label: 'SQL: CREATE FUNCTION',
sqlTemplate: 'CREATE OBJECT',
},
],
};
export async function openDatabaseObjectDetail(
tabComponent,
sqlTemplate,
{ schemaName, pureName, conid, database, objectTypeField },
forceNewTab,
initialData
) {
const connection = await getConnectionInfo({ conid });
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
schemaName,
pureName,
})}`;
openNewTab(
{
title: sqlTemplate ? 'Query #' : pureName,
tooltip,
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
props: {
schemaName,
pureName,
conid,
database,
objectTypeField,
initialArgs: sqlTemplate ? { sqlTemplate } : null,
},
},
initialData,
{ forceNewTab }
);
}
</script>
<script lang="ts">
import _ from 'lodash';
import AppObjectCore from './AppObjectCore.svelte';
import { currentDatabase, extensions, openedConnections } from '../stores';
import openNewTab from '../utility/openNewTab';
import { filterName } from 'dbgate-datalib';
import { getConnectionInfo } from '../utility/metadataLoaders';
import fullDisplayName from '../utility/fullDisplayName';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import { findEngineDriver } from 'dbgate-tools';
import uuidv1 from 'uuid/v1';
export let data;
function handleClick() {
const { schemaName, pureName, conid, database, objectTypeField } = data;
openDatabaseObjectDetail(
defaultTabs[objectTypeField],
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
{
schemaName,
pureName,
conid,
database,
objectTypeField,
},
false,
null
);
// openNewTab({
// title: data.pureName,
// icon: 'img table',
// tabComponent: 'TableDataTab',
// props: {
// schemaName,
// pureName,
// conid,
// database,
// objectTypeField,
// },
// });
}
const getDriver = async () => {
const conn = await getConnectionInfo(data);
if (!conn) return;
const driver = findEngineDriver(conn, $extensions);
return driver;
};
function createMenu() {
const { objectTypeField } = data;
return menus[objectTypeField].map(menu => {
if (menu.divider) return menu;
return {
text: menu.label,
onClick: async () => {
if (menu.isExport) {
showModal(ImportExportModal, {
initialValues: {
sourceStorageType: 'database',
sourceConnectionId: data.conid,
sourceDatabaseName: data.database,
sourceSchemaName: data.schemaName,
sourceList: [data.pureName],
},
});
} else if (menu.isOpenFreeTable) {
const coninfo = await getConnectionInfo(data);
openNewTab({
title: data.pureName,
icon: 'img free-table',
tabComponent: 'FreeTableTab',
props: {
initialArgs: {
functionName: 'tableReader',
props: {
connection: {
...coninfo,
database: data.database,
},
schemaName: data.schemaName,
pureName: data.pureName,
},
},
},
});
} else if (menu.isActiveChart) {
const driver = await getDriver();
const dmp = driver.createDumper();
dmp.put('^select * from %f', data);
openNewTab(
{
title: data.pureName,
icon: 'img chart',
tabComponent: 'ChartTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
config: { chartType: 'bar' },
sql: dmp.s,
},
}
);
} else if (menu.isQueryDesigner) {
openNewTab(
{
title: 'Query #',
icon: 'img query-design',
tabComponent: 'QueryDesignTab',
props: {
conid: data.conid,
database: data.database,
},
},
{
editor: {
tables: [
{
...data,
designerId: uuidv1(),
left: 50,
top: 50,
},
],
},
}
);
} else {
openDatabaseObjectDetail(menu.tab, menu.sqlTemplate, data, menu.forceNewTab, menu.initialData);
}
},
};
});
}
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.schemaName ? `${data.schemaName}.${data.pureName}` : data.pureName}
icon={icons[data.objectTypeField]}
menu={createMenu}
on:click={handleClick}
on:expand
/>

View File

@@ -1,104 +0,0 @@
import React from 'react';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import FavoriteModal from '../modals/FavoriteModal';
import useShowModal from '../modals/showModal';
import axios from '../utility/axios';
import { copyTextToClipboard } from '../utility/clipboard';
import getElectron from '../utility/getElectron';
import useOpenNewTab from '../utility/useOpenNewTab';
import { SavedFileAppObjectBase } from './SavedFileAppObject';
export function useOpenFavorite() {
const openNewTab = useOpenNewTab();
const openFavorite = React.useCallback(
async favorite => {
const { icon, tabComponent, title, props, tabdata } = favorite;
let tabdataNew = tabdata;
if (props.savedFile) {
const resp = await axios.post('files/load', {
folder: props.savedFolder,
file: props.savedFile,
format: props.savedFormat,
});
tabdataNew = {
...tabdata,
editor: resp.data,
};
}
openNewTab(
{
title,
icon: icon || 'img favorite',
props,
tabComponent,
},
tabdataNew
);
},
[openNewTab]
);
return openFavorite;
}
export function FavoriteFileAppObject({ data, commonProps }) {
const { icon, tabComponent, title, props, tabdata, urlPath } = data;
const openNewTab = useOpenNewTab();
const showModal = useShowModal();
const openFavorite = useOpenFavorite();
const electron = getElectron();
const editFavorite = () => {
showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
};
const editFavoriteJson = async () => {
const resp = await axios.post('files/load', {
folder: 'favorites',
file: data.file,
format: 'text',
});
openNewTab(
{
icon: 'icon favorite',
title,
tabComponent: 'FavoriteEditorTab',
props: {
savedFile: data.file,
savedFormat: 'text',
savedFolder: 'favorites',
},
},
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
);
};
const copyLink = () => {
copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`);
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon={icon || 'img favorite'}
title={title}
disableRename
onLoad={async data => {
openFavorite(data);
}}
menuExt={
<>
<DropDownMenuItem onClick={editFavorite}>Edit</DropDownMenuItem>
<DropDownMenuItem onClick={editFavoriteJson}>Edit JSON definition</DropDownMenuItem>
{!electron && urlPath && <DropDownMenuItem onClick={copyLink}>Copy link</DropDownMenuItem>}
</>
}
/>
);
}
FavoriteFileAppObject.extractKey = data => data.file;

View File

@@ -0,0 +1,100 @@
<script lang="ts" context="module">
import AppObjectCore from './AppObjectCore.svelte';
export const extractKey = data => data.file;
export async function openFavorite(favorite) {
const { icon, tabComponent, title, props, tabdata } = favorite;
let tabdataNew = tabdata;
if (props.savedFile) {
const resp = await axiosInstance.post('files/load', {
folder: props.savedFolder,
file: props.savedFile,
format: props.savedFormat,
});
tabdataNew = {
...tabdata,
editor: resp.data,
};
}
openNewTab(
{
title,
icon: icon || 'img favorite',
props,
tabComponent,
},
tabdataNew
);
}
</script>
<script lang="ts">
import axiosInstance from '../utility/axiosInstance';
import openNewTab from '../utility/openNewTab';
import { copyTextToClipboard } from '../utility/clipboard';
import { showModal } from '../modals/modalTools';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import getElectron from '../utility/getElectron';
export let data;
const electron = getElectron();
const editFavorite = () => {
// showModal(modalState => <FavoriteModal modalState={modalState} editingData={data} />);
};
const editFavoriteJson = async () => {
const resp = await axiosInstance.post('files/load', {
folder: 'favorites',
file: data.file,
format: 'text',
});
openNewTab(
{
icon: 'icon favorite',
title: data.title,
tabComponent: 'FavoriteEditorTab',
props: {
savedFile: data.file,
savedFormat: 'text',
savedFolder: 'favorites',
},
},
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
);
};
const copyLink = () => {
copyTextToClipboard(`${document.location.origin}#favorite=${data.urlPath}`);
};
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete favorite ${data.title}?`,
onConfirm: () => {
axiosInstance.post('files/delete', { file: data.file, folder: 'favorites' });
},
});
};
function createMenu() {
return [
{ text: 'Delete', onClick: handleDelete },
{ text: 'Edit', onClick: editFavorite },
{ text: 'Edit JSON definition', onClick: editFavoriteJson },
!electron && data.urlPath && { text: 'Copy link', onClick: copyLink },
];
}
</script>
<AppObjectCore
{...$$restProps}
{data}
icon={data.icon || 'img favorite'}
title={data.title}
menu={createMenu}
on:click={() => openFavorite(data)}
/>

View File

@@ -1,15 +0,0 @@
import _ from 'lodash';
import React from 'react';
import { filterName } from 'dbgate-datalib';
import { AppObjectCore } from './AppObjectCore';
function MacroAppObject({ data, commonProps }) {
const { name, type, title, group } = data;
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
}
MacroAppObject.extractKey = data => data.name;
MacroAppObject.createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
export default MacroAppObject;

View File

@@ -0,0 +1,23 @@
<script lang="ts" context="module">
export const extractKey = data => data.name;
export const createMatcher = ({ name, title }) => filter => filterName(filter, name, title);
</script>
<script lang="ts">
import { filterName } from 'dbgate-datalib';
import { getContext } from 'svelte';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
const selectedMacro = getContext('selectedMacro') as any;
</script>
<AppObjectCore
{...$$restProps}
{data}
title={data.title}
icon="img macro"
isBold={$selectedMacro?.name == data.name}
on:click={() => ($selectedMacro = data)}
/>

View File

@@ -1,314 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import _ from 'lodash';
import { DropDownMenuItem } from '../modals/DropDownMenu';
import { AppObjectCore } from './AppObjectCore';
import useNewQuery from '../query/useNewQuery';
import { useCurrentDatabase } from '../utility/globalState';
import ScriptWriter from '../impexp/ScriptWriter';
import { extractPackageName } from 'dbgate-tools';
import useShowModal from '../modals/showModal';
import InputTextModal from '../modals/InputTextModal';
import useHasPermission from '../utility/useHasPermission';
import useOpenNewTab from '../utility/useOpenNewTab';
import ConfirmModal from '../modals/ConfirmModal';
function Menu({ data, menuExt = null, title = undefined, disableRename = false }) {
const hasPermission = useHasPermission();
const showModal = useShowModal();
const handleDelete = () => {
showModal(modalState => (
<ConfirmModal
modalState={modalState}
message={`Really delete file ${title || data.file}?`}
onConfirm={() => {
axios.post('files/delete', data);
}}
/>
));
};
const handleRename = () => {
showModal(modalState => (
<InputTextModal
modalState={modalState}
value={data.file}
label="New file name"
header="Rename file"
onConfirm={newFile => {
axios.post('files/rename', { ...data, newFile });
}}
/>
));
};
return (
<>
{hasPermission(`files/${data.folder}/write`) && (
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
)}
{hasPermission(`files/${data.folder}/write`) && !disableRename && (
<DropDownMenuItem onClick={handleRename}>Rename</DropDownMenuItem>
)}
{menuExt}
</>
);
}
export function SavedFileAppObjectBase({
data,
commonProps,
format,
icon,
onLoad,
title = undefined,
menuExt = null,
disableRename = false,
}) {
const { file, folder } = data;
const onClick = async () => {
const resp = await axios.post('files/load', { folder, file, format });
onLoad(resp.data);
};
return (
<AppObjectCore
{...commonProps}
data={data}
title={title || file}
icon={icon}
onClick={onClick}
Menu={props => <Menu {...props} menuExt={menuExt} title={title} disableRename={disableRename} />}
/>
);
}
export function SavedSqlFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const newQuery = useNewQuery();
const currentDatabase = useCurrentDatabase();
const openNewTab = useOpenNewTab();
const connection = _.get(currentDatabase, 'connection');
const database = _.get(currentDatabase, 'name');
const handleGenerateExecute = () => {
const script = new ScriptWriter();
const conn = {
..._.omit(connection, ['displayName', '_id']),
database,
};
script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`);
script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`);
// @ts-ignore
script.requirePackage(extractPackageName(conn.engine));
openNewTab(
{
title: 'Shell #',
icon: 'img shell',
tabComponent: 'ShellTab',
},
{ editor: script.getScript() }
);
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img sql-file"
menuExt={
connection && database ? (
<DropDownMenuItem onClick={handleGenerateExecute}>Generate shell execute</DropDownMenuItem>
) : null
}
onLoad={data => {
newQuery({
title: file,
initialData: data,
// @ts-ignore
savedFile: file,
savedFolder: 'sql',
savedFormat: 'text',
});
}}
/>
);
}
export function SavedShellFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img shell"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img shell',
tabComponent: 'ShellTab',
props: {
savedFile: file,
savedFolder: 'shell',
savedFormat: 'text',
},
},
{ editor: data }
);
}}
/>
);
}
export function SavedChartFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const currentDatabase = useCurrentDatabase();
const connection = _.get(currentDatabase, 'connection') || {};
const database = _.get(currentDatabase, 'name');
const tooltip = `${connection.displayName || connection.server}\n${database}`;
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon="img chart"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img chart',
tooltip,
props: {
conid: connection._id,
database,
savedFile: file,
savedFolder: 'charts',
savedFormat: 'json',
},
tabComponent: 'ChartTab',
},
{ editor: data }
);
}}
/>
);
}
export function SavedQueryFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const currentDatabase = useCurrentDatabase();
const connection = _.get(currentDatabase, 'connection') || {};
const database = _.get(currentDatabase, 'name');
const tooltip = `${connection.displayName || connection.server}\n${database}`;
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="json"
icon="img query-design"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img query-design',
tooltip,
props: {
conid: connection._id,
database,
savedFile: file,
savedFolder: 'query',
savedFormat: 'json',
},
tabComponent: 'QueryDesignTab',
},
{ editor: data }
);
}}
/>
);
}
export function SavedMarkdownFileAppObject({ data, commonProps }) {
const { file, folder } = data;
const openNewTab = useOpenNewTab();
const showPage = () => {
openNewTab({
title: file,
icon: 'img markdown',
tabComponent: 'MarkdownViewTab',
props: {
savedFile: file,
savedFolder: 'markdown',
savedFormat: 'text',
},
});
};
return (
<SavedFileAppObjectBase
data={data}
commonProps={commonProps}
format="text"
icon="img markdown"
onLoad={data => {
openNewTab(
{
title: file,
icon: 'img markdown',
tabComponent: 'MarkdownEditorTab',
props: {
savedFile: file,
savedFolder: 'markdown',
savedFormat: 'text',
},
},
{ editor: data }
);
}}
menuExt={<DropDownMenuItem onClick={showPage}>Show page</DropDownMenuItem>}
/>
);
}
export function SavedFileAppObject({ data, commonProps }) {
const { folder } = data;
const folderTypes = {
sql: SavedSqlFileAppObject,
shell: SavedShellFileAppObject,
charts: SavedChartFileAppObject,
markdown: SavedMarkdownFileAppObject,
query: SavedQueryFileAppObject,
};
const AppObject = folderTypes[folder];
if (AppObject) {
return <AppObject data={data} commonProps={commonProps} />;
}
return null;
}
[
SavedSqlFileAppObject,
SavedShellFileAppObject,
SavedChartFileAppObject,
SavedMarkdownFileAppObject,
SavedFileAppObject,
].forEach(fn => {
// @ts-ignore
fn.extractKey = data => data.file;
});

View File

@@ -0,0 +1,154 @@
<script lang="ts" context="module">
interface FileTypeHandler {
icon: string;
format: string;
tabComponent: string;
folder: string;
currentConnection: boolean;
}
const sql: FileTypeHandler = {
icon: 'img sql-file',
format: 'text',
tabComponent: 'QueryTab',
folder: 'sql',
currentConnection: true,
};
const shell: FileTypeHandler = {
icon: 'img shell',
format: 'text',
tabComponent: 'ShellTab',
folder: 'shell',
currentConnection: false,
};
const markdown: FileTypeHandler = {
icon: 'img markdown',
format: 'text',
tabComponent: 'MarkdownEditorTab',
folder: 'markdown',
currentConnection: false,
};
const charts: FileTypeHandler = {
icon: 'img chart',
format: 'json',
tabComponent: 'ChartTab',
folder: 'charts',
currentConnection: true,
};
const query: FileTypeHandler = {
icon: 'img query-design',
format: 'json',
tabComponent: 'QueryDesignTab',
folder: 'query',
currentConnection: true,
};
const HANDLERS = {
sql,
shell,
markdown,
charts,
query,
};
export const extractKey = data => data.file;
</script>
<script lang="ts">
import _ from 'lodash';
import ConfirmModal from '../modals/ConfirmModal.svelte';
import InputTextModal from '../modals/InputTextModal.svelte';
import { showModal } from '../modals/modalTools';
import { currentDatabase } from '../stores';
import axiosInstance from '../utility/axiosInstance';
import hasPermission from '../utility/hasPermission';
import openNewTab from '../utility/openNewTab';
import AppObjectCore from './AppObjectCore.svelte';
export let data;
$: folder = data?.folder;
$: handler = HANDLERS[folder] as FileTypeHandler;
const showMarkdownPage = () => {
openNewTab({
title: data.file,
icon: 'img markdown',
tabComponent: 'MarkdownViewTab',
props: {
savedFile: data.file,
savedFolder: 'markdown',
savedFormat: 'text',
},
});
};
function createMenu() {
return [
{ text: 'Open', onClick: openTab },
hasPermission(`files/${data.folder}/write`) && { text: 'Rename', onClick: handleRename },
hasPermission(`files/${data.folder}/write`) && { text: 'Delete', onClick: handleDelete },
folder == 'markdown' && { text: 'Show page', onClick: showMarkdownPage },
];
}
const handleDelete = () => {
showModal(ConfirmModal, {
message: `Really delete file ${data.file}?`,
onConfirm: () => {
axiosInstance.post('files/delete', data);
},
});
};
const handleRename = () => {
showModal(InputTextModal, {
value: data.file,
label: 'New file name',
header: 'Rename file',
onConfirm: newFile => {
axiosInstance.post('files/rename', { ...data, newFile });
},
});
};
async function openTab() {
const resp = await axiosInstance.post('files/load', { folder, file: data.file, format: handler.format });
const connProps: any = {};
let tooltip = undefined;
if (handler.currentConnection) {
const connection = _.get($currentDatabase, 'connection') || {};
const database = _.get($currentDatabase, 'name');
connProps.conid = connection._id;
connProps.database = database;
tooltip = `${connection.displayName || connection.server}\n${database}`;
}
openNewTab(
{
title: data.file,
icon: handler.icon,
tabComponent: handler.tabComponent,
tooltip,
props: {
savedFile: data.file,
savedFolder: handler.folder,
savedFormat: handler.format,
...connProps,
},
},
{ editor: resp.data }
);
}
</script>
<AppObjectCore {...$$restProps} {data} icon={handler?.icon} title={data?.file} menu={createMenu()} on:click={openTab} />

View File

@@ -1,36 +0,0 @@
import { findForeignKeyForColumn } from 'dbgate-tools';
import React from 'react';
import { getColumnIcon } from '../datagrid/ColumnLabel';
import { AppObjectCore } from './AppObjectCore';
import { AppObjectList } from './AppObjectList';
function ColumnAppObject({ data, commonProps }) {
const { columnName, dataType, foreignKey } = data;
let extInfo = dataType;
if (foreignKey) extInfo += ` -> ${foreignKey.refTableName}`;
return (
<AppObjectCore
{...commonProps}
data={data}
title={columnName}
extInfo={extInfo}
icon={getColumnIcon(data, true)}
disableHover
/>
);
}
ColumnAppObject.extractKey = ({ columnName }) => columnName;
export default function SubColumnParamList({ data }) {
const { columns } = data;
return (
<AppObjectList
list={(columns || []).map(col => ({
...col,
foreignKey: findForeignKeyForColumn(data, col),
}))}
AppObjectComponent={ColumnAppObject}
/>
);
}

View File

@@ -0,0 +1,16 @@
<script lang="ts">
import { findForeignKeyForColumn } from 'dbgate-tools';
import AppObjectList from './AppObjectList.svelte';
import * as columnAppObject from './ColumnAppObject.svelte';
export let data;
</script>
<AppObjectList
list={(data.columns || []).map(col => ({
...col,
foreignKey: findForeignKeyForColumn(data, col),
}))}
module={columnAppObject}
/>

View File

@@ -0,0 +1,11 @@
<script lang="ts">
import { useDatabaseList } from '../utility/metadataLoaders';
import AppObjectList from './AppObjectList.svelte';
import * as databaseAppObject from './DatabaseAppObject.svelte';
export let data;
$: databases = useDatabaseList({ conid: data._id });
</script>
<AppObjectList list={($databases || []).map(db => ({ ...db, connection: data }))} module={databaseAppObject} />

View File

@@ -1,99 +0,0 @@
import React from 'react';
import _ from 'lodash';
import { SelectField } from '../utility/inputs';
import ErrorInfo from '../widgets/ErrorInfo';
import styled from 'styled-components';
import { TextCellViewWrap, TextCellViewNoWrap } from './TextCellView';
import JsonCellView from './JsonCellDataView';
import useTheme from '../theme/useTheme';
const Toolbar = styled.div`
display: flex;
background: ${props => props.theme.toolbar_background};
align-items: center;
`;
const MainWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
`;
const DataWrapper = styled.div`
display: flex;
flex: 1;
`;
const formats = [
{
type: 'textWrap',
title: 'Text (wrap)',
Component: TextCellViewWrap,
single: true,
},
{
type: 'text',
title: 'Text (no wrap)',
Component: TextCellViewNoWrap,
single: true,
},
{
type: 'json',
title: 'Json',
Component: JsonCellView,
single: true,
},
];
function autodetect(selection, grider, value) {
if (_.isString(value)) {
if (value.startsWith('[') || value.startsWith('{')) return 'json';
}
return 'textWrap';
}
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
const theme = useTheme();
let value = null;
if (grider && selection && selection.length == 1) {
const rowData = grider.getRowData(selection[0].row);
const { column } = selection[0];
if (rowData) value = rowData[column];
}
if (selectedValue) {
value = selectedValue;
}
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
const autodetectFormat = formats.find(x => x.type == autodetectFormatType);
const usedFormatType = selectedFormatType == 'autodetect' ? autodetectFormatType : selectedFormatType;
const usedFormat = formats.find(x => x.type == usedFormatType);
const { Component } = usedFormat || {};
return (
<MainWrapper>
<Toolbar theme={theme}>
Format:
<SelectField value={selectedFormatType} onChange={e => setSelectedFormatType(e.target.value)}>
<option value="autodetect">Autodetect - {autodetectFormat.title}</option>
{formats.map(fmt => (
<option value={fmt.type} key={fmt.type}>
{fmt.title}
</option>
))}
</SelectField>
</Toolbar>
<DataWrapper>
{usedFormat == null || (usedFormat.single && value == null) ? (
<ErrorInfo message="Must be selected one cell" />
) : (
<Component value={value} grider={grider} selection={selection} />
)}
</DataWrapper>
</MainWrapper>
);
}

View File

@@ -1,35 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ReactJson from 'react-json-view';
import ErrorInfo from '../widgets/ErrorInfo';
import useTheme from '../theme/useTheme';
const OuterWrapper = styled.div`
flex: 1;
position: relative;
`;
const InnerWrapper = styled.div`
overflow: scroll;
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
`;
export default function JsonCellView({ value }) {
const theme = useTheme();
try {
const json = React.useMemo(() => JSON.parse(value), [value]);
return (
<OuterWrapper>
<InnerWrapper>
<ReactJson src={json} theme={theme.jsonViewerTheme} />
</InnerWrapper>
</OuterWrapper>
);
} catch (err) {
return <ErrorInfo message="Error parsing JSON" />;
}
}

View File

@@ -1,22 +0,0 @@
import React from 'react';
import styled from 'styled-components';
const StyledInput = styled.textarea`
flex: 1;
`;
export function TextCellViewWrap({ value, grider, selection }) {
return <StyledInput value={value} wrap="hard" readOnly />;
}
export function TextCellViewNoWrap({ value, grider, selection }) {
return (
<StyledInput
value={value}
wrap="off"
readOnly
// readOnly={grider ? !grider.editable : true}
// onChange={(e) => grider.setCellValue(selection[0].row, selection[0].column, e.target.value)}
/>
);
}

View File

@@ -0,0 +1,36 @@
<script lang="ts">
import { onMount, afterUpdate, onDestroy } from 'svelte';
import Chart from 'chart.js';
import contextMenu from '../utility/contextMenu';
export let data;
export let type = 'line';
export let options = {};
export let plugins = {};
export let menu;
let chart = null;
let domChart;
onMount(() => {
chart = new Chart(domChart, {
type,
data,
options,
plugins,
});
});
afterUpdate(() => {
if (!chart) return;
chart.data = data;
chart.type = type;
chart.options = options;
chart.plugins = plugins;
chart.update();
});
onDestroy(() => {
chart = null;
});
</script>
<canvas bind:this={domChart} {...$$restProps} use:contextMenu={menu} />

View File

@@ -1,154 +0,0 @@
import React from 'react';
import Chart from 'react-chartjs-2';
import _ from 'lodash';
import styled from 'styled-components';
import useTheme from '../theme/useTheme';
import useDimensions from '../utility/useDimensions';
import { HorizontalSplitter } from '../widgets/Splitter';
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
import { FormCheckboxField, FormSelectField, FormTextField } from '../utility/forms';
import DataChart from './DataChart';
import { FormProviderCore } from '../utility/FormProvider';
import { loadChartData, loadChartStructure } from './chartDataLoader';
import useExtensions from '../utility/useExtensions';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { findEngineDriver } from 'dbgate-tools';
import { FormFieldTemplateTiny } from '../utility/formStyle';
import { ManagerInnerContainer } from '../datagrid/ManagerStyles';
import { presetPrimaryColors } from '@ant-design/colors';
import ErrorInfo from '../widgets/ErrorInfo';
const LeftContainer = styled.div`
background-color: ${props => props.theme.manager_background};
display: flex;
flex: 1;
`;
export default function ChartEditor({ data, config, setConfig, sql, conid, database }) {
const [managerSize, setManagerSize] = React.useState(0);
const theme = useTheme();
const extensions = useExtensions();
const [error, setError] = React.useState(null);
const [availableColumnNames, setAvailableColumnNames] = React.useState([]);
const [loadedData, setLoadedData] = React.useState(null);
const getDriver = async () => {
const conn = await getConnectionInfo({ conid });
if (!conn) return;
const driver = findEngineDriver(conn, extensions);
return driver;
};
const handleLoadColumns = async () => {
const driver = await getDriver();
if (!driver) return;
try {
const columns = await loadChartStructure(driver, conid, database, sql);
setAvailableColumnNames(columns);
} catch (err) {
setError(err.message);
}
};
const handleLoadData = async () => {
const driver = await getDriver();
if (!driver) return;
const loaded = await loadChartData(driver, conid, database, sql, config);
if (!loaded) return;
const { columns, rows } = loaded;
setLoadedData({
structure: columns,
rows,
});
};
React.useEffect(() => {
if (sql && conid && database) {
handleLoadColumns();
}
}, [sql, conid, database, extensions]);
React.useEffect(() => {
if (data) {
setAvailableColumnNames(data ? data.structure.columns.map(x => x.columnName) : []);
}
}, [data]);
React.useEffect(() => {
if (config.labelColumn && sql && conid && database) {
handleLoadData();
}
}, [config, sql, conid, database, availableColumnNames]);
if (error) {
return (
<div>
<ErrorInfo message={error} />
</div>
);
}
return (
<FormProviderCore values={config} setValues={setConfig} template={FormFieldTemplateTiny}>
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
<LeftContainer theme={theme}>
<WidgetColumnBar>
<WidgetColumnBarItem title="Style" name="style" height="40%">
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
<FormSelectField label="Chart type" name="chartType">
<option value="bar">Bar</option>
<option value="line">Line</option>
{/* <option value="radar">Radar</option> */}
<option value="pie">Pie</option>
<option value="polarArea">Polar area</option>
{/* <option value="bubble">Bubble</option>
<option value="scatter">Scatter</option> */}
</FormSelectField>
<FormTextField label="Color set" name="colorSeed" />
<FormSelectField label="Truncate from" name="truncateFrom">
<option value="begin">Begin</option>
<option value="end">End (most recent data for datetime)</option>
</FormSelectField>
<FormTextField label="Truncate limit" name="truncateLimit" />
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Data" name="data">
<ManagerInnerContainer style={{ maxWidth: managerSize }}>
{availableColumnNames.length > 0 && (
<FormSelectField label="Label column" name="labelColumn">
<option value=""></option>
{availableColumnNames.map(col => (
<option value={col} key={col}>
{col}
</option>
))}
</FormSelectField>
)}
{availableColumnNames.map(col => (
<React.Fragment key={col}>
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
{config[`dataColumn_${col}`] && (
<FormSelectField label="Color" name={`dataColumnColor_${col}`}>
<option value="">Random</option>
{_.keys(presetPrimaryColors).map(color => (
<option value={color} key={color}>
{_.startCase(color)}
</option>
))}
</FormSelectField>
)}
</React.Fragment>
))}
</ManagerInnerContainer>
</WidgetColumnBarItem>
</WidgetColumnBar>
</LeftContainer>
<DataChart data={data || loadedData} />
</HorizontalSplitter>
</FormProviderCore>
);
}

View File

@@ -0,0 +1,160 @@
<script lang="ts">
import AboutModal from '../modals/AboutModal.svelte';
import { presetPrimaryColors } from '@ant-design/colors';
import { startCase } from 'lodash';
import FormProviderCore from '../forms/FormProviderCore.svelte';
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import FormSelectField from '../forms/FormSelectField.svelte';
import FormTextField from '../forms/FormTextField.svelte';
import FormCheckboxField from '../forms/FormCheckboxField.svelte';
import { writable } from 'svelte/store';
import FormFieldTemplateTiny from '../forms/FormFieldTemplateTiny.svelte';
import createRef from '../utility/createRef';
import { getConnectionInfo } from '../utility/metadataLoaders';
import { findEngineDriver } from 'dbgate-tools';
import { extensions } from '../stores';
import { loadChartData, loadChartStructure } from './chartDataLoader';
import DataChart from './DataChart.svelte';
import _ from 'lodash';
export let data;
export let configStore;
export let conid;
export let database;
export let sql;
export let menu;
let availableColumnNames = [];
let error = null;
let loadedData = null;
$: config = $configStore;
const getDriver = async () => {
const conn = await getConnectionInfo({ conid });
if (!conn) return;
const driver = findEngineDriver(conn, $extensions);
return driver;
};
const handleLoadColumns = async () => {
const driver = await getDriver();
if (!driver) return;
try {
const columns = await loadChartStructure(driver, conid, database, sql);
availableColumnNames = columns;
} catch (err) {
error = err.message;
}
};
const handleLoadData = async () => {
const driver = await getDriver();
if (!driver) return;
const loaded = await loadChartData(driver, conid, database, sql, config);
if (!loaded) return;
const { columns, rows } = loaded;
loadedData = {
structure: columns,
rows,
};
};
$: {
$extensions;
if (sql && conid && database) {
handleLoadColumns();
}
}
$: {
if (data) {
availableColumnNames = data.structure.columns.map(x => x.columnName);
}
}
$: {
$extensions;
if (config.labelColumn && sql && conid && database) {
handleLoadData();
}
}
let managerSize;
</script>
<FormProviderCore values={configStore} template={FormFieldTemplateTiny}>
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title="Style" name="style" height="40%">
<ManagerInnerContainer width={managerSize}>
<FormSelectField
label="Chart type"
name="chartType"
isNative
options={[
{ value: 'bar', label: 'Bar' },
{ value: 'line', label: 'Line' },
{ value: 'pie', label: 'Pie' },
{ value: 'polarArea', label: 'Polar area' },
]}
/>
<FormTextField label="Color set" name="colorSeed" />
<FormSelectField
label="Truncate from"
name="truncateFrom"
isNative
options={[
{ value: 'begin', label: 'Begin' },
{ value: 'end', label: 'End (most recent data for datetime)' },
]}
/>
<FormTextField label="Truncate limit" name="truncateLimit" />
<FormCheckboxField label="Show relative values" name="showRelativeValues" />
</ManagerInnerContainer>
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Data" name="data">
<ManagerInnerContainer width={managerSize}>
{#if availableColumnNames.length > 0}
<FormSelectField
label="Label column"
name="labelColumn"
isNative
options={[{ value: '' }, ...availableColumnNames.map(col => ({ value: col, label: col }))]}
/>
{/if}
{#each availableColumnNames as col (col)}
<FormCheckboxField label={col} name={`dataColumn_${col}`} />
{#if config[`dataColumn_${col}`]}
<FormSelectField
label="Color"
name={`dataColumnColor_${col}`}
isNative
options={[
{ value: '', label: 'Random' },
..._.keys(presetPrimaryColors).map(color => ({ value: color, label: _.startCase(color) })),
]}
/>
{/if}
{/each}
</ManagerInnerContainer>
</WidgetColumnBarItem>
</WidgetColumnBar>
</div>
<svelte:fragment slot="2">
<DataChart data={data || loadedData} {menu} />
</svelte:fragment>
</HorizontalSplitter>
</FormProviderCore>
<style>
.left {
background-color: var(--theme-bg-0);
display: flex;
flex: 1;
}
</style>

View File

@@ -1,15 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function ChartToolbar({ modelState, dispatchModel }) {
return (
<>
<ToolbarButton disabled={!modelState.canUndo} onClick={() => dispatchModel({ type: 'undo' })} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!modelState.canRedo} onClick={() => dispatchModel({ type: 'redo' })} icon="icon redo">
Redo
</ToolbarButton>
</>
);
}

View File

@@ -1,165 +0,0 @@
import React from 'react';
import _ from 'lodash';
import Chart from 'react-chartjs-2';
import randomcolor from 'randomcolor';
import styled from 'styled-components';
import useDimensions from '../utility/useDimensions';
import { useForm } from '../utility/FormProvider';
import useTheme from '../theme/useTheme';
import moment from 'moment';
const ChartWrapper = styled.div`
flex: 1;
overflow: hidden;
`;
function getTimeAxis(labels) {
const res = [];
for (const label of labels) {
const parsed = moment(label);
if (!parsed.isValid()) return null;
const iso = parsed.toISOString();
if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null;
res.push(parsed);
}
return res;
}
function getLabels(labelValues, timeAxis, chartType) {
if (!timeAxis) return labelValues;
if (chartType === 'line') return timeAxis.map(x => x.toDate());
return timeAxis.map(x => x.format('D. M. YYYY'));
}
function getOptions(timeAxis, chartType) {
if (timeAxis && chartType === 'line') {
return {
scales: {
xAxes: [
{
type: 'time',
distribution: 'linear',
time: {
tooltipFormat: 'D. M. YYYY HH:mm',
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'D.M hA',
day: 'D. M.',
week: 'D. M. YYYY',
month: 'MM-YYYY',
quarter: '[Q]Q - YYYY',
year: 'YYYY',
},
},
},
],
},
};
}
return {};
}
function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors, theme) {
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return [{}, {}];
const colors = randomcolor({
count: _.max([freeData.rows.length, dataColumns.length, 1]),
seed: colorSeed,
});
let backgroundColor = null;
let borderColor = null;
const labelValues = freeData.rows.map(x => x[labelColumn]);
const timeAxis = getTimeAxis(labelValues);
const labels = getLabels(labelValues, timeAxis, chartType);
const res = {
labels,
datasets: dataColumns.map((dataColumn, columnIndex) => {
if (chartType == 'line' || chartType == 'bar') {
const color = dataColumnColors[dataColumn];
if (color) {
backgroundColor = theme.main_palettes[color][4] + '80';
borderColor = theme.main_palettes[color][7];
} else {
backgroundColor = colors[columnIndex] + '80';
borderColor = colors[columnIndex];
}
} else {
backgroundColor = colors;
}
return {
label: dataColumn,
data: freeData.rows.map(row => row[dataColumn]),
backgroundColor,
borderColor,
borderWidth: 1,
};
}),
};
const options = getOptions(timeAxis, chartType);
return [res, options];
}
export function extractDataColumns(values) {
const dataColumns = [];
for (const key in values) {
if (key.startsWith('dataColumn_') && values[key]) {
dataColumns.push(key.substring('dataColumn_'.length));
}
}
return dataColumns;
}
export function extractDataColumnColors(values, dataColumns) {
const res = {};
for (const column of dataColumns) {
const color = values[`dataColumnColor_${column}`];
if (color) res[column] = color;
}
return res;
}
export default function DataChart({ data }) {
const [containerRef, { height: containerHeight, width: containerWidth }] = useDimensions();
const { values } = useForm();
const theme = useTheme();
const { labelColumn } = values;
const dataColumns = extractDataColumns(values);
const dataColumnColors = extractDataColumnColors(values, dataColumns);
const [chartData, options] = createChartData(
data,
labelColumn,
dataColumns,
values.colorSeed || '5',
values.chartType,
dataColumnColors,
theme
);
return (
<ChartWrapper ref={containerRef}>
<Chart
key={`${values.chartType}|${containerWidth}|${containerHeight}`}
width={containerWidth}
height={containerHeight}
data={chartData}
type={values.chartType}
options={{
...options,
// elements: {
// point: {
// radius: 0,
// },
// },
// tooltips: {
// mode: 'index',
// intersect: false,
// },
}}
/>
</ChartWrapper>
);
}

View File

@@ -0,0 +1,143 @@
<script lang="ts" context="module">
function getTimeAxis(labels) {
const res = [];
for (const label of labels) {
const parsed = moment(label);
if (!parsed.isValid()) return null;
const iso = parsed.toISOString();
if (iso < '1850-01-01T00:00:00' || iso > '2150-01-01T00:00:00') return null;
res.push(parsed);
}
return res;
}
function getLabels(labelValues, timeAxis, chartType) {
if (!timeAxis) return labelValues;
if (chartType === 'line') return timeAxis.map(x => x.toDate());
return timeAxis.map(x => x.format('D. M. YYYY'));
}
function getOptions(timeAxis, chartType) {
if (timeAxis && chartType === 'line') {
return {
scales: {
xAxes: [
{
type: 'time',
distribution: 'linear',
time: {
tooltipFormat: 'D. M. YYYY HH:mm',
displayFormats: {
millisecond: 'HH:mm:ss.SSS',
second: 'HH:mm:ss',
minute: 'HH:mm',
hour: 'D.M hA',
day: 'D. M.',
week: 'D. M. YYYY',
month: 'MM-YYYY',
quarter: '[Q]Q - YYYY',
year: 'YYYY',
},
},
},
],
},
};
}
return {};
}
function createChartData(freeData, labelColumn, dataColumns, colorSeed, chartType, dataColumnColors) {
if (!freeData || !labelColumn || !dataColumns || !freeData.rows || dataColumns.length == 0) return null;
const colors = randomcolor({
count: _.max([freeData.rows.length, dataColumns.length, 1]),
seed: colorSeed,
});
let backgroundColor = null;
let borderColor = null;
const labelValues = freeData.rows.map(x => x[labelColumn]);
const timeAxis = getTimeAxis(labelValues);
const labels = getLabels(labelValues, timeAxis, chartType);
const res = {
labels,
datasets: dataColumns.map((dataColumn, columnIndex) => {
if (chartType == 'line' || chartType == 'bar') {
const color = dataColumnColors[dataColumn];
if (color) {
backgroundColor = presetPalettes[color][4] + '80';
borderColor = presetPalettes[color][7];
} else {
backgroundColor = colors[columnIndex] + '80';
borderColor = colors[columnIndex];
}
} else {
backgroundColor = colors;
}
return {
label: dataColumn,
data: freeData.rows.map(row => row[dataColumn]),
backgroundColor,
borderColor,
borderWidth: 1,
};
}),
};
const options = getOptions(timeAxis, chartType);
return [res, options];
}
</script>
<script lang="ts">
import _ from 'lodash';
import randomcolor from 'randomcolor';
import moment from 'moment';
import ChartCore from './ChartCore.svelte';
import { getFormContext } from '../forms/FormProviderCore.svelte';
import { generate, presetPalettes, presetDarkPalettes, presetPrimaryColors } from '@ant-design/colors';
import { extractDataColumnColors, extractDataColumns } from './chartDataLoader';
export let data;
export let menu;
const { values } = getFormContext();
let clientWidth;
let clientHeight;
$: dataColumns = extractDataColumns($values);
$: dataColumnColors = extractDataColumnColors($values, dataColumns);
$: chartData = createChartData(
data,
$values.labelColumn,
dataColumns,
$values.colorSeed || '5',
$values.chartType,
dataColumnColors
);
</script>
<div class="wrapper" bind:clientWidth bind:clientHeight>
{#if chartData}
{#key `${$values.chartType}|${clientWidth}|${clientHeight}`}
<ChartCore
width={clientWidth}
height={clientHeight}
data={chartData[0]}
type={$values.chartType}
options={chartData[1]}
{menu}
/>
{/key}
{/if}
</div>
<style>
.wrapper {
flex: 1;
overflow: hidden;
}
</style>

View File

@@ -1,8 +1,7 @@
import { dumpSqlSelect, Select } from 'dbgate-sqltree';
import { EngineDriver } from 'dbgate-types';
import axios from '../utility/axios';
import axiosInstance from '../utility/axiosInstance';
import _ from 'lodash';
import { extractDataColumns } from './DataChart';
export async function loadChartStructure(driver: EngineDriver, conid, database, sql) {
const select: Select = {
@@ -17,7 +16,7 @@ export async function loadChartStructure(driver: EngineDriver, conid, database,
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s });
if (resp.data.errorMessage) throw new Error(resp.data.errorMessage);
return resp.data.columns.map(x => x.columnName);
}
@@ -75,7 +74,7 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
const dmp = driver.createDumper();
dumpSqlSelect(dmp, select);
const resp = await axios.post('database-connections/query-data', { conid, database, sql: dmp.s });
const resp = await axiosInstance.post('database-connections/query-data', { conid, database, sql: dmp.s });
let { rows, columns } = resp.data;
if (truncateFrom == 'end' && rows) {
rows = _.reverse([...rows]);
@@ -103,3 +102,21 @@ export async function loadChartData(driver: EngineDriver, conid, database, sql,
rows,
};
}
export function extractDataColumns(values) {
const dataColumns = [];
for (const key in values) {
if (key.startsWith('dataColumn_') && values[key]) {
dataColumns.push(key.substring('dataColumn_'.length));
}
}
return dataColumns;
}
export function extractDataColumnColors(values, dataColumns) {
const res = {};
for (const column of dataColumns) {
const color = values[`dataColumnColor_${column}`];
if (color) res[column] = color;
}
return res;
}

View File

@@ -0,0 +1,53 @@
<script lang="ts" context="module">
import { commands } from '../stores';
import { get } from 'svelte/store';
import { runGroupCommand } from './runCommand';
export function handleCommandKeyDown(e) {
let keyText = '';
if (e.ctrlKey) keyText += 'Ctrl+';
if (e.shiftKey) keyText += 'Shift+';
if (e.altKey) keyText += 'Alt+';
keyText += e.key;
// console.log('keyText', keyText);
const commandsValue = get(commands);
const commandsFiltered: any = Object.values(commandsValue).filter(
(x: any) =>
x.keyText &&
x.keyText
.toLowerCase()
.split('|')
.map(x => x.trim())
.includes(keyText.toLowerCase()) &&
(x.disableHandleKeyText == null ||
!x.disableHandleKeyText
.toLowerCase()
.split('|')
.map(x => x.trim())
.includes(keyText.toLowerCase()))
);
if (commandsFiltered.length > 0) {
e.preventDefault();
e.stopPropagation();
}
const notGroup = commandsFiltered.filter(x => x.enabled && !x.isGroupCommand);
if (notGroup.length == 1) {
const command = notGroup[0];
if (command.onClick) command.onClick();
return;
}
const group = commandsFiltered.filter(x => x.enabled && x.isGroupCommand);
if (group.length == 1) {
const command = group[0];
runGroupCommand(command.group);
}
}
</script>
<svelte:window on:keydown={handleCommandKeyDown} />

View File

@@ -0,0 +1,112 @@
<script context="module">
registerCommand({
id: 'commandPalette.show',
category: 'Command palette',
name: 'Show',
toolbarName: 'Menu',
toolbarOrder: 0,
keyText: 'F1',
toolbar: true,
showDisabled: true,
icon: 'icon menu',
onClick: () => visibleCommandPalette.set(true),
testEnabled: () => !getVisibleCommandPalette(),
});
</script>
<script>
import { filterName } from 'dbgate-datalib';
import _ from 'lodash';
import { derived } from 'svelte/store';
import { onMount } from 'svelte';
import { commands, getVisibleCommandPalette, visibleCommandPalette } from '../stores';
import clickOutside from '../utility/clickOutside';
import keycodes from '../utility/keycodes';
import registerCommand from './registerCommand';
let domInput;
let parentCommand;
let filter = '';
$: selectedIndex = true ? 0 : filter;
onMount(() => {
const oldFocus = document.activeElement;
domInput.focus();
return () => {
if (oldFocus) oldFocus.focus();
};
});
$: sortedComands = _.sortBy(
Object.values($commands).filter(x => x.enabled),
'text'
);
$: filteredItems = (parentCommand ? parentCommand.getSubCommands() : sortedComands).filter(
x => !x.isGroupCommand && filterName(filter, x.text)
);
function handleCommand(command) {
if (command.getSubCommands) {
parentCommand = command;
domInput.focus();
filter = '';
selectedIndex = 0;
} else {
$visibleCommandPalette = false;
command.onClick();
}
}
function handleKeyDown(e) {
if (e.keyCode == keycodes.upArrow && selectedIndex > 0) selectedIndex--;
if (e.keyCode == keycodes.downArrow && selectedIndex < filteredItems.length - 1) selectedIndex++;
if (e.keyCode == keycodes.enter) handleCommand(filteredItems[selectedIndex]);
if (e.keyCode == keycodes.escape) $visibleCommandPalette = false;
}
</script>
<div class="main" use:clickOutside on:clickOutside={() => ($visibleCommandPalette = false)}>
<div class="search">
<input type="text" bind:this={domInput} bind:value={filter} on:keydown={handleKeyDown} />
</div>
{#each filteredItems as command, index}
<div class="command" class:selected={index == selectedIndex} on:click={() => handleCommand(command)}>
<div>{command.text}</div>
{#if command.keyText}
<div class="shortcut">{command.keyText}</div>
{/if}
</div>
{/each}
</div>
<style>
.main {
width: 500px;
max-height: 500px;
background: var(--theme-bg-2);
padding: 5px;
}
.search {
display: flex;
}
input {
width: 100%;
}
.command {
padding: 5px;
display: flex;
justify-content: space-between;
}
.command:hover {
background: var(--theme-bg-3);
}
.command.selected {
background: var(--theme-bg-selected);
}
.shortcut {
background: var(--theme-bg-3);
}
</style>

View File

@@ -0,0 +1,67 @@
import { tick } from 'svelte';
import { commands } from '../stores';
import { GlobalCommand } from './registerCommand';
let isInvalidated = false;
export default async function invalidateCommands() {
if (isInvalidated) return;
isInvalidated = true;
await tick();
isInvalidated = false;
commands.update(dct => {
let res = null;
for (const command of Object.values(dct) as GlobalCommand[]) {
if (command.isGroupCommand) continue;
const { testEnabled } = command;
let enabled = command.enabled;
if (testEnabled) enabled = testEnabled();
if (enabled != command.enabled) {
if (!res) res = { ...dct };
res[command.id].enabled = enabled;
}
}
if (res) {
const values = Object.values(res) as GlobalCommand[];
// test enabled for group commands
for (const command of values) {
if (!command.isGroupCommand) continue;
const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand && x.enabled);
command.enabled = groupSources.length > 0;
// for (const source of groupSources) {
// source.keyTextFromGroup = command.keyText;
// }
}
}
return res || dct;
});
}
let isInvalidatedDefinitions = false;
export async function invalidateCommandDefinitions() {
if (isInvalidatedDefinitions) return;
isInvalidatedDefinitions = true;
await tick();
isInvalidatedDefinitions = false;
commands.update(dct => {
let res = { ...dct };
const values = Object.values(res) as GlobalCommand[];
// test enabled for group commands
for (const command of values) {
if (!command.isGroupCommand) continue;
const groupSources = values.filter(x => x.group == command.group && !x.isGroupCommand);
for (const source of groupSources) {
source.keyTextFromGroup = command.keyText;
}
}
return res;
});
invalidateCommands();
}

View File

@@ -0,0 +1,43 @@
import { commands } from '../stores';
import { invalidateCommandDefinitions } from './invalidateCommands';
export interface SubCommand {
text: string;
onClick: Function;
}
export interface GlobalCommand {
id: string;
category: string; // null for group commands
isGroupCommand?: boolean;
name: string;
text?: string /* category: name */;
keyText?: string;
keyTextFromGroup?: string; // automatically filled from group
group?: string;
getSubCommands?: () => SubCommand[];
onClick?: Function;
testEnabled?: () => boolean;
// enabledStore?: any;
icon?: string;
toolbar?: boolean;
enabled?: boolean;
showDisabled?: boolean;
toolbarName?: string;
menuName?: string;
toolbarOrder?: number;
disableHandleKeyText?: string;
}
export default function registerCommand(command: GlobalCommand) {
const { testEnabled } = command;
commands.update(x => ({
...x,
[command.id]: {
text: `${command.category}: ${command.name}`,
...command,
enabled: !testEnabled,
},
}));
invalidateCommandDefinitions();
}

View File

@@ -0,0 +1,26 @@
import { getCommands } from '../stores';
import { GlobalCommand } from './registerCommand';
export default function runCommand(id) {
const commandsValue = getCommands();
const command = commandsValue[id];
if (command) {
if (!command.enabled) return;
if (command.isGroupCommand) {
runGroupCommand(command.group);
} else {
if (command.onClick) {
command.onClick();
}
}
}
}
window['dbgate_runCommand'] = runCommand;
export function runGroupCommand(group) {
const commandsValue = getCommands();
const values = Object.values(commandsValue) as GlobalCommand[];
const real = values.find(x => x.group == group && !x.isGroupCommand && x.enabled);
if (real && real.onClick) real.onClick();
}

View File

@@ -0,0 +1,291 @@
import { currentTheme, extensions, getVisibleToolbar, visibleToolbar } from '../stores';
import registerCommand from './registerCommand';
import { derived, get } from 'svelte/store';
import { ThemeDefinition } from 'dbgate-types';
import ConnectionModal from '../modals/ConnectionModal.svelte';
import AboutModal from '../modals/AboutModal.svelte';
import ImportExportModal from '../modals/ImportExportModal.svelte';
import { showModal } from '../modals/modalTools';
import newQuery from '../query/newQuery';
import saveTabFile from '../utility/saveTabFile';
import openNewTab from '../utility/openNewTab';
import getElectron from '../utility/getElectron';
import { openElectronFile } from '../utility/openElectronFile';
import { getDefaultFileFormat } from '../plugins/fileformats';
const electron = getElectron();
function themeCommand(theme: ThemeDefinition) {
return {
text: theme.themeName,
onClick: () => currentTheme.set(theme.className),
// onPreview: () => {
// const old = get(currentTheme);
// currentTheme.set(css);
// return ok => {
// if (!ok) currentTheme.set(old);
// };
// },
};
}
registerCommand({
id: 'theme.changeTheme',
category: 'Theme',
name: 'Change',
getSubCommands: () => get(extensions).themes.map(themeCommand),
});
registerCommand({
id: 'toolbar.show',
category: 'Toolbar',
name: 'Show',
onClick: () => visibleToolbar.set(1),
testEnabled: () => !getVisibleToolbar(),
});
registerCommand({
id: 'toolbar.hide',
category: 'Toolbar',
name: 'Hide',
onClick: () => visibleToolbar.set(0),
testEnabled: () => getVisibleToolbar(),
});
registerCommand({
id: 'about.show',
category: 'About',
name: 'Show',
toolbarName: 'About',
onClick: () => showModal(AboutModal),
});
registerCommand({
id: 'new.connection',
toolbar: true,
icon: 'icon connection',
toolbarName: 'Add connection',
category: 'New',
toolbarOrder: 1,
name: 'Connection',
onClick: () => showModal(ConnectionModal),
});
registerCommand({
id: 'new.query',
category: 'New',
icon: 'icon file',
toolbar: true,
toolbarOrder: 2,
name: 'Query',
keyText: 'Ctrl+Q',
onClick: () => newQuery(),
});
registerCommand({
id: 'new.shell',
category: 'New',
icon: 'img shell',
name: 'JavaScript Shell',
onClick: () => {
openNewTab({
title: 'Shell #',
icon: 'img shell',
tabComponent: 'ShellTab',
});
},
});
registerCommand({
id: 'new.markdown',
category: 'New',
icon: 'img markdown',
name: 'Markdown page',
onClick: () => {
openNewTab({
title: 'Page #',
icon: 'img markdown',
tabComponent: 'MarkdownEditorTab',
});
},
});
registerCommand({
id: 'new.freetable',
category: 'New',
icon: 'img markdown',
name: 'Free table editor',
onClick: () => {
openNewTab({
title: 'Data #',
icon: 'img free-table',
tabComponent: 'FreeTableTab',
});
},
});
registerCommand({
id: 'group.save',
category: null,
isGroupCommand: true,
name: 'Save',
keyText: 'Ctrl+S',
group: 'save',
});
registerCommand({
id: 'group.saveAs',
category: null,
isGroupCommand: true,
name: 'Save As',
keyText: 'Ctrl+Shift+S',
group: 'saveAs',
});
registerCommand({
id: 'group.undo',
category: null,
isGroupCommand: true,
name: 'Undo',
keyText: 'Ctrl+Z',
group: 'undo',
});
registerCommand({
id: 'group.redo',
category: null,
isGroupCommand: true,
name: 'Redo',
keyText: 'Ctrl+Y',
group: 'redo',
});
if (electron) {
registerCommand({
id: 'file.open',
category: 'File',
name: 'Open',
keyText: 'Ctrl+O',
onClick: openElectronFile,
});
}
registerCommand({
id: 'file.import',
category: 'File',
name: 'Import data',
toolbar: true,
icon: 'icon import',
onClick: () =>
showModal(ImportExportModal, {
importToArchive: true,
initialValues: { sourceStorageType: getDefaultFileFormat(get(extensions)).storageType },
}),
});
export function registerFileCommands({
idPrefix,
category,
getCurrentEditor,
folder,
format,
fileExtension,
save = true,
execute = false,
toggleComment = false,
findReplace = false,
undoRedo = false,
}) {
if (save) {
registerCommand({
id: idPrefix + '.save',
group: 'save',
category,
name: 'Save',
// keyText: 'Ctrl+S',
icon: 'icon save',
toolbar: true,
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), false, folder, format, fileExtension),
});
registerCommand({
id: idPrefix + '.saveAs',
group: 'saveAs',
category,
name: 'Save As',
testEnabled: () => getCurrentEditor() != null,
onClick: () => saveTabFile(getCurrentEditor(), true, folder, format, fileExtension),
});
}
if (execute) {
registerCommand({
id: idPrefix + '.execute',
category,
name: 'Execute',
icon: 'icon run',
toolbar: true,
keyText: 'F5 | Ctrl+Enter',
testEnabled: () => getCurrentEditor() != null && !getCurrentEditor()?.isBusy(),
onClick: () => getCurrentEditor().execute(),
});
registerCommand({
id: idPrefix + '.kill',
category,
name: 'Kill',
icon: 'icon close',
toolbar: true,
testEnabled: () => getCurrentEditor() != null && getCurrentEditor()?.canKill(),
onClick: () => getCurrentEditor().kill(),
});
}
if (toggleComment) {
registerCommand({
id: idPrefix + '.toggleComment',
category,
name: 'Toggle comment',
keyText: 'Ctrl+/',
disableHandleKeyText: 'Ctrl+/',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().toggleComment(),
});
}
if (findReplace) {
registerCommand({
id: idPrefix + '.find',
category,
name: 'Find',
keyText: 'Ctrl+F',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().find(),
});
registerCommand({
id: idPrefix + '.replace',
category,
keyText: 'Ctrl+H',
name: 'Replace',
testEnabled: () => getCurrentEditor() != null,
onClick: () => getCurrentEditor().replace(),
});
}
if (undoRedo) {
registerCommand({
id: idPrefix + '.undo',
category,
name: 'Undo',
group: 'undo',
testEnabled: () => getCurrentEditor()?.canUndo(),
onClick: () => getCurrentEditor().undo(),
});
registerCommand({
id: idPrefix + '.redo',
category,
group: 'redo',
name: 'Redo',
testEnabled: () => getCurrentEditor()?.canRedo(),
onClick: () => getCurrentEditor().redo(),
});
}
}

View File

@@ -143,10 +143,10 @@ export default class ChangeSetGrider extends Grider {
this.dispatchChangeSet({ type: 'redo' });
}
get canUndo() {
return this.changeSetState.canUndo;
return this.changeSetState?.canUndo;
}
get canRedo() {
return this.changeSetState.canRedo;
return this.changeSetState?.canRedo;
}
get containsChanges() {
return changeSetContainsChanges(this.changeSet);
@@ -155,10 +155,10 @@ export default class ChangeSetGrider extends Grider {
return this.insertedRows.length > 0;
}
static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
}
static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
}
// static factory({ sourceRows, changeSetState, dispatchChangeSet, display }): ChangeSetGrider {
// return new ChangeSetGrider(sourceRows, changeSetState, dispatchChangeSet, display);
// }
// static factoryDeps({ sourceRows, changeSetState, dispatchChangeSet, display }) {
// return [sourceRows, changeSetState ? changeSetState.value : null, dispatchChangeSet, display];
// }
}

View File

@@ -1,134 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnLabel from './ColumnLabel';
import DropDownButton from '../widgets/DropDownButton';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import { useSplitterDrag } from '../widgets/Splitter';
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject';
import { useSetOpenedTabs } from '../utility/globalState';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import useOpenNewTab from '../utility/useOpenNewTab';
const HeaderDiv = styled.div`
display: flex;
flex-wrap: nowrap;
`;
const LabelDiv = styled.div`
flex: 1;
min-width: 10px;
// padding-left: 2px;
padding: 2px;
margin: auto;
white-space: nowrap;
`;
const IconWrapper = styled.span`
margin-left: 3px;
`;
const ResizeHandle = styled.div`
background-color: ${props => props.theme.border};
width: 2px;
cursor: col-resize;
z-index: 1;
`;
const GroupingLabel = styled.span`
color: green;
white-space: nowrap;
`;
export default function ColumnHeaderControl({
column,
setSort,
onResize,
order,
setGrouping,
grouping,
conid,
database,
}) {
const onResizeDown = useSplitterDrag('clientX', onResize);
const { foreignKey } = column;
const openNewTab = useOpenNewTab();
const theme = useTheme();
const openReferencedTable = () => {
openDatabaseObjectDetail(openNewTab, 'TableDataTab', null, {
schemaName: foreignKey.refSchemaName,
pureName: foreignKey.refTableName,
conid,
database,
objectTypeField: 'tables',
});
// openNewTab(setOpenedTabs, {
// title: foreignKey.refTableName,
// tooltip,
// icon: sqlTemplate ? 'sql.svg' : icons[objectTypeField],
// tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
// props: {
// schemaName,
// pureName,
// conid,
// database,
// objectTypeField,
// initialArgs: sqlTemplate ? { sqlTemplate } : null,
// },
// });
};
return (
<HeaderDiv>
<LabelDiv>
{grouping && (
<GroupingLabel>{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}:</GroupingLabel>
)}
<ColumnLabel {...column} />
{order == 'ASC' && (
<IconWrapper>
<FontIcon icon="img sort-asc" />
</IconWrapper>
)}
{order == 'DESC' && (
<IconWrapper>
<FontIcon icon="img sort-desc" />
</IconWrapper>
)}
</LabelDiv>
{setSort && (
<DropDownButton>
<DropDownMenuItem onClick={() => setSort('ASC')}>Sort ascending</DropDownMenuItem>
<DropDownMenuItem onClick={() => setSort('DESC')}>Sort descending</DropDownMenuItem>
<DropDownMenuDivider />
{foreignKey && (
<DropDownMenuItem onClick={openReferencedTable}>
Open table <strong>{foreignKey.refTableName}</strong>
</DropDownMenuItem>
)}
{foreignKey && <DropDownMenuDivider />}
<DropDownMenuItem onClick={() => setGrouping('GROUP')}>Group by</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MAX')}>MAX</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('MIN')}>MIN</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('SUM')}>SUM</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('AVG')}>AVG</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT')}>COUNT</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('COUNT DISTINCT')}>COUNT DISTINCT</DropDownMenuItem>
{isTypeDateTime(column.dataType) && (
<>
<DropDownMenuDivider />
<DropDownMenuItem onClick={() => setGrouping('GROUP:YEAR')}>Group by YEAR</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:MONTH')}>Group by MONTH</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:DAY')}>Group by DAY</DropDownMenuItem>
{/* <DropDownMenuItem onClick={() => setGrouping('GROUP:HOUR')}>Group by HOUR</DropDownMenuItem>
<DropDownMenuItem onClick={() => setGrouping('GROUP:MINUTE')}>Group by MINUTE</DropDownMenuItem> */}
</>
)}
</DropDownButton>
)}
<ResizeHandle className="resizeHandleControl" onMouseDown={onResizeDown} theme={theme} />
</HeaderDiv>
);
}

View File

@@ -0,0 +1,104 @@
<script>
import FontIcon from '../icons/FontIcon.svelte';
import DropDownButton from '../elements/DropDownButton.svelte';
import splitterDrag from '../utility/splitterDrag';
import ColumnLabel from '../elements/ColumnLabel.svelte';
import { isTypeDateTime } from 'dbgate-tools';
import { openDatabaseObjectDetail } from '../appobj/DatabaseObjectAppObject.svelte';
export let column;
export let conid = undefined;
export let database = undefined;
export let setSort;
export let grouping = undefined;
export let order = undefined;
export let setGrouping;
const openReferencedTable = () => {
openDatabaseObjectDetail('TableDataTab', null, {
schemaName: column.foreignKey.refSchemaName,
pureName: column.foreignKey.refTableName,
conid,
database,
objectTypeField: 'tables',
});
};
function getMenu() {
return [
{ onClick: () => setSort('ASC'), text: 'Sort ascending' },
{ onClick: () => setSort('DESC'), text: 'Sort descending' },
column.foreignKey && [{ divider: true }, { onClick: openReferencedTable, text: column.foreignKey.refTableName }],
{ divider: true },
{ onClick: () => setGrouping('GROUP'), text: 'Group by' },
{ onClick: () => setGrouping('MAX'), text: 'MAX' },
{ onClick: () => setGrouping('MIN'), text: 'MIN' },
{ onClick: () => setGrouping('SUM'), text: 'SUM' },
{ onClick: () => setGrouping('AVG'), text: 'AVG' },
{ onClick: () => setGrouping('COUNT'), text: 'COUNT' },
{ onClick: () => setGrouping('COUNT DISTINCT'), text: 'COUNT DISTINCT' },
isTypeDateTime(column.dataType) && [
{ divider: true },
{ onClick: () => setGrouping('GROUP:YEAR'), text: 'Group by YEAR' },
{ onClick: () => setGrouping('GROUP:MONTH'), text: 'Group by MONTH' },
{ onClick: () => setGrouping('GROUP:DAY'), text: 'Group by DAY' },
],
];
}
</script>
<div class="header">
<div class="label">
{#if grouping}
<span class="grouping">
{grouping == 'COUNT DISTINCT' ? 'distinct' : grouping.toLowerCase()}
</span>
{/if}
<ColumnLabel {...column} />
</div>
{#if order == 'ASC'}
<span class="icon">
<FontIcon icon="img sort-asc" />
</span>
{/if}
{#if order == 'DESC'}
<span class="icon">
<FontIcon icon="img sort-desc" />
</span>
{/if}
<DropDownButton menu={getMenu} />
<div class="horizontal-split-handle resizeHandleControl" use:splitterDrag={'clientX'} on:resizeSplitter />
</div>
<style>
.header {
display: flex;
flex-wrap: nowrap;
}
.label {
flex: 1;
min-width: 10px;
padding: 2px;
margin: auto;
white-space: nowrap;
}
.icon {
margin-left: 3px;
align-self: center;
font-size: 18px;
}
/* .resizer {
background-color: var(--theme-border);
width: 2px;
cursor: col-resize;
z-index: 1;
} */
.grouping {
color: var(--theme-font-alt);
white-space: nowrap;
}
</style>

View File

@@ -1,35 +0,0 @@
//@ts-nocheck
import React from 'react';
import styled from 'styled-components';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
const Label = styled.span`
font-weight: ${props => (props.notNull ? 'bold' : 'normal')};
white-space: nowrap;
`;
const ExtInfoWrap = styled.span`
font-weight: normal;
margin-left: 5px;
color: ${props => props.theme.left_font3};
`;
export function getColumnIcon(column, forceIcon = false) {
if (column.autoIncrement) return 'img autoincrement';
if (column.foreignKey) return 'img foreign-key';
if (forceIcon) return 'img column';
return null;
}
/** @param column {import('dbgate-datalib').DisplayColumn|import('dbgate-types').ColumnInfo} */
export default function ColumnLabel(column) {
const icon = getColumnIcon(column, column.forceIcon);
const theme = useTheme();
return (
<Label {...column}>
{icon ? <FontIcon icon={icon} /> : null} {column.headerText || column.columnName}
{column.extInfo ? <ExtInfoWrap theme={theme}>{column.extInfo}</ExtInfoWrap> : null}
</Label>
);
}

View File

@@ -1,94 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnLabel from './ColumnLabel';
import { filterName } from 'dbgate-datalib';
import { ExpandIcon } from '../icons';
import InlineButton from '../widgets/InlineButton';
import { ManagerInnerContainer } from './ManagerStyles';
import SearchInput from '../widgets/SearchInput';
import useTheme from '../theme/useTheme';
const Wrapper = styled.div``;
const Row = styled.div`
margin-left: 5px;
margin-right: 5px;
cursor: pointer;
white-space: nowrap;
&:hover {
background-color: ${props => props.theme.manager_background_blue[1]};
}
`;
const SearchBoxWrapper = styled.div`
display: flex;
margin-bottom: 5px;
`;
const Button = styled.button`
// -webkit-appearance: none;
// -moz-appearance: none;
// appearance: none;
// width: 50px;
`;
/**
* @param {object} props
* @param {import('dbgate-datalib').GridDisplay} props.display
* @param {import('dbgate-datalib').DisplayColumn} props.column
*/
function ColumnManagerRow(props) {
const { display, column } = props;
const [isHover, setIsHover] = React.useState(false);
const theme = useTheme();
return (
<Row
onMouseEnter={() => setIsHover(true)}
onMouseLeave={() => setIsHover(false)}
theme={theme}
onClick={e => {
// @ts-ignore
if (e.target.closest('.expandColumnIcon')) return;
display.focusColumn(column.uniqueName);
}}
>
<ExpandIcon
className="expandColumnIcon"
isBlank={!column.foreignKey}
isExpanded={column.foreignKey && display.isExpandedColumn(column.uniqueName)}
onClick={() => display.toggleExpandedColumn(column.uniqueName)}
/>
<input
type="checkbox"
style={{ marginLeft: `${5 + (column.uniquePath.length - 1) * 10}px` }}
checked={column.isChecked}
onChange={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
></input>
<ColumnLabel {...column} />
</Row>
);
}
/** @param props {import('./types').DataGridProps} */
export default function ColumnManager(props) {
const { display } = props;
const [columnFilter, setColumnFilter] = React.useState('');
return (
<>
<SearchBoxWrapper>
<SearchInput placeholder="Search columns" filter={columnFilter} setFilter={setColumnFilter} />
<InlineButton onClick={() => display.hideAllColumns()}>Hide</InlineButton>
<InlineButton onClick={() => display.showAllColumns()}>Show</InlineButton>
</SearchBoxWrapper>
<ManagerInnerContainer style={{ maxWidth: props.managerSize }}>
{display
.getColumns(columnFilter)
.filter(column => filterName(columnFilter, column.columnName))
.map(column => (
<ColumnManagerRow key={column.uniqueName} display={display} column={column} />
))}
</ManagerInnerContainer>
</>
);
}

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { filterName, GridDisplay } from 'dbgate-datalib';
import InlineButton from '../elements/InlineButton.svelte';
import ManagerInnerContainer from '../elements/ManagerInnerContainer.svelte';
import SearchBoxWrapper from '../elements/SearchBoxWrapper.svelte';
import SearchInput from '../elements/SearchInput.svelte';
import ColumnManagerRow from './ColumnManagerRow.svelte';
export let managerSize;
export let display: GridDisplay;
let filter;
</script>
<SearchBoxWrapper>
<SearchInput placeholder="Search columns" bind:value={filter} />
<InlineButton on:click={() => display.hideAllColumns()}>Hide</InlineButton>
<InlineButton on:click={() => display.showAllColumns()}>Show</InlineButton>
</SearchBoxWrapper>
<ManagerInnerContainer width={managerSize}>
{#each display
.getColumns(filter)
.filter(column => filterName(filter, column.columnName)) as column (column.uniqueName)}
<ColumnManagerRow {display} {column} />
{/each}
</ManagerInnerContainer>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import { plusExpandIcon } from '../icons/expandIcons';
import FontIcon from '../icons/FontIcon.svelte';
import ColumnLabel from '../elements/ColumnLabel.svelte';
export let column;
export let display;
</script>
<div
class="row"
on:click={e => {
// @ts-ignore
if (e.target.closest('.expandColumnIcon')) return;
display.focusColumn(column.uniqueName);
}}
>
<span class="expandColumnIcon">
<FontIcon
icon={column.foreignKey ? plusExpandIcon(display.isExpandedColumn(column.uniqueName)) : 'icon invisible-box'}
on:click={() => display.toggleExpandedColumn(column.uniqueName)}
/>
</span>
<input
type="checkbox"
style={`margin-left: ${5 + (column.uniquePath.length - 1) * 10}px`}
checked={column.isChecked}
on:change={() => display.setColumnVisibility(column.uniquePath, !column.isChecked)}
/>
<ColumnLabel {...column} />
</div>
<style>
.row {
margin-left: 5px;
margin-right: 5px;
cursor: pointer;
white-space: nowrap;
}
.row:hover {
background: var(--theme-bg-hover);
}
</style>

View File

@@ -1,501 +0,0 @@
// @ts-nocheck
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
import { parseFilter, createMultiLineFilter } from 'dbgate-filterparser';
import InlineButton from '../widgets/InlineButton';
import useShowModal from '../modals/showModal';
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal';
import SetFilterModal from '../modals/SetFilterModal';
import { FontIcon } from '../icons';
import useTheme from '../theme/useTheme';
import { useShowMenu } from '../modals/showMenu';
// import { $ } from '../../Utility/jquery';
// import autobind from 'autobind-decorator';
// import * as React from 'react';
// import { createMultiLineFilter } from '../../DataLib/FilterTools';
// import { ModalDialog } from '../Dialogs';
// import { FilterDialog } from '../Dialogs/FilterDialog';
// import { FilterMultipleValuesDialog } from '../Dialogs/FilterMultipleValuesDialog';
// import { IconSpan } from '../Navigation/NavUtils';
// import { KeyCodes } from '../ReactDataGrid/KeyCodes';
// import { DropDownMenu, DropDownMenuDivider, DropDownMenuItem, DropDownSubmenuItem } from './DropDownMenu';
// import { FilterParserType } from '../../SwaggerClients';
// import { IFilterHolder } from '../CommonControls';
// import { GrayFilterIcon } from '../Icons';
// export interface IDataFilterControlProps {
// filterType: FilterParserType;
// getFilter: Function;
// setFilter: Function;
// width: number;
// onControlKey?: Function;
// isReadOnly?: boolean;
// inputElementId?: string;
// }
const FilterDiv = styled.div`
display: flex;
`;
const FilterInput = styled.input`
flex: 1;
min-width: 10px;
background-color: ${props =>
props.state == 'ok'
? props.theme.input_background_green[1]
: props.state == 'error'
? props.theme.input_background_red[1]
: props.theme.input_background};
`;
// const FilterButton = styled.button`
// color: gray;
// `;
function DropDownContent({ filterType, setFilter, filterMultipleValues, openFilterWindow }) {
switch (filterType) {
case 'number':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<')}>Less Than...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
</>
);
case 'logical':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TRUE')}>Is True</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FALSE')}>Is False</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
</>
);
case 'datetime':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => openFilterWindow('<=')}>Before...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=')}>After...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TODAY')}>Today</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
<DropDownMenuDivider />
{/* <DropDownSubmenuItem title="All dates in period">
<DropDownMenuItem onClick={x => setFilter('JAN')}>January</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FEB')}>February</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('MAR')}>March</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('APR')}>April</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('JUN')}>June</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('JUL')}>July</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('AUG')}>August</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SEP')}>September</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('OCT')}>October</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOV')}>November</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('DEC')}>December</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => setFilter('MON')}>Monday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('TUE')}>Tuesday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('WED')}>Wednesday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('THU')}>Thursday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('FRI')}>Friday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SAT')}>Saturday</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('SUN')}>Sunday</DropDownMenuItem>
</DropDownSubmenuItem> */}
</>
);
case 'string':
return (
<>
<DropDownMenuItem onClick={x => setFilter('')}>Clear Filter</DropDownMenuItem>
<DropDownMenuItem onClick={x => filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('=')}>Equals...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NULL')}>Is Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
<DropDownMenuItem onClick={x => setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
<DropDownMenuDivider />
<DropDownMenuItem onClick={x => openFilterWindow('+')}>Contains...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('^')}>Begins With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('$')}>Ends With...</DropDownMenuItem>
<DropDownMenuItem onClick={x => openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
</>
);
}
}
export default function DataFilterControl({
isReadOnly = false,
filterType,
filter,
setFilter,
focusIndex = 0,
onFocusGrid = undefined,
}) {
const showModal = useShowModal();
const showMenu = useShowMenu();
const theme = useTheme();
const [filterState, setFilterState] = React.useState('empty');
const setFilterText = filter => {
setFilter(filter);
editorRef.current.value = filter || '';
updateFilterState();
};
const applyFilter = () => {
if ((filter || '') == (editorRef.current.value || '')) return;
setFilter(editorRef.current.value);
};
const filterMultipleValues = () => {
showModal(modalState => (
<FilterMultipleValuesModal
modalState={modalState}
onFilter={(mode, text) => setFilterText(createMultiLineFilter(mode, text))}
/>
));
};
const openFilterWindow = operator => {
showModal(modalState => (
<SetFilterModal
filterType={filterType}
modalState={modalState}
onFilter={text => setFilterText(text)}
condition1={operator}
/>
));
};
const buttonRef = React.useRef();
const editorRef = React.useRef();
React.useEffect(() => {
if (focusIndex) editorRef.current.focus();
}, [focusIndex]);
const handleKeyDown = ev => {
if (isReadOnly) return;
if (ev.keyCode == keycodes.enter) {
applyFilter();
}
if (ev.keyCode == keycodes.escape) {
setFilterText('');
}
if (ev.keyCode == keycodes.downArrow) {
if (onFocusGrid) onFocusGrid();
// ev.stopPropagation();
ev.preventDefault();
}
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
};
const updateFilterState = () => {
const value = editorRef.current.value;
try {
if (value) {
parseFilter(value, filterType);
setFilterState('ok');
} else {
setFilterState('empty');
}
} catch (err) {
// console.log('PARSE ERROR', err);
setFilterState('error');
}
};
React.useEffect(() => {
editorRef.current.value = filter || '';
updateFilterState();
}, [filter]);
const handleShowMenu = () => {
const rect = buttonRef.current.getBoundingClientRect();
showMenu(
rect.left,
rect.bottom,
<DropDownContent
filterType={filterType}
setFilter={setFilterText}
filterMultipleValues={filterMultipleValues}
openFilterWindow={openFilterWindow}
/>
);
};
function handlePaste(event) {
var pastedText = undefined;
// @ts-ignore
if (window.clipboardData && window.clipboardData.getData) {
// IE
// @ts-ignore
pastedText = window.clipboardData.getData('Text');
} else if (event.clipboardData && event.clipboardData.getData) {
pastedText = event.clipboardData.getData('text/plain');
}
if (pastedText && pastedText.includes('\n')) {
event.preventDefault();
setFilterText(createMultiLineFilter('is', pastedText));
}
}
return (
<FilterDiv>
<FilterInput
theme={theme}
ref={editorRef}
onKeyDown={handleKeyDown}
type="text"
readOnly={isReadOnly}
onChange={updateFilterState}
state={filterState}
onBlur={applyFilter}
onPaste={handlePaste}
autocomplete="off"
/>
<InlineButton buttonRef={buttonRef} onClick={handleShowMenu} square>
<FontIcon icon="icon filter" />
</InlineButton>
</FilterDiv>
);
}
// domEditor: Element;
// @autobind
// applyFilter() {
// this.props.setFilter($(this.domEditor).val());
// }
// @autobind
// clearFilter() {
// $(this.domEditor).val('');
// this.applyFilter();
// }
// setFilter(value: string) {
// $(this.domEditor).val(value);
// this.applyFilter();
// return false;
// }
// render() {
// let dropDownContent = null;
// let filterIconSpan = <span className='fa fa-filter' style={{color: 'gray', display: 'inline-block', width: '8px', height: '0', whiteSpace: 'nowrap'}} ></span>;
// //filterIconSpan = null;
// if (this.props.filterType == 'Number') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>')}>Greater Than...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>Greater Than Or Equal To...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<')}>Less Than...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Less Than Or Equal To...</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'Logical') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TRUE')}>Is True</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FALSE')}>Is False</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TRUE, NULL')}>Is True or NULL</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FALSE, NULL')}>Is False or NULL</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'DateTime') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.openFilterWindow('<=')}>Before...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=')}>After...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('>=;<=')}>Between...</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('TOMORROW')}>Tomorrow</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TODAY')}>Today</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('YESTERDAY')}>Yesterday</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT WEEK')}>Next Week</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS WEEK')}>This Week</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST WEEK')}>Last Week</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT MONTH')}>Next Month</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS MONTH')}>This Month</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST MONTH')}>Last Month</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('NEXT YEAR')}>Next Year</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THIS YEAR')}>This Year</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('LAST YEAR')}>Last Year</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownSubmenuItem title='All dates in period'>
// <DropDownMenuItem onClick={x => this.setFilter('JAN')}>January</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FEB')}>February</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('MAR')}>March</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('APR')}>April</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('JUN')}>June</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('JUL')}>July</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('AUG')}>August</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SEP')}>September</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('OCT')}>October</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOV')}>November</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('DEC')}>December</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.setFilter('MON')}>Monday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('TUE')}>Tuesday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('WED')}>Wednesday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('THU')}>Thursday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('FRI')}>Friday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SAT')}>Saturday</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('SUN')}>Sunday</DropDownMenuItem>
// </DropDownSubmenuItem>
// </DropDownMenu>;
// }
// if (this.props.filterType == 'String') {
// dropDownContent = <DropDownMenu iconSpan={filterIconSpan}>
// <DropDownMenuItem onClick={x => this.setFilter('')}>Clear Filter</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.filterMultipleValues()}>Filter multiple values</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('=')}>Equals...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('<>')}>Does Not Equal...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NULL')}>Is Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT NULL')}>Is Not Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('EMPTY, NULL')}>Is Empty Or Null</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.setFilter('NOT EMPTY NOT NULL')}>Has Not Empty Value</DropDownMenuItem>
// <DropDownMenuDivider />
// <DropDownMenuItem onClick={x => this.openFilterWindow('+')}>Contains...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('~')}>Does Not Contain...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('^')}>Begins With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('!^')}>Does Not Begin With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('$')}>Ends With...</DropDownMenuItem>
// <DropDownMenuItem onClick={x => this.openFilterWindow('!$')}>Does Not End With...</DropDownMenuItem>
// </DropDownMenu>;
// }
// if (this.props.isReadOnly) {
// dropDownContent = <GrayFilterIcon style={{marginLeft: '5px'}} />;
// }
// return <div style={{ minWidth: `${this.props.width}px`, maxWidth: `${this.props.width}px`, width: `${this.props.width}` }}>
// <input id={this.props.inputElementId} type='text' style={{ 'width': `${(this.props.width - 20)}px` }} readOnly={this.props.isReadOnly}
// onBlur={this.applyFilter} ref={x => this.setDomEditor(x)} onKeyDown={this.editorKeyDown} placeholder='Search' ></input>
// {dropDownContent}
// </div>;
// }
// async filterMultipleValues() {
// let result = await ModalDialog.run(<FilterMultipleValuesDialog header='Filter multiple values' />);
// if (!result) return;
// let { mode, text } = result;
// let filter = createMultiLineFilter(mode, text);
// this.setFilter(filter);
// }
// openFilterWindow(selectedOperator: string) {
// FilterDialog.runFilter(this, this.props.filterType, selectedOperator);
// return false;
// }
// setDomEditor(editor) {
// this.domEditor = editor;
// $(editor).val(this.props.getFilter());
// }
// @autobind
// editorKeyDown(ev) {
// if (this.props.isReadOnly) return;
// if (ev.keyCode == KeyCodes.Enter) {
// this.applyFilter();
// }
// if (ev.keyCode == KeyCodes.Escape) {
// this.clearFilter();
// }
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
// }
// focus() {
// $(this.domEditor).focus();
// }
// }

View File

@@ -0,0 +1,218 @@
<script context="module">
</script>
<script>
import { createMultiLineFilter, parseFilter } from 'dbgate-filterparser';
import splitterDrag from '../utility/splitterDrag';
import FilterMultipleValuesModal from '../modals/FilterMultipleValuesModal.svelte';
import { showModal } from '../modals/modalTools';
import SetFilterModal from '../modals/SetFilterModal.svelte';
import keycodes from '../utility/keycodes';
import DropDownButton from '../elements/DropDownButton.svelte';
export let isReadOnly = false;
export let filterType;
export let filter;
export let setFilter;
export let showResizeSplitter = false;
let value;
let isError;
let isOk;
function openFilterWindow(condition1) {
showModal(SetFilterModal, { condition1, filterType, onFilter: setFilter });
}
const filterMultipleValues = () => {
showModal(FilterMultipleValuesModal, {
onFilter: (mode, text) => setFilter(createMultiLineFilter(mode, text)),
});
};
function createMenu() {
switch (filterType) {
case 'number':
return [
{ onClick: () => setFilter(''), text: 'Clear Filter' },
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
{ onClick: () => openFilterWindow('='), text: 'Equals...' },
{ onClick: () => openFilterWindow('['), text: 'Does Not Equal...' },
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
{ onClick: () => openFilterWindow('>'), text: 'Greater Than...' },
{ onClick: () => openFilterWindow('>='), text: 'Greater Than Or Equal To...' },
{ onClick: () => openFilterWindow('<'), text: 'Less Than...' },
{ onClick: () => openFilterWindow('<='), text: 'Less Than Or Equal To...' },
];
case 'logical':
return [
{ onClick: () => setFilter(''), text: 'Clear Filter' },
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
{ onClick: () => setFilter('TRUE'), text: 'Is True' },
{ onClick: () => setFilter('FALSE'), text: 'Is False' },
{ onClick: () => setFilter('TRUE, NULL'), text: 'Is True or NULL' },
{ onClick: () => setFilter('FALSE, NULL'), text: 'Is False or NULL' },
];
case 'datetime':
return [
{ onClick: () => setFilter(''), text: 'Clear Filter' },
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
{ divider: true },
{ onClick: () => openFilterWindow('<='), text: 'Before...' },
{ onClick: () => openFilterWindow('>='), text: 'After...' },
{ onClick: () => openFilterWindow('>=;<='), text: 'Between...' },
{ divider: true },
{ onClick: () => setFilter('TOMORROW'), text: 'Tomorrow' },
{ onClick: () => setFilter('TODAY'), text: 'Today' },
{ onClick: () => setFilter('YESTERDAY'), text: 'Yesterday' },
{ divider: true },
{ onClick: () => setFilter('NEXT WEEK'), text: 'Next Week' },
{ onClick: () => setFilter('THIS WEEK'), text: 'This Week' },
{ onClick: () => setFilter('LAST WEEK'), text: 'Last Week' },
{ divider: true },
{ onClick: () => setFilter('NEXT MONTH'), text: 'Next Month' },
{ onClick: () => setFilter('THIS MONTH'), text: 'This Month' },
{ onClick: () => setFilter('LAST MONTH'), text: 'Last Month' },
{ divider: true },
{ onClick: () => setFilter('NEXT YEAR'), text: 'Next Year' },
{ onClick: () => setFilter('THIS YEAR'), text: 'This Year' },
{ onClick: () => setFilter('LAST YEAR'), text: 'Last Year' },
{ divider: true },
];
case 'string':
return [
{ onClick: () => setFilter(''), text: 'Clear Filter' },
{ onClick: () => filterMultipleValues(), text: 'Filter multiple values' },
{ onClick: () => openFilterWindow('='), text: 'Equals...' },
{ onClick: () => openFilterWindow('['), text: 'Does Not Equal...' },
{ onClick: () => setFilter('NULL'), text: 'Is Null' },
{ onClick: () => setFilter('NOT NULL'), text: 'Is Not Null' },
{ onClick: () => setFilter('EMPTY, NULL'), text: 'Is Empty Or Null' },
{ onClick: () => setFilter('NOT EMPTY NOT NULL'), text: 'Has Not Empty Value' },
{ divider: true },
{ onClick: () => openFilterWindow('+'), text: 'Contains...' },
{ onClick: () => openFilterWindow('~'), text: 'Does Not Contain...' },
{ onClick: () => openFilterWindow('^'), text: 'Begins With...' },
{ onClick: () => openFilterWindow('!^'), text: 'Does Not Begin With...' },
{ onClick: () => openFilterWindow('$'), text: 'Ends With...' },
{ onClick: () => openFilterWindow('!$'), text: 'Does Not End With...' },
];
}
// return [
// { text: 'Clear filter', onClick: () => (value = '') },
// { text: 'Is Null', onClick: () => (value = 'NULL') },
// { text: 'Is Not Null', onClick: () => (value = 'NOT NULL') },
// ];
}
const handleKeyDown = ev => {
if (isReadOnly) return;
if (ev.keyCode == keycodes.enter) {
applyFilter();
}
if (ev.keyCode == keycodes.escape) {
setFilter('');
}
// if (ev.keyCode == keycodes.downArrow) {
// if (onFocusGrid) onFocusGrid();
// // ev.stopPropagation();
// ev.preventDefault();
// }
// if (ev.keyCode == KeyCodes.DownArrow || ev.keyCode == KeyCodes.UpArrow) {
// if (this.props.onControlKey) this.props.onControlKey(ev.keyCode);
// }
};
function handlePaste(event) {
var pastedText = undefined;
// @ts-ignore
if (window.clipboardData && window.clipboardData.getData) {
// IE
// @ts-ignore
pastedText = window.clipboardData.getData('Text');
} else if (event.clipboardData && event.clipboardData.getData) {
pastedText = event.clipboardData.getData('text/plain');
}
if (pastedText && pastedText.includes('\n')) {
event.preventDefault();
setFilter(createMultiLineFilter('is', pastedText));
}
}
$: value = filter;
$: {
try {
isOk = false;
isError = false;
if (value) {
parseFilter(value, filterType);
isOk = true;
}
} catch (err) {
isError = true;
}
}
function applyFilter() {
setFilter(value);
}
// $: if (value != filter) setFilter(value);
</script>
<div class="flex">
<input
type="text"
autocomplete="off"
readOnly={isReadOnly}
bind:value
on:keydown={handleKeyDown}
on:blur={applyFilter}
on:paste={handlePaste}
class:isError
class:isOk
/>
<DropDownButton icon="icon filter" menu={createMenu} />
{#if showResizeSplitter}
<div class="horizontal-split-handle resizeHandleControl" use:splitterDrag={'clientX'} on:resizeSplitter />
{/if}
</div>
<style>
input {
flex: 1;
min-width: 10px;
}
input.isError {
background-color: var(--theme-bg-red);
}
input.isOk {
background-color: var(--theme-bg-green);
}
</style>

View File

@@ -1,83 +0,0 @@
import React from 'react';
import styled from 'styled-components';
import ColumnManager from './ColumnManager';
import FormViewFilters from '../formview/FormViewFilters';
import ReferenceManager from './ReferenceManager';
import { HorizontalSplitter } from '../widgets/Splitter';
import WidgetColumnBar, { WidgetColumnBarItem } from '../widgets/WidgetColumnBar';
import CellDataView from '../celldata/CellDataView';
import useTheme from '../theme/useTheme';
const LeftContainer = styled.div`
background-color: ${props => props.theme.manager_background};
display: flex;
flex: 1;
`;
const DataGridContainer = styled.div`
position: relative;
flex-grow: 1;
`;
export default function DataGrid(props) {
const { GridCore, FormView, formDisplay } = props;
const theme = useTheme();
const [managerSize, setManagerSize] = React.useState(0);
const [selection, setSelection] = React.useState([]);
const [formSelection, setFormSelection] = React.useState(null);
const [grider, setGrider] = React.useState(null);
const [collapsedWidgets, setCollapsedWidgets] = React.useState([]);
// const [formViewData, setFormViewData] = React.useState(null);
const isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
return (
<HorizontalSplitter initialValue="300px" size={managerSize} setSize={setManagerSize}>
<LeftContainer theme={theme}>
<WidgetColumnBar onChangeCollapsedWidgets={setCollapsedWidgets}>
{!isFormView && (
<WidgetColumnBarItem title="Columns" name="columns" height={props.showReferences ? '40%' : '60%'}>
<ColumnManager {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
{isFormView && (
<WidgetColumnBarItem title="Filters" name="filters" height="30%">
<FormViewFilters {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
{props.showReferences && props.display.hasReferences && (
<WidgetColumnBarItem title="References" name="references" height="30%" collapsed={props.isDetailView}>
<ReferenceManager {...props} managerSize={managerSize} />
</WidgetColumnBarItem>
)}
<WidgetColumnBarItem
title="Cell data"
name="cellData"
// cell data must be collapsed by default, because of performance reasons
// when not collapsed, onSelectionChanged of grid is set and RERENDER of this component is done on every selection change
collapsed
>
{isFormView ? (
<CellDataView selectedValue={formSelection} />
) : (
<CellDataView selection={selection} grider={grider} />
)}
</WidgetColumnBarItem>
</WidgetColumnBar>
</LeftContainer>
<DataGridContainer>
{isFormView ? (
<FormView {...props} onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setFormSelection} />
) : (
<GridCore
{...props}
onSelectionChanged={collapsedWidgets.includes('cellData') ? null : setSelection}
onChangeGrider={setGrider}
formViewAvailable={!!FormView && !!formDisplay}
/>
)}
</DataGridContainer>
</HorizontalSplitter>
);
}

View File

@@ -0,0 +1,63 @@
<script lang="ts">
import HorizontalSplitter from '../elements/HorizontalSplitter.svelte';
import FormViewFilters from '../formview/FormViewFilters.svelte';
import WidgetColumnBar from '../widgets/WidgetColumnBar.svelte';
import WidgetColumnBarItem from '../widgets/WidgetColumnBarItem.svelte';
import ColumnManager from './ColumnManager.svelte';
import ReferenceManager from './ReferenceManager.svelte';
export let config;
export let gridCoreComponent;
export let formViewComponent;
export let formDisplay;
export let isDetailView = false;
export let showReferences = false;
let managerSize;
$: isFormView = !!(formDisplay && formDisplay.config && formDisplay.config.isFormView);
</script>
<HorizontalSplitter initialValue="300px" bind:size={managerSize}>
<div class="left" slot="1">
<WidgetColumnBar>
<WidgetColumnBarItem title="Columns" name="columns" height={showReferences ? '40%' : '60%'} skip={isFormView}>
<ColumnManager {...$$props} {managerSize} />
</WidgetColumnBarItem>
<WidgetColumnBarItem title="Filters" name="filters" height="30%" skip={!isFormView}>
<FormViewFilters {...$$props} {managerSize} />
</WidgetColumnBarItem>
<WidgetColumnBarItem
title="References"
name="references"
height="30%"
collapsed={isDetailView}
skip={!showReferences}
>
<ReferenceManager {...$$props} {managerSize} />
</WidgetColumnBarItem>
</WidgetColumnBar>
</div>
<svelte:fragment slot="2">
{#if isFormView}
<svelte:component this={formViewComponent} {...$$props} />
{:else}
<svelte:component
this={gridCoreComponent}
{...$$props}
formViewAvailable={!!formViewComponent && !!formDisplay}
/>
{/if}
</svelte:fragment>
</HorizontalSplitter>
<style>
.left {
display: flex;
flex: 1;
background-color: var(--theme-bg-0);
}
</style>

View File

@@ -0,0 +1,153 @@
<script context="module">
function makeBulletString(value) {
return _.pad('', value.length, '•');
}
function highlightSpecialCharacters(value) {
value = value.replace(/\n/g, '↲');
value = value.replace(/\r/g, '');
value = value.replace(/^(\s+)/, makeBulletString);
value = value.replace(/(\s+)$/, makeBulletString);
value = value.replace(/(\s\s+)/g, makeBulletString);
return value;
}
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
</script>
<script lang="ts">
import moment from 'moment';
import _ from 'lodash';
import { isTypeLogical } from 'dbgate-tools';
import ShowFormButton from '../formview/ShowFormButton.svelte';
export let rowIndex;
export let col;
export let rowData;
export let colIndex = undefined;
export let hintFieldsAllowed = undefined;
export let isSelected = false;
export let isFrameSelected = false;
export let isModifiedRow = false;
export let isModifiedCell = false;
export let isInserted = false;
export let isDeleted = false;
export let isAutofillSelected = false;
export let isFocusedColumn = false;
export let domCell = undefined;
export let hideContent = false;
export let onSetFormView;
$: value = (rowData || {})[col.uniqueName];
</script>
<td
bind:this={domCell}
data-row={rowIndex}
data-col={colIndex == null ? col.colIndex : colIndex}
class:isSelected
class:isFrameSelected
class:isModifiedRow
class:isModifiedCell
class:isInserted
class:isDeleted
class:isAutofillSelected
class:isFocusedColumn
style={`width:${col.width}px; min-width:${col.width}px; max-width:${col.width}px`}
>
{#if hideContent}
<slot />
{:else}
{#if value == null}
<span class="null">(NULL)</span>
{:else if _.isDate(value)}
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
{:else if value === true}
1
{:else if value === false}
0
{:else if _.isNumber(value)}
{#if value >= 10000 || value <= -10000}
{value.toLocaleString()}
{:else}
{value.toString()}
{/if}
{:else if _.isString(value)}
{#if dateTimeRegex.test(value)}
{moment(value).format('YYYY-MM-DD HH:mm:ss')}
{:else}
{highlightSpecialCharacters(value)}
{/if}
{:else if _.isPlainObject(value)}
{#if _.isArray(value.data)}
{#if value.data.length == 1 && isTypeLogical(col.dataType)}
{value.data[0]}
{:else}
<span class="null">({value.data.length} bytes)</span>
{/if}
{:else}
<span class="null">(RAW)</span>
{/if}
{:else}
{value.toString()}
{/if}
{#if hintFieldsAllowed && hintFieldsAllowed.includes(col.uniqueName) && rowData}
<span class="hint">{rowData[col.hintColumnName]}</span>
{/if}
{#if col.foreignKey && rowData[col.uniqueName]}
<ShowFormButton on:click={() => onSetFormView(rowData, col)} />
{/if}
{/if}
</td>
<style>
td {
font-weight: normal;
border: 1px solid var(--theme-border);
padding: 2px;
white-space: nowrap;
position: relative;
overflow: hidden;
}
td.isFrameSelected {
outline: 3px solid var(--theme-bg-selected);
outline-offset: -3px;
}
td.isAutofillSelected {
outline: 3px solid var(--theme-bg-selected);
outline-offset: -3px;
}
td.isFocusedColumn {
background: var(--theme-bg-alt);
}
td.isModifiedRow {
background: var(--theme-bg-gold);
}
td.isModifiedCell {
background: var(--theme-bg-orange);
}
td.isInserted {
background: var(--theme-bg-green);
}
td.isDeleted {
background: var(--theme-bg-volcano);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
background-repeat: repeat-x;
background-position: 50% 50%;
}
td.isSelected {
background: var(--theme-bg-selected);
}
.hint {
color: var(--theme-font-3);
margin-left: 5px;
}
.null {
color: var(--theme-font-3);
font-style: italic;
}
</style>

View File

@@ -1,68 +0,0 @@
import React from 'react';
import { DropDownMenuItem, DropDownMenuDivider } from '../modals/DropDownMenu';
export default function DataGridContextMenu({
copy,
revertRowChanges,
deleteSelectedRows,
insertNewRow,
setNull,
reload,
exportGrid,
filterSelectedValue,
openQuery,
openFreeTable,
openChartSelection,
openActiveChart,
switchToForm,
}) {
return (
<>
{!!reload && (
<DropDownMenuItem onClick={reload} keyText="F5">
Reload
</DropDownMenuItem>
)}
{!!reload && <DropDownMenuDivider />}
<DropDownMenuItem onClick={copy} keyText="Ctrl+C">
Copy
</DropDownMenuItem>
{revertRowChanges && (
<DropDownMenuItem onClick={revertRowChanges} keyText="Ctrl+R">
Revert row changes
</DropDownMenuItem>
)}
{deleteSelectedRows && (
<DropDownMenuItem onClick={deleteSelectedRows} keyText="Ctrl+Delete">
Delete selected rows
</DropDownMenuItem>
)}
{insertNewRow && (
<DropDownMenuItem onClick={insertNewRow} keyText="Insert">
Insert new row
</DropDownMenuItem>
)}
<DropDownMenuDivider />
{setNull && (
<DropDownMenuItem onClick={setNull} keyText="Ctrl+0">
Set NULL
</DropDownMenuItem>
)}
{exportGrid && <DropDownMenuItem onClick={exportGrid}>Export</DropDownMenuItem>}
{filterSelectedValue && (
<DropDownMenuItem onClick={filterSelectedValue} keyText="Ctrl+F">
Filter selected value
</DropDownMenuItem>
)}
{openQuery && <DropDownMenuItem onClick={openQuery}>Open query</DropDownMenuItem>}
<DropDownMenuItem onClick={openFreeTable}>Open selection in free table editor</DropDownMenuItem>
<DropDownMenuItem onClick={openChartSelection}>Open chart from selection</DropDownMenuItem>
{openActiveChart && <DropDownMenuItem onClick={openActiveChart}>Open active chart</DropDownMenuItem>}
{!!switchToForm && (
<DropDownMenuItem onClick={switchToForm} keyText="F4">
Form view
</DropDownMenuItem>
)}
</>
);
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,333 +0,0 @@
// @ts-nocheck
import moment from 'moment';
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import InplaceEditor from './InplaceEditor';
import { cellIsSelected } from './gridutil';
import { isTypeLogical } from 'dbgate-tools';
import useTheme from '../theme/useTheme';
import { FontIcon } from '../icons';
const TableBodyCell = styled.td`
font-weight: normal;
border: 1px solid ${props => props.theme.border};
// border-collapse: collapse;
padding: 2px;
white-space: nowrap;
position: relative;
overflow: hidden;
${props =>
props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
`
background: initial;
background-color: ${props.theme.gridbody_selection[4]};
color: ${props.theme.gridbody_invfont1};`}
${props =>
props.isFrameSelected &&
`
outline: 3px solid ${props.theme.gridbody_selection[4]};
outline-offset: -3px;`}
${props =>
props.isAutofillSelected &&
!props.isFocusedColumn &&
`
outline: 3px solid ${props.theme.gridbody_selection[4]};
outline-offset: -3px;`}
${props =>
props.isModifiedRow &&
!props.isInsertedRow &&
!props.isSelected &&
!props.isAutofillSelected &&
!props.isModifiedCell &&
!props.isFocusedColumn &&
`
background-color: ${props.theme.gridbody_background_gold[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isInsertedRow &&
!props.isFocusedColumn &&
props.isModifiedCell &&
`
background-color: ${props.theme.gridbody_background_orange[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
props.isInsertedRow &&
`
background-color: ${props.theme.gridbody_background_green[1]};`}
${props =>
!props.isSelected &&
!props.isAutofillSelected &&
!props.isFocusedColumn &&
props.isDeletedRow &&
`
background-color: ${props.theme.gridbody_background_volcano[1]};
`}
${props =>
props.isFocusedColumn &&
`
background-color: ${props.theme.gridbody_background_yellow[0]};
`}
${props =>
props.isDeletedRow &&
`
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAEElEQVQImWNgIAX8x4KJBAD+agT8INXz9wAAAABJRU5ErkJggg==');
// from http://www.patternify.com/
background-repeat: repeat-x;
background-position: 50% 50%;`}
`;
const HintSpan = styled.span`
color: ${props => props.theme.gridbody_font3};
margin-left: 5px;
`;
const NullSpan = styled.span`
color: ${props => props.theme.gridbody_font3};
font-style: italic;
`;
const TableBodyRow = styled.tr`
// height: 35px;
background-color: ${props => props.theme.gridbody_background};
&:nth-child(6n + 3) {
background-color: ${props => props.theme.gridbody_background_alt2};
}
&:nth-child(6n + 6) {
background-color: ${props => props.theme.gridbody_background_alt3};
}
`;
const TableHeaderCell = styled.td`
border: 1px solid ${props => props.theme.border};
text-align: left;
padding: 2px;
background-color: ${props => props.theme.gridheader_background};
overflow: hidden;
position: relative;
`;
const AutoFillPoint = styled.div`
width: 8px;
height: 8px;
background-color: ${props => props.theme.gridbody_selection[6]};
position: absolute;
right: 0px;
bottom: 0px;
overflow: visible;
cursor: crosshair;
`;
export const ShowFormButton = styled.div`
position: absolute;
right: 0px;
top: 1px;
color: ${props => props.theme.gridbody_font3};
background-color: ${props => props.theme.gridheader_background};
border: 1px solid ${props => props.theme.gridheader_background};
&:hover {
color: ${props => props.theme.gridheader_font_hover};
border: 1px solid ${props => props.theme.border};
top: 1px;
right: 0px;
}
`;
function makeBulletString(value) {
return _.pad('', value.length, '•');
}
function highlightSpecialCharacters(value) {
value = value.replace(/\n/g, '↲');
value = value.replace(/\r/g, '');
value = value.replace(/^(\s+)/, makeBulletString);
value = value.replace(/(\s+)$/, makeBulletString);
value = value.replace(/(\s\s+)/g, makeBulletString);
return value;
}
const dateTimeRegex = /^\d\d\d\d-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d\d\d)?Z?$/;
export function CellFormattedValue({ value, dataType, theme }) {
if (value == null) return <NullSpan theme={theme}>(NULL)</NullSpan>;
if (_.isDate(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
if (value === true) return '1';
if (value === false) return '0';
if (_.isNumber(value)) {
if (value >= 10000 || value <= -10000) {
return value.toLocaleString();
}
return value.toString();
}
if (_.isString(value)) {
if (dateTimeRegex.test(value)) return moment(value).format('YYYY-MM-DD HH:mm:ss');
return highlightSpecialCharacters(value);
}
if (_.isPlainObject(value)) {
if (_.isArray(value.data)) {
if (value.data.length == 1 && isTypeLogical(dataType)) return value.data[0];
return <NullSpan theme={theme}>({value.data.length} bytes)</NullSpan>;
}
return <NullSpan theme={theme}>(RAW)</NullSpan>;
}
return value.toString();
}
function RowHeaderCell({ rowIndex, theme, onSetFormView, rowData }) {
const [mouseIn, setMouseIn] = React.useState(false);
return (
<TableHeaderCell
data-row={rowIndex}
data-col="header"
theme={theme}
onMouseEnter={onSetFormView ? () => setMouseIn(true) : null}
onMouseLeave={onSetFormView ? () => setMouseIn(false) : null}
>
{rowIndex + 1}
{!!onSetFormView && mouseIn && (
<ShowFormButton
theme={theme}
onClick={e => {
e.stopPropagation();
onSetFormView(rowData);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</TableHeaderCell>
);
}
/** @param props {import('./types').DataGridProps} */
function DataGridRow(props) {
const {
rowHeight,
rowIndex,
visibleRealColumns,
inplaceEditorState,
dispatchInsplaceEditor,
autofillMarkerCell,
selectedCells,
autofillSelectedCells,
focusedColumn,
grider,
frameSelection,
onSetFormView,
} = props;
// usePropsCompare({
// rowHeight,
// rowIndex,
// visibleRealColumns,
// inplaceEditorState,
// dispatchInsplaceEditor,
// row,
// display,
// changeSet,
// setChangeSet,
// insertedRowIndex,
// autofillMarkerCell,
// selectedCells,
// autofillSelectedCells,
// });
// console.log('RENDER ROW', rowIndex);
const theme = useTheme();
const rowData = grider.getRowData(rowIndex);
const rowStatus = grider.getRowStatus(rowIndex);
const hintFieldsAllowed = visibleRealColumns
.filter(col => {
if (!col.hintColumnName) return false;
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
return true;
})
.map(col => col.uniqueName);
if (!rowData) return null;
return (
<TableBodyRow style={{ height: `${rowHeight}px` }} theme={theme}>
<RowHeaderCell rowIndex={rowIndex} theme={theme} onSetFormView={onSetFormView} rowData={rowData} />
{visibleRealColumns.map(col => (
<TableBodyCell
key={col.uniqueName}
theme={theme}
style={{
width: col.widthPx,
minWidth: col.widthPx,
maxWidth: col.widthPx,
}}
data-row={rowIndex}
data-col={col.colIndex}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
isModifiedRow={rowStatus.status == 'updated'}
isFocusedColumn={col.uniqueName == focusedColumn}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
isInsertedRow={
rowStatus.status == 'inserted' || (rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))
}
isDeletedRow={
rowStatus.status == 'deleted' || (rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))
}
>
{inplaceEditorState.cell &&
rowIndex == inplaceEditorState.cell[0] &&
col.colIndex == inplaceEditorState.cell[1] ? (
<InplaceEditor
widthPx={col.widthPx}
inplaceEditorState={inplaceEditorState}
dispatchInsplaceEditor={dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
// grider={grider}
// rowIndex={rowIndex}
// uniqueName={col.uniqueName}
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
/>
) : (
<>
<CellFormattedValue value={rowData[col.uniqueName]} dataType={col.dataType} theme={theme} />
{hintFieldsAllowed.includes(col.uniqueName) && (
<HintSpan theme={theme}>{rowData[col.hintColumnName]}</HintSpan>
)}
{col.foreignKey && rowData[col.uniqueName] && (
<ShowFormButton
theme={theme}
className="buttonLike"
onClick={e => {
e.stopPropagation();
onSetFormView(rowData, col);
}}
>
<FontIcon icon="icon form" />
</ShowFormButton>
)}
</>
)}
{autofillMarkerCell && autofillMarkerCell[1] == col.colIndex && autofillMarkerCell[0] == rowIndex && (
<AutoFillPoint className="autofillHandleMarker" theme={theme}></AutoFillPoint>
)}
</TableBodyCell>
))}
</TableBodyRow>
);
}
export default React.memo(DataGridRow);

View File

@@ -0,0 +1,80 @@
<script lang="ts">
import openReferenceForm from '../formview/openReferenceForm';
import DataGridCell from './DataGridCell.svelte';
import { cellIsSelected } from './gridutil';
import InplaceEditor from './InplaceEditor.svelte';
import RowHeaderCell from './RowHeaderCell.svelte';
export let rowHeight;
export let rowIndex;
export let visibleRealColumns: any[];
export let grider;
export let frameSelection = undefined;
export let selectedCells = undefined;
export let autofillSelectedCells = undefined;
export let autofillMarkerCell = undefined;
export let focusedColumn = undefined;
export let inplaceEditorState;
export let dispatchInsplaceEditor;
export let onSetFormView;
$: rowData = grider.getRowData(rowIndex);
$: rowStatus = grider.getRowStatus(rowIndex);
$: hintFieldsAllowed = visibleRealColumns
.filter(col => {
if (!col.hintColumnName) return false;
if (rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)) return false;
return true;
})
.map(col => col.uniqueName);
</script>
<tr style={`height: ${rowHeight}px`}>
<RowHeaderCell {rowIndex} onShowForm={onSetFormView ? () => onSetFormView(rowData, null) : null} />
{#each visibleRealColumns as col (col.uniqueName)}
{#if inplaceEditorState.cell && rowIndex == inplaceEditorState.cell[0] && col.colIndex == inplaceEditorState.cell[1]}
<td>
<InplaceEditor
width={col.width}
{inplaceEditorState}
{dispatchInsplaceEditor}
cellValue={rowData[col.uniqueName]}
onSetValue={value => grider.setCellValue(rowIndex, col.uniqueName, value)}
/>
</td>
{:else}
<DataGridCell
{rowIndex}
{rowData}
{col}
{hintFieldsAllowed}
isSelected={frameSelection ? false : cellIsSelected(rowIndex, col.colIndex, selectedCells)}
isFrameSelected={frameSelection ? cellIsSelected(rowIndex, col.colIndex, selectedCells) : false}
isAutofillSelected={cellIsSelected(rowIndex, col.colIndex, autofillSelectedCells)}
isFocusedColumn={col.uniqueName == focusedColumn}
isModifiedCell={rowStatus.modifiedFields && rowStatus.modifiedFields.has(col.uniqueName)}
isModifiedRow={rowStatus.status == 'updated'}
isInserted={rowStatus.status == 'inserted' ||
(rowStatus.insertedFields && rowStatus.insertedFields.has(col.uniqueName))}
isDeleted={rowStatus.status == 'deleted' ||
(rowStatus.deletedFields && rowStatus.deletedFields.has(col.uniqueName))}
{onSetFormView}
/>
{/if}
{/each}
</tr>
<style>
tr {
background-color: var(--theme-bg-0);
}
tr:nth-child(6n + 3) {
background-color: var(--theme-bg-1);
}
tr:nth-child(6n + 6) {
background-color: var(--theme-bg-alt);
}
</style>

View File

@@ -1,32 +0,0 @@
import React from 'react';
import ToolbarButton from '../widgets/ToolbarButton';
export default function DataGridToolbar({ reload, reconnect, grider, save, switchToForm }) {
return (
<>
{switchToForm && (
<ToolbarButton onClick={switchToForm} icon="icon form">
Form view
</ToolbarButton>
)}
<ToolbarButton onClick={reload} icon="icon reload">
Refresh
</ToolbarButton>
<ToolbarButton onClick={reconnect} icon="icon connection">
Reconnect
</ToolbarButton>
<ToolbarButton disabled={!grider.canUndo} onClick={() => grider.undo()} icon="icon undo">
Undo
</ToolbarButton>
<ToolbarButton disabled={!grider.canRedo} onClick={() => grider.redo()} icon="icon redo">
Redo
</ToolbarButton>
<ToolbarButton disabled={!grider.allowSave} onClick={save} icon="icon save">
Save
</ToolbarButton>
<ToolbarButton disabled={!grider.containsChanges} onClick={() => grider.revertAllChanges()} icon="icon close">
Revert
</ToolbarButton>
</>
);
}

View File

@@ -0,0 +1,41 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
export let viewportRatio = 0.5;
export let minimum;
export let maximum;
const dispatch = createEventDispatcher();
let width;
let node;
$: contentSize = Math.round(width / viewportRatio);
function handleScroll() {
const position = node.scrollLeft;
const ratio = position / (contentSize - width);
if (ratio < 0) return 0;
const res = ratio * (maximum - minimum + 1) + minimum;
dispatch('scroll', Math.floor(res + 0.3));
}
export function scroll(value) {
const position01 = (value - minimum) / (maximum - minimum + 1);
const position = position01 * (contentSize - width);
if (node) node.scrollLeft = Math.floor(position);
}
</script>
<div bind:clientWidth={width} bind:this={node} on:scroll={handleScroll} class="main">
<div style={`width: ${contentSize}px`}>&nbsp;</div>
</div>
<style>
.main {
overflow-x: scroll;
height: 16px;
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
</style>

View File

@@ -1,98 +0,0 @@
// @ts-nocheck
import _ from 'lodash';
import React from 'react';
import styled from 'styled-components';
import keycodes from '../utility/keycodes';
const StyledInput = styled.input`
border: 0px solid;
outline: none;
margin: 0px;
padding: 0px;
`;
export default function InplaceEditor({
widthPx,
// rowIndex,
// uniqueName,
// grider,
cellValue,
inplaceEditorState,
dispatchInsplaceEditor,
onSetValue,
}) {
const editorRef = React.useRef();
const widthRef = React.useRef(widthPx);
const isChangedRef = React.useRef(!!inplaceEditorState.text);
React.useEffect(() => {
const editor = editorRef.current;
editor.value = inplaceEditorState.text || cellValue;
editor.focus();
if (inplaceEditorState.selectAll) {
editor.select();
}
}, []);
function handleBlur() {
if (isChangedRef.current) {
const editor = editorRef.current;
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
dispatchInsplaceEditor({ type: 'close' });
}
if (inplaceEditorState.shouldSave) {
const editor = editorRef.current;
if (isChangedRef.current) {
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
editor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
function handleKeyDown(event) {
const editor = editorRef.current;
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.current = false;
dispatchInsplaceEditor({ type: 'close' });
break;
case keycodes.enter:
if (isChangedRef.current) {
// grider.setCellValue(rowIndex, uniqueName, editor.value);
onSetValue(editor.value);
isChangedRef.current = false;
}
editor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
break;
case keycodes.s:
if (event.ctrlKey) {
if (isChangedRef.current) {
onSetValue(editor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.current = false;
}
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
break;
}
}
return (
<StyledInput
onBlur={handleBlur}
ref={editorRef}
type="text"
onChange={() => (isChangedRef.current = true)}
onKeyDown={handleKeyDown}
style={{
width: widthRef.current,
minWidth: widthRef.current,
maxWidth: widthRef.current,
}}
/>
);
}

View File

@@ -0,0 +1,81 @@
<script lang="ts">
import keycodes from '../utility/keycodes';
import { onMount } from 'svelte';
import createRef from '../utility/createRef';
export let inplaceEditorState;
export let dispatchInsplaceEditor;
export let onSetValue;
export let width;
export let cellValue;
let domEditor;
const widthCopy = width;
const isChangedRef = createRef(!!inplaceEditorState.text);
function handleKeyDown(event) {
switch (event.keyCode) {
case keycodes.escape:
isChangedRef.set(false);
dispatchInsplaceEditor({ type: 'close' });
break;
case keycodes.enter:
if (isChangedRef.get()) {
// grider.setCellValue(rowIndex, uniqueName, editor.value);
onSetValue(domEditor.value);
isChangedRef.set(false);
}
domEditor.blur();
dispatchInsplaceEditor({ type: 'close', mode: 'enter' });
break;
case keycodes.s:
if (event.ctrlKey) {
if (isChangedRef.get()) {
onSetValue(domEditor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.set(false);
}
event.preventDefault();
dispatchInsplaceEditor({ type: 'close', mode: 'save' });
}
break;
}
}
function handleBlur() {
if (isChangedRef.get()) {
onSetValue(domEditor.value);
// grider.setCellValue(rowIndex, uniqueName, editor.value);
isChangedRef.set(false);
}
dispatchInsplaceEditor({ type: 'close' });
}
onMount(() => {
domEditor.value = inplaceEditorState.text || cellValue;
domEditor.focus();
if (inplaceEditorState.selectAll) {
domEditor.select();
}
});
</script>
<input
type="text"
on:change={() => isChangedRef.set(true)}
on:keydown={handleKeyDown}
on:blur={handleBlur}
bind:this={domEditor}
style={widthCopy ? `width:${widthCopy}px;min-width:${widthCopy}px;max-width:${widthCopy}px` : undefined}
/>
<style>
input {
border: 0px solid;
outline: none;
margin: 0px;
padding: 0px;
}
</style>

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import { createGridCache, createGridConfig, JslGridDisplay } from 'dbgate-datalib';
import { writable } from 'svelte/store';
import socket from '../utility/socket';
import useEffect from '../utility/useEffect';
import useFetch from '../utility/useFetch';
import DataGrid from './DataGrid.svelte';
import JslDataGridCore from './JslDataGridCore.svelte';
export let jslid;
$: info = useFetch({
params: { jslid },
url: 'jsldata/get-info',
defaultValue: {},
});
$: columns = ($info && $info.columns) || [];
const config = writable(createGridConfig());
const cache = writable(createGridCache());
$: display = new JslGridDisplay(jslid, columns, $config, config.update, $cache, cache.update);
</script>
{#key jslid}
<DataGrid {display} {jslid} gridCoreComponent={JslDataGridCore} />
{/key}

View File

@@ -1,97 +0,0 @@
import React from 'react';
import axios from '../utility/axios';
import { useSetOpenedTabs } from '../utility/globalState';
import useSocket from '../utility/SocketProvider';
import useShowModal from '../modals/showModal';
import ImportExportModal from '../modals/ImportExportModal';
import LoadingDataGridCore from './LoadingDataGridCore';
import RowsArrayGrider from './RowsArrayGrider';
async function loadDataPage(props, offset, limit) {
const { jslid, display } = props;
const response = await axios.post('jsldata/get-rows', {
jslid,
offset,
limit,
filters: display ? display.compileFilters() : null,
});
return response.data;
}
function dataPageAvailable(props) {
return true;
}
async function loadRowCount(props) {
const { jslid } = props;
const response = await axios.request({
url: 'jsldata/get-stats',
method: 'get',
params: {
jslid,
},
});
return response.data.rowCount;
}
export default function JslDataGridCore(props) {
const { jslid } = props;
const [changeIndex, setChangeIndex] = React.useState(0);
const [rowCountLoaded, setRowCountLoaded] = React.useState(null);
const showModal = useShowModal();
const setOpenedTabs = useSetOpenedTabs();
const socket = useSocket();
function exportGrid() {
const initialValues = {};
const archiveMatch = jslid.match(/^archive:\/\/([^/]+)\/(.*)$/);
if (archiveMatch) {
initialValues.sourceStorageType = 'archive';
initialValues.sourceArchiveFolder = archiveMatch[1];
initialValues.sourceList = [archiveMatch[2]];
} else {
initialValues.sourceStorageType = 'jsldata';
initialValues.sourceJslId = jslid;
initialValues.sourceList = ['query-data'];
}
showModal(modalState => <ImportExportModal modalState={modalState} initialValues={initialValues} />);
}
const handleJslDataStats = React.useCallback(
stats => {
if (stats.changeIndex < changeIndex) return;
setChangeIndex(stats.changeIndex);
setRowCountLoaded(stats.rowCount);
},
[changeIndex]
);
React.useEffect(() => {
if (jslid && socket) {
socket.on(`jsldata-stats-${jslid}`, handleJslDataStats);
return () => {
socket.off(`jsldata-stats-${jslid}`, handleJslDataStats);
};
}
}, [jslid]);
return (
<LoadingDataGridCore
{...props}
exportGrid={exportGrid}
loadDataPage={loadDataPage}
dataPageAvailable={dataPageAvailable}
loadRowCount={loadRowCount}
rowCountLoaded={rowCountLoaded}
loadNextDataToken={changeIndex}
onReload={() => setChangeIndex(0)}
griderFactory={RowsArrayGrider.factory}
griderFactoryDeps={RowsArrayGrider.factoryDeps}
/>
);
}

Some files were not shown because too many files have changed in this diff Show More