commit aed5db2cfaee22f15cd719580e491571ab0812b8 Author: serge-rider Date: Thu Apr 16 13:06:54 2020 +0300 Cloudbeaver 1.0 initial diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..e52cc913ca --- /dev/null +++ b/.gitattributes @@ -0,0 +1,32 @@ +# https://help.github.com/articles/dealing-with-line-endings/#example + +# Default is text with LF line endings. +* text eol=lf + +# Denote all files that are truly binary and should not be modified. +*.eot binary +*.ttf binary +*.woff binary +*.woff2 binary +*.tgz binary +*.zip binary +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.7z binary +*.pyc binary +*.pdf binary +*.jar binary +*.dll binary + + +# public/assets/**/* binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..f646565ca3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,42 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.war +*.nar +*.ear +*.rar + +# editor files +.vscode + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# web +/sources/web/node_modules/ +dist/ + +## IDEA configs + +*.iml +/idea/ +.idea/ + +## Eclipse configs + +.project +.classpath +.settings/ + +deploy/cloudbeaver +server/**/target \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..d804e0814f --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# Cloudbeaver +Cloud Database Manager + +### Build cloudbeaver + +Cloudbeaver is multi-platform server side application. +It can run on Linux, Windows and MacOS X. + +#### Prerequisites + +- Java 8, 9, 10, 11 or 12 +- Apache Maven +- Node.js +- Yarn + +#### Build and deploy + +- cd deploy +- ./build.sh + +Final artifacts are in `deploy/cloudbeaver`. +You can copy this folder to any other location. + +#### Running server + +- cd cloudbeaver +- run-server + +By default server listens port `8978` (you can change it in conf/cloudbeaver.conf). +You can configure nginx, Apache or any other web server in front of it. + diff --git a/deploy/.gitignore b/deploy/.gitignore new file mode 100644 index 0000000000..fd111a06b9 --- /dev/null +++ b/deploy/.gitignore @@ -0,0 +1,2 @@ +build +cloudbeaver diff --git a/deploy/build.bat b/deploy/build.bat new file mode 100644 index 0000000000..57ea92d443 --- /dev/null +++ b/deploy/build.bat @@ -0,0 +1,52 @@ +echo "Clone and build Cloudbeaver" + +IF EXIST cloudbeaver rmdir /S /Q cloudbeaver +mkdir cloudbeaver\server +mkdir cloudbeaver\conf +mkdir cloudbeaver\workspace +mkdir cloudbeaver\web + +IF EXIST build rmdir /S /Q build +mkdir build +cd build + +echo "Build dbeaver application" + +rem git clone --single-branch --branch release_7_0_1 https://github.com/dbeaver/dbeaver.git +git clone https://github.com/dbeaver/dbeaver.git +cd dbeaver +call mvn clean install +cd .. + +echo "Build cloudbeaver server" + +cd ..\..\server +call mvn clean package + +cd ..\deploy + +echo "Copy server packages" + +xcopy /E /Q ..\server\product\web-server\target\products\io.cloudbeaver.product\all\all\all\* cloudbeaver\server >NUL +copy scripts\run-server.bat cloudbeaver >NUL +mkdir cloudbeaver\workspace\GlobalConfiguration +xcopy /E /Q ..\samples\sample-databases\GlobalConfiguration cloudbeaver\workspace\GlobalConfiguration >NUL +copy ..\samples\sample-databases\cloudbeaver-sample.conf cloudbeaver\conf\cloudbeaver.conf >NUL + +echo "Build static content" + +cd ..\webapp + +call npm i lerna -g +call lerna bootstrap +call lerna run build --scope @dbeaver/dbeaver -- -- --pluginsList=../../../../products/default/plugins-list.js + +cd ..\deploy + +echo "Copy static content" + +xcopy /E /Q ..\webapp\packages\dbeaver\dist cloudbeaver\web >NUL + +echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server." + +pause diff --git a/deploy/build.sh b/deploy/build.sh new file mode 100644 index 0000000000..2063d8ef43 --- /dev/null +++ b/deploy/build.sh @@ -0,0 +1,51 @@ +#!/bin/bash +echo "Clone and build Cloudbeaver" + +rm -rf ./cloudbeaver +mkdir ./cloudbeaver +mkdir ./cloudbeaver/server +mkdir ./cloudbeaver/conf +mkdir ./cloudbeaver/workspace +mkdir ./cloudbeaver/web + +rm -rf build +mkdir build +cd build + +echo "Build dbeaver application" + +git clone https://github.com/dbeaver/dbeaver.git +cd dbeaver +mvn clean install +cd .. + +echo "Build cloudbeaver server" + +cd ../../server +mvn clean package + +cd ../deploy + +echo "Copy server packages" + +cp -rp ../server/product/web-server/target/products/io.cloudbeaver.product/all/all/all/* ./cloudbeaver/server +cp -p ./scripts/run-server.sh ./cloudbeaver +mkdir ./cloudbeaver/workspace/GlobalConfiguration +cp -rp ../samples/sample-databases/GlobalConfiguration cloudbeaver/workspace/GlobalConfiguration +cp ../samples/sample-databases/cloudbeaver-sample.conf cloudbeaver/conf/cloudbeaver.conf + +echo "Build static content" + +cd ../webapp + +npm i lerna +npx lerna bootstrap +npx lerna run build --scope @dbeaver/dbeaver -- -- --pluginsList=../../../../products/default/plugins-list.js + +cd ../deploy + +echo "Copy static content" + +cp -rp ../webapp/packages/dbeaver/dist cloudbeaver/web + +echo "Cloudbeaver is ready. Run run-server.bat in cloudbeaver folder to start the server." diff --git a/deploy/scripts/run-server.bat b/deploy/scripts/run-server.bat new file mode 100644 index 0000000000..04c377b33e --- /dev/null +++ b/deploy/scripts/run-server.bat @@ -0,0 +1,8 @@ +@rem echo off + +for /f %%a in ('dir /B /S server\plugins\org.eclipse.equinox.launcher*.jar') do SET launcherJar="%%a" +SET workspacePath=workspace + +echo "Starting Cloudbeaver Server" + +java -jar %launcherJar% -product io.cloudbeaver.server.product -data %workspacePath% -web-config conf/cloudbeaver.conf -nl en -registryMultiLanguage -vmargs -Xmx2048M diff --git a/deploy/scripts/run-server.sh b/deploy/scripts/run-server.sh new file mode 100644 index 0000000000..aa7e3f42df --- /dev/null +++ b/deploy/scripts/run-server.sh @@ -0,0 +1,8 @@ +#!/bin/bash +cd server +launcherJar=( plugins/org.eclipse.equinox.launcher*.jar ) +workspacePath=./../workspace + +echo "Starting Cloudbeaver Server" + +java -jar ${launcherJar} -product io.cloudbeaver.server.product -data ${workspacePath} -web-config ./../conf/cloudbeaver.conf -nl en -registryMultiLanguage -vmargs -Xmx2048M diff --git a/samples/sample-databases/GlobalConfiguration/.dbeaver/data-sources.json b/samples/sample-databases/GlobalConfiguration/.dbeaver/data-sources.json new file mode 100644 index 0000000000..4e1dc62fd0 --- /dev/null +++ b/samples/sample-databases/GlobalConfiguration/.dbeaver/data-sources.json @@ -0,0 +1,19 @@ +{ + "folders": {}, + "connections": { + "sqlite_xerial-sample-database": { + "provider": "generic", + "driver": "sqlite_jdbc", + "name": "SQLite - Chinook (Sample)", + "save-password": true, + "navigator-show-only-entities": false, + "navigator-hide-folders": false, + "read-only": false, + "configuration": { + "database": "${project.path}/db/Chinook.db", + "type": "dev", + "auth-model": "native" + } + } + } +} diff --git a/samples/sample-databases/GlobalConfiguration/.dbeaver/provided-connections.json b/samples/sample-databases/GlobalConfiguration/.dbeaver/provided-connections.json new file mode 100644 index 0000000000..edc5802a0a --- /dev/null +++ b/samples/sample-databases/GlobalConfiguration/.dbeaver/provided-connections.json @@ -0,0 +1,4 @@ +{ + "folders": {}, + "connections": {} +} diff --git a/samples/sample-databases/GlobalConfiguration/db/Chinook.db b/samples/sample-databases/GlobalConfiguration/db/Chinook.db new file mode 100644 index 0000000000..f6331c0185 Binary files /dev/null and b/samples/sample-databases/GlobalConfiguration/db/Chinook.db differ diff --git a/samples/sample-databases/GlobalConfiguration/db/README b/samples/sample-databases/GlobalConfiguration/db/README new file mode 100644 index 0000000000..0882204624 --- /dev/null +++ b/samples/sample-databases/GlobalConfiguration/db/README @@ -0,0 +1,6 @@ + Chinook Database - Version 1.3 + Script: Chinook_Sqlite.sql + Description: Creates and populates the Chinook database. + DB Server: Sqlite + Author: Luis Rocha + License: http://www.codeplex.com/ChinookDatabase/license diff --git a/samples/sample-databases/README.md b/samples/sample-databases/README.md new file mode 100644 index 0000000000..a6fe87c936 --- /dev/null +++ b/samples/sample-databases/README.md @@ -0,0 +1,3 @@ +### Sample databases + +Provides access to locally deployed SQLite sample database diff --git a/samples/sample-databases/cloudbeaver-sample.conf b/samples/sample-databases/cloudbeaver-sample.conf new file mode 100644 index 0000000000..8a3f008423 --- /dev/null +++ b/samples/sample-databases/cloudbeaver-sample.conf @@ -0,0 +1,6 @@ +ServerPort = 8978 +ServerName = Cloudbeaver Sample Server + +ContentRoot = web + +DevelMode = false diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..97b5ab6077 --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/META-INF/MANIFEST.MF @@ -0,0 +1,22 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: %Bundle-Vendor +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: io.cloudbeaver.connect.provider.sample;singleton:=true +Bundle-Version: 1.0.3.qualifier +Bundle-Release-Date: 20200420 +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: . +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.expressions, + org.apache.commons.logging, + org.eclipse.core.commands, + org.eclipse.core.resources, + io.cloudbeaver.server, + com.google.gson, + org.jkiss.bundle.graphql.java, + org.jkiss.dbeaver.model, + org.jkiss.dbeaver.model.sql +Bundle-Localization: OSGI-INF/l10n/bundle +Automatic-Module-Name: io.cloudbeaver.connect.provider.sample diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.connect.provider.sample/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..48e85a5cff --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,2 @@ +Bundle-Vendor = DBeaver Corp +Bundle-Name = Cloudbeaver - Connections configuration - Sample diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/build.properties b/server/bundles/io.cloudbeaver.connect.provider.sample/build.properties new file mode 100644 index 0000000000..3d281081e5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/build.properties @@ -0,0 +1,6 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + OSGI-INF/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/plugin.xml b/server/bundles/io.cloudbeaver.connect.provider.sample/plugin.xml new file mode 100644 index 0000000000..d4fb5ebc2a --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/plugin.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml new file mode 100644 index 0000000000..9368f1062d --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.connect.provider.sample + 1.0.3-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.connect.provider.sample/src/io/cloudbeaver/connect/provider/sample/SampleDataSourceConfigurationStorage.java b/server/bundles/io.cloudbeaver.connect.provider.sample/src/io/cloudbeaver/connect/provider/sample/SampleDataSourceConfigurationStorage.java new file mode 100644 index 0000000000..097dd68f38 --- /dev/null +++ b/server/bundles/io.cloudbeaver.connect.provider.sample/src/io/cloudbeaver/connect/provider/sample/SampleDataSourceConfigurationStorage.java @@ -0,0 +1,83 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.connect.provider.sample; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceConfigurationStorage; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * Sample configuration storage. + * Is the main source of data in web application + */ +public class SampleDataSourceConfigurationStorage implements DBPDataSourceConfigurationStorage { + + private static final Log log = Log.getLog(SampleDataSourceConfigurationStorage.class); + + public static final String SAMPLE_CONFIG_NAME = "provided-connections.json"; + + @Override + public String getStorageId() { + return "Sample"; + } + + @Override + public boolean isValid() { + return false; + } + + @Override + public boolean isDefault() { + return false; + } + + @Override + public String getStatus() { + return "Valid"; + } + + @Override + public List loadDataSources(DBPDataSourceRegistry registry, Map options) throws DBException { + IFolder metadataFolder = registry.getProject().getMetadataFolder(false); + if (metadataFolder.exists()) { + IFile sampleConfigFile = metadataFolder.getFile(SAMPLE_CONFIG_NAME); + if (sampleConfigFile.exists()) { + log.debug("Loading provided connections from [" + sampleConfigFile.getFullPath().toString() + "]"); + List dsList = registry.loadDataSourcesFromFile(this, sampleConfigFile); + for (DBPDataSourceContainer ds : dsList) { + log.debug("\tProvided connection: " + ds.getName()); + } + return dsList; + } + } + return Collections.emptyList(); + } + + @Override + public String getConfigurationFileSuffix() { + return "sample"; + } + +} diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..e8e503d4b0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/META-INF/MANIFEST.MF @@ -0,0 +1,10 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: Base JDBC drivers +Bundle-SymbolicName: io.cloudbeaver.resources.drivers.base;singleton:=true +Bundle-Version: 1.0.0 +Bundle-Vendor: DBeaver Corp +Bundle-ActivationPolicy: lazy +Eclipse-BundleShape: dir +Automatic-Module-Name: io.cloudbeaver.resources.drivers.base +Require-Bundle: io.cloudbeaver.server diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/build.properties b/server/bundles/io.cloudbeaver.resources.drivers.base/build.properties new file mode 100644 index 0000000000..90763a0f9f --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/build.properties @@ -0,0 +1,3 @@ +source.. = +bin.includes = plugin.xml,META-INF/,drivers/ +src.includes = diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/LICENSE.txt b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/LICENSE.txt new file mode 100644 index 0000000000..c27f75debe --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/LICENSE.txt @@ -0,0 +1,5 @@ +Jaybird JCA/JDBC driver is distributed under the GNU Lesser General Public License (LGPL). Text of the license can be obtained from http://www.gnu.org/copyleft/lesser.html. + +Using Jaybird (by importing Jaybird's public interfaces in your Java code), and extending Jaybird by subclassing or implementation of an extension interface (but not abstract or concrete class) is considered by the authors of Jaybird to be dynamic linking. Hence our interpretation of the LGPL is that the use of the unmodified Jaybird source does not affect the license of your application code. + +Even more, all extension interfaces to which application might want to link are released under dual LGPL/modified BSD license. Latter is basically “AS IS” license that allows any kind of use of that source code. Jaybird should be viewed as an implementation of that interfaces and LGPL section for dynamic linking is applicable in this case. \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/jaybird-full-3.0.3.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/jaybird-full-3.0.3.jar new file mode 100644 index 0000000000..d4be302d96 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/firebird3/jaybird-full-3.0.3.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/LICENSE.txt b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/LICENSE.txt new file mode 100644 index 0000000000..a2f19194a4 --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/LICENSE.txt @@ -0,0 +1 @@ +MariaDB JDBC driver is distributed under the GNU Lesser General Public License (LGPL). Text of the license can be obtained from http://www.gnu.org/copyleft/lesser.html. diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/mariadb-java-client-2.6.0.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/mariadb-java-client-2.6.0.jar new file mode 100644 index 0000000000..d0932844b7 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mariadb/mariadb-java-client-2.6.0.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/LICENSE.txt b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/LICENSE.txt new file mode 100644 index 0000000000..ffa372e2fe --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/LICENSE.txt @@ -0,0 +1,307 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software + interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as +a special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version + 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details + type 'show w'. This is free software, and you are welcome + to redistribute it under certain conditions; type 'show c' + for details. + +The hypothetical commands 'show w' and 'show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than 'show w' and +'show c'; they could even be mouse-clicks or menu items--whatever +suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the + program 'Gnomovision' (which makes passes at compilers) written + by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use +the GNU Lesser General Public License instead of this License. diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-5.1.44.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-5.1.44.jar new file mode 100644 index 0000000000..ebc4cc7ea7 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-5.1.44.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-8.0.11.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-8.0.11.jar new file mode 100644 index 0000000000..c8b8b3fc63 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/mysql-connector-java-8.0.11.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/protobuf-java-2.6.0.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/protobuf-java-2.6.0.jar new file mode 100644 index 0000000000..4bfbd4b71a Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/mysql/protobuf-java-2.6.0.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgis/postgis-jdbc-2.2.1.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgis/postgis-jdbc-2.2.1.jar new file mode 100644 index 0000000000..60bec3ad96 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgis/postgis-jdbc-2.2.1.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/LICENSE.txt b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/LICENSE.txt new file mode 100644 index 0000000000..ae3b6723ba --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/LICENSE.txt @@ -0,0 +1,26 @@ +Copyright (c) 1997-2010, PostgreSQL Global Development Group +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +3. Neither the name of the PostgreSQL Global Development Group nor the names + of its contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/postgresql-42.2.5.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/postgresql-42.2.5.jar new file mode 100644 index 0000000000..d89d4331a4 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/postgresql/postgresql-42.2.5.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/LICENSE.txt b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/LICENSE.txt new file mode 100644 index 0000000000..aafdcd3ade --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/LICENSE.txt @@ -0,0 +1,62 @@ +SQLite JDBC driver developed by Taro L. Saito is an extension of Zentus's SQLite JDBC driver that enables Java to access SQLite database files. +This program follows the Apache License version 2.0 (http://www.apache.org/licenses/ ) + + +/* ==================================================================== + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2000 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, + * if any, must include the following acknowledgment: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowledgment may appear in the software itself, + * if and wherever such third-party acknowledgments normally appear. + * + * 4. The names "Apache" and "Apache Software Foundation" must + * not be used to endorse or promote products derived from this + * software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache", + * nor may "Apache" appear in their name, without prior written + * permission of the Apache Software Foundation. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + * Portions of this software are based upon public domain software + * originally written at the National Center for Supercomputing Applications, + * University of Illinois, Urbana-Champaign. + */ + diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/sqlite-jdbc-3.30.1.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/sqlite-jdbc-3.30.1.jar new file mode 100644 index 0000000000..1895f83328 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/sqlite/xerial/sqlite-jdbc-3.30.1.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/caffeine-2.6.2.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/caffeine-2.6.2.jar new file mode 100644 index 0000000000..f2f724bf7d Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/caffeine-2.6.2.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/waffle-jna-1.9.0.jar b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/waffle-jna-1.9.0.jar new file mode 100644 index 0000000000..6a00507fb4 Binary files /dev/null and b/server/bundles/io.cloudbeaver.resources.drivers.base/drivers/waffle/waffle-jna-1.9.0.jar differ diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml new file mode 100644 index 0000000000..063faead1a --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/plugin.xml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml new file mode 100644 index 0000000000..d37c53433b --- /dev/null +++ b/server/bundles/io.cloudbeaver.resources.drivers.base/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.resources.drivers.base + 1.0.0 + eclipse-plugin + diff --git a/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..7784e1a024 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/META-INF/MANIFEST.MF @@ -0,0 +1,38 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: %Bundle-Vendor +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: io.cloudbeaver.server;singleton:=true +Bundle-Version: 7.0.3.qualifier +Bundle-Release-Date: 20200420 +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-Activator: io.cloudbeaver.server.WebPlatformActivator +Bundle-ClassPath: . +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.expressions, + org.apache.commons.logging, + org.eclipse.core.commands, + org.eclipse.core.resources, + org.apache.commons.jexl, + org.jkiss.utils, + org.jkiss.dbeaver.model, + org.jkiss.dbeaver.model.sql, + org.jkiss.dbeaver.registry, + org.jkiss.dbeaver.data.gis, + javax.servlet, + org.eclipse.jetty.server, + org.eclipse.jetty.servlet, + org.eclipse.jetty.util, + com.google.gson, + org.jkiss.bundle.graphql.java +Export-Package: io.cloudbeaver, + io.cloudbeaver.api, + io.cloudbeaver.server, + io.cloudbeaver.server.model, + io.cloudbeaver.server.model.resources, + io.cloudbeaver.server.model.session, + io.cloudbeaver.server.model.sql +Import-Package: org.slf4j +Bundle-Localization: OSGI-INF/l10n/bundle +Automatic-Module-Name: io.cloudbeaver.server diff --git a/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..26e0eb4189 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,2 @@ +Bundle-Vendor = DBeaver Corp +Bundle-Name = Cloudbeaver Web Server diff --git a/server/bundles/io.cloudbeaver.server/build.properties b/server/bundles/io.cloudbeaver.server/build.properties new file mode 100644 index 0000000000..6728418a75 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/build.properties @@ -0,0 +1,8 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + OSGI-INF/,\ + schema/,\ + static/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.server/docs/dbeaver-server.properties b/server/bundles/io.cloudbeaver.server/docs/dbeaver-server.properties new file mode 100644 index 0000000000..e4030493f2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/docs/dbeaver-server.properties @@ -0,0 +1,9 @@ +# Sample property file + +ServerPort = 8080 +ServerName = Cloudbeaver Web Server + +ContentRoot = /var/www/cloudbeaver/static + +RootURI = / +ServicesURI = /cloudbeaver/ diff --git a/server/bundles/io.cloudbeaver.server/plugin.xml b/server/bundles/io.cloudbeaver.server/plugin.xml new file mode 100644 index 0000000000..6a5b324043 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/plugin.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/pom.xml b/server/bundles/io.cloudbeaver.server/pom.xml new file mode 100644 index 0000000000..a9ccf278a1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.server + 7.0.3-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.driver.exsd b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.driver.exsd new file mode 100644 index 0000000000..538d10ab17 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.driver.exsd @@ -0,0 +1,34 @@ + + + + + + + + DBeaver data source provider + + + + + + + + + + + + + + + + + + + + Web service description + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.service.exsd b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.service.exsd new file mode 100644 index 0000000000..18e57e04cd --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/schema/io.cloudbeaver.service.exsd @@ -0,0 +1,34 @@ + + + + + + + + DBeaver data source provider + + + + + + + + + + + + + + + + + + + + Web service description + + + + + + diff --git a/server/bundles/io.cloudbeaver.server/schema/schema.graphqls b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls new file mode 100644 index 0000000000..9267d38c3c --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/schema/schema.graphqls @@ -0,0 +1,531 @@ +# Any object (JSON) +scalar Object +# Date/Time +scalar DateTime + +#################################################### +# General stuff +#################################################### + +# Async types + +union AsyncTaskResult = SQLExecuteInfo + +type AsyncTaskInfo { + id: String! + name: String + running: Boolean! + + status: String + error: ServerError + + result: SQLExecuteInfo +} + +# Various server errors descriptor + +type ServerError { + message: String + errorCode: String + stackTrace: String + causedBy: ServerError +} + +type ServerMessage { + time: String + message: String +} + +# Languages supported by server + +type ServerLanguage { + isoCode: String! + displayName: String + nativeName: String +} + +type WebServiceConfig { + id: String! + name: String! + description: String! + bundleVersion: String! +} + +type ServerConfig { + name: String + version: String + supportsPredefinedConnections: Boolean + supportsProvidedConnections: Boolean + supportsCustomConnections: Boolean + supportsConnectionBrowser: Boolean + supportsWorkspaces: Boolean + supportedLanguages: [ ServerLanguage ] + services: [ WebServiceConfig ] +} + +type SessionInfo { + id: ID! + + createTime: String + lastAccessTime: String + locale: String + + serverMessages: [ ServerMessage ] + connections: [ ConnectionInfo ] +} + +#################################################### +# Drivers and connections +#################################################### + +type DriverInfo { + id: ID! + name: String + description: String + icon: String + iconBig: String + + # Driver provider ID + providerId: ID + # Driver Java class name + driverClassName: String + defaultPort: String + sampleURL: String + driverInfoURL: String + driverPropertiesURL: String + embedded: Boolean + anonymousAccess: Boolean + allowsEmptyPassword: Boolean + licenseRequired: Boolean + license: String + custom: Boolean + # Driver score for ordering, biggest first + promotedScore: Int + + connectionProperties: Object + defaultConnectionProperties: Object + # Driver properties. + # Note: it is expensive property and it may produce database server roundtrips. + # Call it only when you really need it. + driverProperties: [DriverPropertyInfo] + driverParameters: Object +} + +type DriverPropertyInfo { + id: ID! + displayName: String + description: String + category: String + dataType: String + defaultValue: Object + validValues: [ Object ] +} + +# Data source info is a description of some remote database. Doesn't include user credentials +type DataSourceInfo { + id: ID! + driverId: ID! + name: String! + description: String + + host: String + server: String + port: String + url: String + properties: String +} + +# Configuration of particular connection. Used for new connection create. Includes auth info +input ConnectionConfig { + name: String + description: String + + # ID of predefined datasource + dataSourceId: ID + + # Custom connection parameters (all optional) + + # Driver ID + driverId: ID + # Host + host: String + # Port + port: String + # Databae name + databaseName: String + # Databae name + url: String + # Properties + properties: Object + + # User credentials + + userName: String + userPassword: String +} + +# Connection instance +type ConnectionInfo { + id: ID! + driverId: ID! + + name: String! + description: String + properties: String + + connected: Boolean! + provided: Boolean! + connectTime: String + connectionError: ServerError + serverVersion: String + clientVersion: String + + # Supported features (provided etc) + features: [ String! ] +} + +#################################################### +# Nodes, objects and properties +#################################################### + +type ObjectDescriptor { + id: Int + displayName: String + fullName: String + uniqueName: String + description: String + value: String +} + +type ObjectPropertyInfo { + # ID + id: String + # Human readable name + displayName: String + # Property description + description: String + # Property category (may be used if object has a lot of properties) + category: String + # Property data type (int, String, etc) + dataType: String + + # Property value. Note: for some properties value reading may take a lot of time (e.g. RowCount for tables) + value: Object + + # List of values this property can take. Makes sense only for enumerable properties + validValues: [ Object ] + # Supported features (system, hidden, inherited, foreign, expensive, etc) + features: [ String! ] +} + +input ObjectPropertyFilter { + ids: [String] + features: [String] + categories: [String] + dataTypes: [String] +} + +type DatabaseObjectInfo { + # Object name + name: String + # Description - optional + description: String + # Object type. Java class name in most cases + type: String + + # Read object properties. + # Optional parameter 'ids' filters properties by id. null means all properties. + # Note: property value reading may take a lot of time so don't read all property values always + # Examine property meta (features in particular) before reading them + properties(filter: ObjectPropertyFilter): [ ObjectPropertyInfo ] + + # Optional properties + + ordinalPosition: Int + fullyQualifiedName: String + overloadedName: String + uniqueName: String + state: String + + # Features: script, scriptExtended, dataContainer, dataManipulator, entity, schema, catalog + features: [ String ] + + # Supported editors: ddl, permissions, sourceDeclaration, sourceDefinition + editors: [ String ] +} + +type NavigatorNodeInfo { + # Node ID - generally a full path to the node from root of tree + id: ID! + # Node human readable name + name: String + # Node icon path + icon: String + # Node description + description: String + # Node type + nodeType: String + # Can this property have child nodes? + hasChildren: Boolean + + # Associated object. Maybe null for non-database objects + object: DatabaseObjectInfo + + # Supported features: item, container, leaf + features: [ String ] + + folder: Boolean + inline: Boolean + navigable: Boolean +} + +#################################################### +# SQL helpers +#################################################### + +type SQLDialectInfo { + name: String + + dataTypes: [ String ] + functions: [ String ] + reservedWords: [ String ] + quoteStrings: [ [ String ] ] + singleLineComments: [ String ] + multiLineComments: [ [ String ] ] + + catalogSeparator: String + structSeparator: String + scriptDelimiter: String + +} + +type SQLCompletionProposal { + displayString: String + type: String + score: Int + + replacementString: String + replacementOffset: Int + replacementLength: Int + cursorPosition: Int + + icon: String + nodePath: String +} + +#################################################### +# Data read / SQL execution +#################################################### + +# SQL context must be created for each SQL editor +type SQLContextInfo { + + id: ID! + defaultCatalog: String + defaultSchema: String + +} + +type DatabaseStructContainers { + + catalogList: [ DatabaseObjectInfo! ]! + schemaList: [ DatabaseObjectInfo! ]! + +} + +input SQLDataFilterConstraint { + attribute: String! + orderPosition: Int + orderAsc: Boolean + + criteria: String + operator: String + value: Object +} + +input SQLDataFilter { + + offset: Int + limit: Int + + constraints: [ SQLDataFilterConstraint ] + where: String +} + +type SQLResultColumn { + position: Int + name: String + label: String + icon: String + entityName: String + + dataKind: String + typeName: String + fullTypeName: String + maxLength: Int + scale: Int + precision: Int +} + +type SQLResultSet { + id: ID! + columns: [ SQLResultColumn ] + rows: [ [ Object ] ] + # server always returns hasMoreData = false + hasMoreData: Boolean +} + +type SQLQueryResults { + title: String + updateRowCount: Int + sourceQuery: String + + resultSet: SQLResultSet +} + +type SQLExecuteInfo { + + statusMessage: String + duration: Int + results: [ SQLQueryResults! ] +} + +type LogEntry { + time: DateTime + type: String! + message: String + stackTrace: String +} + +#################################################### +# Main API +#################################################### + +type Query { + # Returns server config + serverConfig: ServerConfig + + # Returns session state ( initialize if not ) + sessionState: SessionInfo + + # Get driver info + driverList( id: ID ): [ DriverInfo! ] + + # Get list of predefined data sources + dataSourceList: [ DataSourceInfo! ] + + # Return connection state + connectionState( id: ID! ): ConnectionInfo + + readSessionLog(maxEntries: Int, clearEntries: Boolean): [ LogEntry! ] + + # Get child nodes + navNodeChildren( + parentPath: ID!, + offset: Int, + limit: Int, + onlyFolders: Boolean): [ NavigatorNodeInfo! ] + + navNodeInfo( nodePath: ID! ): NavigatorNodeInfo + + navRefreshNode( nodePath: ID! ): Boolean + + navGetStructContainers( connectionId: ID!, catalog: ID ): DatabaseStructContainers! + + sqlDialectInfo( connectionId: ID! ): SQLDialectInfo + + sqlListContexts( connectionId: ID! ): [ SQLContextInfo ] + + sqlCompletionProposals( + connectionId: ID!, + contextId: ID!, + query: String!, + position: Int!, + maxResults: Int + ): [ SQLCompletionProposal ] + +} + +type Mutation { + # Initialize session + openSession: SessionInfo + + # Destroy session + closeSession: Boolean + + # Refreshes session on server and returns its state + touchSession: Boolean + + # Refreshes session on server and returns its state + changeSessionLanguage(locale: String): Boolean + + # Create new connection + createConnection( config: ConnectionConfig! ): ConnectionInfo + + # Test connection configuration. Returns remote server version + testConnection( config: ConnectionConfig! ): ConnectionInfo + + # Connect to database + openConnection( config: ConnectionConfig! ): ConnectionInfo + + # Disconnect from database + closeConnection( id: ID! ): Boolean! + + sqlContextCreate( connectionId: ID!, defaultCatalog: String, defaultSchema: String ): SQLContextInfo! + + sqlContextSetDefaults( connectionId: ID!, contextId: ID!, defaultCatalog: ID, defaultSchema: ID ): Boolean! + + sqlContextDestroy( connectionId: ID!, contextId: ID! ): Boolean! + + sqlExecuteQuery( + connectionId: ID!, + contextId: ID!, + sql: String!, + filter: SQLDataFilter + ): SQLExecuteInfo + + sqlResultClose(connectionId: ID!, contextId: ID!, resultId: ID!): Boolean! + + readDataFromContainer( + connectionId: ID!, + contextId: ID!, + containerNodePath: ID!, + filter: SQLDataFilter + ): SQLExecuteInfo + + updateResultsData( + connectionId: ID!, + contextId: ID!, + resultsId: ID!, + + updateRow: [ Object ]!, + # Map + updateValues: Object + ): SQLExecuteInfo + + ### Async API + + # Returns SQLExecuteInfo + asyncSqlExecuteQuery( + connectionId: ID!, + contextId: ID!, + sql: String!, + filter: SQLDataFilter + ): AsyncTaskInfo! + + #### Generic async functions + + asyncTaskCancel(id: String!): Boolean + + asyncTaskStatus(id: String!): AsyncTaskInfo! + +} + +#################################################### +# Schema +#################################################### + +schema { + query: Query + mutation: Mutation +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java new file mode 100644 index 0000000000..9fead31ae5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/DBWebException.java @@ -0,0 +1,129 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver; + +import graphql.ErrorClassification; +import graphql.ErrorType; +import graphql.GraphQLError; +import graphql.language.SourceLocation; +import io.cloudbeaver.server.graphql.GraphQLEndpoint; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +/** + * The activator class controls the plug-in life cycle + */ +public class DBWebException extends DBException implements GraphQLError { + + public static final String ERROR_CODE_SESSION_EXPIRED = "sessionExpired"; + + private List path; + private List locations; + private String webErrorCode; + + public DBWebException(String message) { + super(message); + } + + public DBWebException(String message, String errorCode) { + super(message); + this.webErrorCode = errorCode; + } + + public DBWebException(String message, Throwable cause) { + super(makeMessage(message, cause), cause); + } + + public DBWebException(Throwable cause, DBPDataSource dataSource) { + super(cause, dataSource); + } + + public DBWebException(String message, Throwable cause, DBPDataSource dataSource) { + super(makeMessage(message, cause), cause, dataSource); + } + + public String getWebErrorCode() { + return webErrorCode; + } + + @Override + public List getPath() { + return path; + } + + public void setPath(List path) { + this.path = path; + } + + @Override + public List getLocations() { + return locations; + } + + public void setLocations(List locations) { + this.locations = locations; + } + + @Override + public ErrorClassification getErrorType() { + return ErrorType.DataFetchingException; + } + + @Override + public Map getExtensions() { + StringWriter buf = new StringWriter(); + GeneralUtils.getRootCause(this).printStackTrace(new PrintWriter(buf, true)); + + Map extensions = new LinkedHashMap<>(); + String stString = buf.toString(); + int divPos = stString.indexOf(GraphQLEndpoint.class.getName()); + if (divPos != -1) { + stString = stString.substring(0, divPos); + divPos = stString.lastIndexOf(")"); + if (divPos != -1) { + stString = stString.substring(0, divPos + 1); + } + } + extensions.put("stackTrace", stString); + int errorCode = getErrorCode(); + if (errorCode != ERROR_CODE_NONE) { + extensions.put("errorCode", errorCode); + } + if (!CommonUtils.isEmpty(webErrorCode)) { + extensions.put("webErrorCode", webErrorCode); + } + String databaseState = getDatabaseState(); + if (databaseState != null) { + extensions.put("databaseState", databaseState); + } + + return extensions; + } + + private static String makeMessage(String message, Throwable cause) { + return message + ":\n" + cause.getMessage(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java new file mode 100644 index 0000000000..8f337bdbef --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/WebServiceUtils.java @@ -0,0 +1,77 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver; + +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPImage; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.registry.DataSourceProviderDescriptor; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.registry.driver.DriverDescriptor; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import java.io.InputStream; + +/** + * Various constants + */ +public class WebServiceUtils { + + private static final Log log = Log.getLog(WebServiceUtils.class); + + public static String makeIconId(DBPImage icon) { + return icon.getLocation(); + } + + public static String makeDriverFullId(DBPDriver driver) { + return driver.getProviderId() + ":" + driver.getId(); + } + + @NotNull + public static DBPDriver getDriverById(String id) throws DBWebException { + int divPos = id.indexOf(':'); + if (divPos < 0) { + throw new DBWebException("Bad driver id [" + id + "]"); + } + String dsId = id.substring(0, divPos); + String driverId = id.substring(divPos + 1); + DataSourceProviderDescriptor dsProvider = DataSourceProviderRegistry.getInstance().getDataSourceProvider(dsId); + if (dsProvider == null) { + throw new DBWebException("Data source provider '" + dsId + "' not found"); + } + DriverDescriptor driver = dsProvider.getDriver(driverId); + if (driver == null) { + throw new DBWebException("Driver '" + driverId + "' not found in provider '" + dsId + "'"); + } + return driver; + } + + @NotNull + public static DBPDataSourceRegistry getDataSourceRegistry() throws DBWebException { + DBPDataSourceRegistry registry = DBWorkbench.getPlatform().getWorkspace().getDefaultDataSourceRegistry(); + if (registry == null) { + throw new DBWebException("No activate data source registry"); + } + return registry; + } + + public static InputStream openStaticResource(String path) { + return WebServiceUtils.class.getClassLoader().getResourceAsStream(path); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWModel.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWModel.java new file mode 100644 index 0000000000..587d8cdcf0 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWModel.java @@ -0,0 +1,36 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.api; + +import graphql.schema.idl.TypeRuntimeWiring; +import io.cloudbeaver.server.CloudbeaverPlatform; +import io.cloudbeaver.server.model.session.WebSessionManager; + +/** + * Web service implementation + */ +public interface DBWModel { + + CloudbeaverPlatform getPlatform(); + + WebSessionManager getSessionManager(); + + TypeRuntimeWiring.Builder getQueryType(); + + TypeRuntimeWiring.Builder getMutationType(); + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWService.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWService.java new file mode 100644 index 0000000000..791fe6ecd3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWService.java @@ -0,0 +1,24 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.api; + +/** + * Web service implementation + */ +public interface DBWService { + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWServiceGraphQL.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWServiceGraphQL.java new file mode 100644 index 0000000000..8f71d76fb5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWServiceGraphQL.java @@ -0,0 +1,30 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.api; + +import graphql.schema.idl.TypeDefinitionRegistry; +import io.cloudbeaver.DBWebException; + +/** + * Web service implementation + */ +public interface DBWServiceGraphQL extends DBWService { + + TypeDefinitionRegistry getTypeDefinition() throws DBWebException; + + void bindWiring(DBWModel model) throws DBWebException; +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWUtils.java new file mode 100644 index 0000000000..83134819b1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/api/DBWUtils.java @@ -0,0 +1,55 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.api; + +import graphql.GraphQLContext; +import graphql.schema.DataFetchingEnvironment; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.model.session.WebSessionManager; +import io.cloudbeaver.server.model.sql.WebSQLContextInfo; +import io.cloudbeaver.server.model.sql.WebSQLProcessor; + +import javax.servlet.http.HttpServletRequest; + +/** + * Web utils + */ +public class DBWUtils { + + public static WebSQLProcessor getSQLProcessor(WebSessionManager sessionManager, DataFetchingEnvironment env) throws DBWebException { + return sessionManager.getWebSession(getServletRequest(env)).getSQLProcessor(env.getArgument("connectionId")); + } + + public static WebSQLContextInfo getSQLContext(WebSessionManager sessionManager, DataFetchingEnvironment env) throws DBWebException { + WebSQLProcessor processor = getSQLProcessor(sessionManager, env); + String contextId = env.getArgument("contextId"); + WebSQLContextInfo context = processor.getContext(contextId); + if (context == null) { + throw new DBWebException("SQL context '" + contextId + "' not found"); + } + return context; + } + + public static HttpServletRequest getServletRequest(DataFetchingEnvironment env) { + GraphQLContext context = env.getContext(); + HttpServletRequest request = context.get("request"); + if (request == null) { + throw new IllegalStateException("Null request"); + } + return request; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverApplication.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverApplication.java new file mode 100644 index 0000000000..ebd56712f6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverApplication.java @@ -0,0 +1,244 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import io.cloudbeaver.server.jetty.CloudbeaverJettyServer; +import org.eclipse.core.runtime.Platform; +import org.eclipse.equinox.app.IApplicationContext; +import org.eclipse.osgi.service.datalocation.Location; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.ModelPreferences; +import org.jkiss.dbeaver.model.app.DBPApplication; +import org.jkiss.dbeaver.registry.BaseApplicationImpl; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.dbeaver.utils.PrefUtils; +import org.jkiss.dbeaver.utils.SystemVariablesResolver; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.Properties; + +/** + * This class controls all aspects of the application's execution + */ +public class CloudbeaverApplication extends BaseApplicationImpl { + + private static final Log log = Log.getLog(CloudbeaverApplication.class); + + public static final String APPLICATION_PLUGIN_ID = "io.cloudbeaver.server"; + + private int serverPort = CloudbeaverConstants.DEFAULT_SERVER_PORT; + private String serverName = CloudbeaverConstants.DEFAULT_SERVER_NAME; + private String contentRoot = CloudbeaverConstants.DEFAULT_CONTENT_ROOT; + private String rootURI = CloudbeaverConstants.DEFAULT_ROOT_URI; + private String servicesURI = CloudbeaverConstants.DEFAULT_SERVICES_URI; + + private String workspaceLocation = CloudbeaverConstants.DEFAULT_WORKSPACE_LOCATION; + private String driversLocation = CloudbeaverConstants.DEFAULT_DRIVERS_LOCATION; + + private long maxSessionIdleTime = CloudbeaverConstants.MAX_SESSION_IDLE_TIME; + + private boolean develMode = false; + + public CloudbeaverApplication() { + } + + public int getServerPort() { + return serverPort; + } + + public String getServerName() { + return serverName; + } + + public String getContentRoot() { + return contentRoot; + } + + public String getRootURI() { + return rootURI; + } + + public String getServicesURI() { + return servicesURI; + } + + public String getDriversLocation() { + return driversLocation; + } + + public long getMaxSessionIdleTime() { + return maxSessionIdleTime; + } + + /** + * Gets singleton instance of DBeaver application + * + * @return application or null if application wasn't started or was stopped. + */ + public static CloudbeaverApplication getInstance() { + return (CloudbeaverApplication) BaseApplicationImpl.getInstance(); + } + + @Override + public boolean isHeadlessMode() { + return true; + } + + @Override + public Object start(IApplicationContext context) { + String configPath = CloudbeaverConstants.DEFAULT_CONFIG_FILE_PATH; + + String[] args = Platform.getCommandLineArgs(); + for (int i = 0; i < args.length; i++) { + if (args[i].equals(CloudbeaverConstants.CLI_PARAM_WEB_CONFIG) & args.length > i + 1) { + configPath = args[i + 1]; + break; + } + } + loadConfiguration(configPath); + + final Runtime runtime = Runtime.getRuntime(); + + Location instanceLoc = Platform.getInstanceLocation(); + try { + if (!instanceLoc.isSet()) { + URL wsLocationURL = new URL( + "file", //$NON-NLS-1$ + null, + workspaceLocation); + instanceLoc.set(wsLocationURL, true); + } + } catch (Exception e) { + log.debug("Error setting workspace location to " + workspaceLocation, e); + } + + CloudbeaverPlatform.setApplication(this); + + log.debug(GeneralUtils.getProductName() + " " + GeneralUtils.getProductVersion() + " is starting"); //$NON-NLS-1$ + log.debug("\tOS: " + System.getProperty(StandardConstants.ENV_OS_NAME) + " " + System.getProperty(StandardConstants.ENV_OS_VERSION) + " (" + System.getProperty(StandardConstants.ENV_OS_ARCH) + ")"); + log.debug("\tJava version: " + System.getProperty(StandardConstants.ENV_JAVA_VERSION) + " by " + System.getProperty(StandardConstants.ENV_JAVA_VENDOR) + " (" + System.getProperty(StandardConstants.ENV_JAVA_ARCH) + "bit)"); + log.debug("\tInstall path: '" + SystemVariablesResolver.getInstallPath() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + log.debug("\tGlobal workspace: '" + instanceLoc.getURL() + "'"); //$NON-NLS-1$ //$NON-NLS-2$ + log.debug("\tMemory available " + (runtime.totalMemory() / (1024 * 1024)) + "Mb/" + (runtime.maxMemory() / (1024 * 1024)) + "Mb"); + + DBPApplication application = DBWorkbench.getPlatform().getApplication(); + + log.debug("\tContent root: " + contentRoot); + //log.debug("\tDrivers root: " + driversLocation); + log.debug("\tProduct details: " + application.getInfoDetails()); + log.debug("\tBase port: " + serverPort); + log.debug("\tBase URI: " + servicesURI); + if (develMode) { + log.debug("\tDevelopment mode"); + } else { + log.debug("\tProduction mode"); + } + + runWebServer(); + + log.debug("Shutdown"); + + return null; + } + + private void loadConfiguration(String configPath) { + log.debug("Using configuration " + configPath); + + File configFile = new File(configPath); + if (!configFile.exists()) { + log.debug("Configuration file " + configPath + " doesn't exist. Use defaults."); + } else { + Properties props = new Properties(); + try (InputStream is = new FileInputStream(configFile)) { + props.load(is); + parseConfiguration(props); + } catch (IOException e) { + log.debug("Error reading config file", e); + } + } + // Set default preferences + PrefUtils.setDefaultPreferenceValue(ModelPreferences.getPreferences(), ModelPreferences.UI_DRIVERS_HOME, getDriversLocation()); + } + + private void parseConfiguration(Properties props) { + serverPort = CommonUtils.toInt(getConfigParameter(props, CloudbeaverConstants.PARAM_SERVER_PORT, String.valueOf(CloudbeaverConstants.DEFAULT_SERVER_PORT))); + serverName = getConfigParameter(props, CloudbeaverConstants.PARAM_SERVER_NAME, CloudbeaverConstants.DEFAULT_SERVER_NAME); + contentRoot = getConfigParameter(props, CloudbeaverConstants.PARAM_CONTENT_ROOT, CloudbeaverConstants.DEFAULT_CONTENT_ROOT); + rootURI = getConfigParameter(props, CloudbeaverConstants.PARAM_ROOT_URI, CloudbeaverConstants.DEFAULT_ROOT_URI); + servicesURI = getConfigParameter(props, CloudbeaverConstants.PARAM_SERVICES_URI, CloudbeaverConstants.DEFAULT_SERVICES_URI); + driversLocation = getConfigParameter(props, CloudbeaverConstants.PARAM_DRIVERS_LOCATION, CloudbeaverConstants.DEFAULT_DRIVERS_LOCATION); + workspaceLocation = getConfigParameter(props, CloudbeaverConstants.PARAM_WORKSPACE_LOCATION, CloudbeaverConstants.DEFAULT_WORKSPACE_LOCATION); + + maxSessionIdleTime = getConfigParameter(props, CloudbeaverConstants.PARAM_SESSION_EXPIRE_PERIOD, CloudbeaverConstants.MAX_SESSION_IDLE_TIME); + + develMode = CommonUtils.toBoolean(getConfigParameter(props, CloudbeaverConstants.PARAM_DEVEL_MODE, "false")); + } + + @NotNull + private String getConfigParameter(@NotNull Properties props, @NotNull String paramName, @NotNull String defValue) { + String property = props.getProperty(paramName); + if (property == null) { + property = defValue; + } + return property; + } + + private long getConfigParameter(@NotNull Properties props, @NotNull String paramName, long defValue) { + String property = props.getProperty(paramName); + if (property == null) { + return defValue; + } + return CommonUtils.toLong(property, defValue); + } + + private void runWebServer() { + log.debug("Starting Jetty server (" + serverPort + ") "); + new CloudbeaverJettyServer().runServer(); + } + + + @Override + public void stop() { + shutdown(); + } + + private void shutdown() { + log.debug("Cloudbeaver Server is stopping"); //$NON-NLS-1$ + } + + @Override + public String getInfoDetails() { + return serverName; + } + + @Override + public String getDefaultProjectName() { + return "GlobalConfiguration"; + } + + public boolean isDevelMode() { + return develMode; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverConstants.java new file mode 100644 index 0000000000..133e8f9bf7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverConstants.java @@ -0,0 +1,60 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.jkiss.dbeaver.model.DBConstants; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; + +/** + * Various constants + */ +public class CloudbeaverConstants { + + public static final DateFormat ISO_DATE_FORMAT = new SimpleDateFormat(DBConstants.DEFAULT_ISO_TIMESTAMP_FORMAT); + + public static final String DEFAULT_CONFIG_FILE_PATH = "/etc/cloudbeaver.conf"; + + public static final String CLI_PARAM_WEB_CONFIG = "-web-config"; + + public static final String PARAM_SERVER_PORT = "ServerPort"; + public static final String PARAM_SERVER_NAME = "ServerName"; + public static final String PARAM_CONTENT_ROOT = "ContentRoot"; + public static final String PARAM_ROOT_URI = "RootURI"; + public static final String PARAM_SERVICES_URI = "ServiceURI"; + public static final String PARAM_DRIVERS_LOCATION = "DriversLocation"; + public static final String PARAM_WORKSPACE_LOCATION = "WorkspaceLocation"; + + public static final String PARAM_SESSION_EXPIRE_PERIOD = "ExpireSessionAfterPeriod"; + + public static final String PARAM_DEVEL_MODE = "DevelMode"; + + public static final int DEFAULT_SERVER_PORT = 8080; + public static final String DEFAULT_SERVER_NAME = "Cloudbeaver Web Server"; + public static final String DEFAULT_CONTENT_ROOT = "/var/www/cloudbeaver"; + public static final String DEFAULT_ROOT_URI = "/"; + public static final String DEFAULT_SERVICES_URI = "/dbeaver/"; + + public static final String DEFAULT_DEPLOY_LOCATION = "/opt/cloudbeaver"; + public static final String DEFAULT_DRIVERS_LOCATION = DEFAULT_DEPLOY_LOCATION + "/drivers"; + public static final String DEFAULT_WORKSPACE_LOCATION = DEFAULT_DEPLOY_LOCATION + "/workspace"; + + // Default max idle time (10 minutes) + public static final long MAX_SESSION_IDLE_TIME = 10 * 60 * 1000; + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatform.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatform.java new file mode 100644 index 0000000000..232e769943 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatform.java @@ -0,0 +1,356 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.server; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.server.model.WebDataSourceConfig; +import io.cloudbeaver.server.model.WebServerConfig; +import io.cloudbeaver.server.model.session.WebSessionManager; +import io.cloudbeaver.server.registry.WebDriverRegistry; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Platform; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.DBPExternalFileManager; +import org.jkiss.dbeaver.model.app.*; +import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderDescriptor; +import org.jkiss.dbeaver.model.connection.DBPDataSourceProviderRegistry; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.connection.DBPDriverLibrary; +import org.jkiss.dbeaver.model.impl.app.DefaultCertificateStorage; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; +import org.jkiss.dbeaver.model.qm.QMController; +import org.jkiss.dbeaver.model.qm.QMUtils; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor; +import org.jkiss.dbeaver.registry.BasePlatformImpl; +import org.jkiss.dbeaver.registry.DataSourceProviderRegistry; +import org.jkiss.dbeaver.runtime.SecurityProviderUtils; +import org.jkiss.dbeaver.runtime.qm.QMControllerImpl; +import org.jkiss.dbeaver.runtime.qm.QMLogFileWriter; +import org.jkiss.dbeaver.utils.ContentUtils; +import org.jkiss.dbeaver.utils.GeneralUtils; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.StandardConstants; +import org.osgi.framework.Bundle; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.List; + +/** + * CloudbeaverPlatform + */ +public class CloudbeaverPlatform extends BasePlatformImpl { + + // The plug-in ID + public static final String PLUGIN_ID = "io.cloudbeaver.server"; //$NON-NLS-1$ + + private static final Log log = Log.getLog(CloudbeaverPlatform.class); + + private static final String TEMP_PROJECT_NAME = "cloudbeaver-temp"; + + static CloudbeaverPlatform instance; + + @Nullable + private static CloudbeaverApplication application = null; + + private File tempFolder; + + private QMControllerImpl queryManager; + private QMLogFileWriter qmLogWriter; + private DBACertificateStorage certificateStorage; + private CloudbeaverWorkspace workspace; + + private WebSessionManager sessionManager; + private final List applicableDrivers = new ArrayList<>(); + + + public static CloudbeaverPlatform getInstance() { + if (instance == null) { + synchronized (CloudbeaverPlatform.class) { + if (instance == null) { + // Initialize DBeaver Core + CloudbeaverPlatform.createInstance(); + } + } + } + return instance; + } + + private static CloudbeaverPlatform createInstance() { + log.debug("Initializing product: " + GeneralUtils.getProductTitle()); + if (Platform.getProduct() != null) { + Bundle definingBundle = Platform.getProduct().getDefiningBundle(); + if (definingBundle != null) { + log.debug("Host plugin: " + definingBundle.getSymbolicName() + " " + definingBundle.getVersion()); + } else { + log.debug("!!! No product bundle found"); + } + } + + try { + instance = new CloudbeaverPlatform(); + instance.initialize(); + return instance; + } catch (Throwable e) { + log.error("Error initializing CloudbeaverPlatform", e); + throw new IllegalStateException("Error initializing CloudbeaverPlatform", e); + } + } + + public static DBPPreferenceStore getGlobalPreferenceStore() { + return WebPlatformActivator.getInstance().getPreferences(); + } + + private CloudbeaverPlatform() { + } + + public static void setApplication(CloudbeaverApplication application) { + CloudbeaverPlatform.application = application; + } + + @Override + protected void initialize() { + long startTime = System.currentTimeMillis(); + log.debug("Initialize web platform..."); + + // Register BC security provider + SecurityProviderUtils.registerSecurityProvider(); + + // Register properties adapter + this.workspace = new CloudbeaverWorkspace(this, ResourcesPlugin.getWorkspace()); + this.workspace.initializeProjects(); + + QMUtils.initApplication(this); + this.queryManager = new QMControllerImpl(); + + this.qmLogWriter = new QMLogFileWriter(); + this.queryManager.registerMetaListener(qmLogWriter); + + // Init default network settings + installProxySelector(); + + this.certificateStorage = new DefaultCertificateStorage( + new File(WebPlatformActivator.getInstance().getStateLocation().toFile(), "security")); + + super.initialize(); + + for (DBPDataSourceProviderDescriptor dspd : DataSourceProviderRegistry.getInstance().getEnabledDataSourceProviders()) { + for (DBPDriver driver : dspd.getEnabledDrivers()) { + if (!WebDriverRegistry.getInstance().isDriverEnabled(driver)) { + continue; + } + List libraries = driver.getDriverLibraries(); + if (!libraries.isEmpty()) { + boolean hasAllFiles = true; + for (DBPDriverLibrary lib : libraries) { + if (!lib.isOptional() && (lib.getLocalFile() == null || !lib.getLocalFile().exists())) { + hasAllFiles = false; + log.error("Driver '" + driver.getId() + "' is missing library '" + lib.getDisplayName() + "'"); + break; + } + } + if (hasAllFiles) { + applicableDrivers.add(driver); + } + } + } + } + + sessionManager = WebSessionManager.getInstance(); + + new WebSessionMonitorJob(this).scheduleMonitor(); + + log.debug("Web platform initialized (" + (System.currentTimeMillis() - startTime) + "ms)"); + } + + public synchronized void dispose() { + long startTime = System.currentTimeMillis(); + log.debug("Shutdown Core..."); + + super.dispose(); + + if (this.qmLogWriter != null) { + this.queryManager.unregisterMetaListener(qmLogWriter); + this.qmLogWriter.dispose(); + this.qmLogWriter = null; + } + if (this.queryManager != null) { + this.queryManager.dispose(); + //queryManager = null; + } + DataSourceProviderRegistry.getInstance().dispose(); + + if (workspace != null) { + try { + workspace.save(new VoidProgressMonitor()); + } catch (DBException ex) { + log.error("Can't save workspace", ex); //$NON-NLS-1$ + } + } + + // Remove temp folder + if (tempFolder != null) { + + if (!ContentUtils.deleteFileRecursive(tempFolder)) { + log.warn("Can't delete temp folder '" + tempFolder.getAbsolutePath() + "'"); + } + tempFolder = null; + } + + CloudbeaverPlatform.application = null; + CloudbeaverPlatform.instance = null; + System.gc(); + log.debug("Shutdown completed in " + (System.currentTimeMillis() - startTime) + "ms"); + } + + @NotNull + @Override + public DBPWorkspace getWorkspace() { + return workspace; + } + + @NotNull + @Override + public DBPResourceHandler getDefaultResourceHandler() { + return CloudbeaverResourceHandler.INSTANCE; + } + + @NotNull + @Override + public CloudbeaverApplication getApplication() { + return application; + } + + public List getApplicableDrivers() { + return applicableDrivers; + } + + @NotNull + @Override + public DBPDataSourceProviderRegistry getDataSourceProviderRegistry() { + return DataSourceProviderRegistry.getInstance(); + } + + @NotNull + public QMController getQueryManager() { + return queryManager; + } + + @NotNull + @Override + public DBPPreferenceStore getPreferenceStore() { + return WebPlatformActivator.getInstance().getPreferences(); + } + + @NotNull + @Override + public DBACertificateStorage getCertificateStorage() { + return certificateStorage; + } + + @NotNull + @Override + public DBASecureStorage getSecureStorage() { + return application.getSecureStorage(); + } + + @NotNull + @Override + public DBPExternalFileManager getExternalFileManager() { + return workspace; + } + + @NotNull + public File getTempFolder(DBRProgressMonitor monitor, String name) { + if (tempFolder == null) { + // Make temp folder + monitor.subTask("Create temp folder"); + try { + final java.nio.file.Path tempDirectory = Files.createTempDirectory(TEMP_PROJECT_NAME); + tempFolder = tempDirectory.toFile(); + } catch (IOException e) { + final String sysTempFolder = System.getProperty(StandardConstants.ENV_TMP_DIR); + if (!CommonUtils.isEmpty(sysTempFolder)) { + tempFolder = new File(sysTempFolder, TEMP_PROJECT_NAME); + if (!tempFolder.mkdirs()) { + final String sysUserFolder = System.getProperty(StandardConstants.ENV_USER_HOME); + if (!CommonUtils.isEmpty(sysUserFolder)) { + tempFolder = new File(sysUserFolder, TEMP_PROJECT_NAME); + if (!tempFolder.mkdirs()) { + tempFolder = new File(TEMP_PROJECT_NAME); + } + } + + } + } + } + } + if (!tempFolder.exists() && !tempFolder.mkdirs()) { + log.error("Can't create temp directory!"); + } + return tempFolder; + } + + @NotNull + @Override + public File getConfigurationFile(String fileName) { + return WebPlatformActivator.getConfigurationFile(fileName); + } + + @Override + public boolean isShuttingDown() { + return false; + } + + public WebServerConfig getServerConfig() { + WebServerConfig config = new WebServerConfig( + CloudbeaverApplication.getInstance().getServerName(), + GeneralUtils.getProductVersion().toString() + ); + config.setSupportsPredefinedConnections(true); + return config; + } + + public WebSessionManager getSessionManager() { + return sessionManager; + } + + public List getGlobalDataSources() throws DBWebException { + List result = new ArrayList<>(); + DBPDataSourceRegistry dsRegistry = WebServiceUtils.getDataSourceRegistry(); + + for (DBPDataSourceContainer ds : dsRegistry.getDataSources()) { + if (applicableDrivers.contains(ds.getDriver()) && !ds.isProvided()) { + result.add(new WebDataSourceConfig(ds)); + } else { + log.debug("Global datasource '" + ds.getName() + "' ignored - driver is not applicable"); + } + } + + return result; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatformUI.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatformUI.java new file mode 100644 index 0000000000..321c50013e --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverPlatformUI.java @@ -0,0 +1,28 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.jkiss.dbeaver.runtime.ui.console.ConsoleUserInterface; + +/** + * The activator class controls the plug-in life cycle + */ +public class CloudbeaverPlatformUI extends ConsoleUserInterface { + + public static final CloudbeaverPlatformUI INSTANCE = new CloudbeaverPlatformUI(); + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverResourceHandler.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverResourceHandler.java new file mode 100644 index 0000000000..536c1d31e7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverResourceHandler.java @@ -0,0 +1,83 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.runtime.CoreException; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPResourceHandler; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.navigator.DBNResource; + +import java.util.List; + +/** + * Default resource handler + */ +public class CloudbeaverResourceHandler implements DBPResourceHandler { + + public static final CloudbeaverResourceHandler INSTANCE = new CloudbeaverResourceHandler(); + + @Override + public int getFeatures(IResource resource) { + if (resource instanceof IFile) { + return FEATURE_OPEN | FEATURE_DELETE | FEATURE_RENAME; + } else { + return FEATURE_DELETE | FEATURE_RENAME | FEATURE_CREATE_FOLDER | FEATURE_MOVE_INTO; + } + } + + @NotNull + @Override + public String getTypeName(@NotNull IResource resource) { + return "resource"; //$NON-NLS-1$ + } + + @Override + public String getResourceDescription(@NotNull IResource resource) { + return ""; + } + + @Override + public List getAssociatedDataSources(DBNResource resource) { + return null; + } + + @Override + public String getResourceNodeName(IResource resource) { + return resource.toString(); + } + + @NotNull + @Override + public DBNResource makeNavigatorNode(@NotNull DBNNode parentNode, @NotNull IResource resource) throws CoreException, DBException { + DBNResource node = new DBNResource(parentNode, resource, this); + return node; + } + + @Override + public void updateNavigatorNode(@NotNull DBNResource node, @NotNull IResource resource) { + } + + @Override + public void openResource(@NotNull IResource resource) throws CoreException, DBException { + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverWorkspace.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverWorkspace.java new file mode 100644 index 0000000000..1194bdb4b3 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/CloudbeaverWorkspace.java @@ -0,0 +1,41 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.eclipse.core.resources.IWorkspace; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.registry.BaseWorkspaceImpl; + +/** + * Web workspace. + * + * Basically just a wrapper around Eclipse workspace. + */ +public class CloudbeaverWorkspace extends BaseWorkspaceImpl { + + public CloudbeaverWorkspace(DBPPlatform platform, IWorkspace eclipseWorkspace) { + super(platform, eclipseWorkspace); + } + + @NotNull + @Override + public String getWorkspaceId() { + return "web-workspace"; + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformActivator.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformActivator.java new file mode 100644 index 0000000000..9834c1d52f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformActivator.java @@ -0,0 +1,110 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.Plugin; +import org.jkiss.dbeaver.ModelPreferences; +import org.jkiss.dbeaver.model.impl.preferences.BundlePreferenceStore; +import org.jkiss.dbeaver.model.preferences.DBPPreferenceStore; +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +import java.io.File; +import java.io.PrintStream; + +/** + * The activator class controls the plug-in life cycle + */ +public class WebPlatformActivator extends Plugin { + + // The shared instance + private static WebPlatformActivator instance; + private static File configDir; + private PrintStream debugWriter; + private DBPPreferenceStore preferences; + + public WebPlatformActivator() { + } + + public static WebPlatformActivator getInstance() { + return instance; + } + + @Override + public void start(BundleContext context) + throws Exception { + super.start(context); + + instance = this; + + Bundle bundle = getBundle(); + ModelPreferences.setMainBundle(bundle); + preferences = new BundlePreferenceStore(bundle); + } + + @Override + public void stop(BundleContext context) + throws Exception { + this.shutdownPlatform(); + + if (debugWriter != null) { + debugWriter.close(); + debugWriter = null; + } + instance = null; + + super.stop(context); + } + + /** + * Returns configuration file + */ + public static synchronized File getConfigurationFile(String fileName) + { + if (configDir == null) { + configDir = getInstance().getStateLocation().toFile(); + } + return new File(configDir, fileName); + } + + public DBPPreferenceStore getPreferences() { + return preferences; + } + + /** + * Returns the workspace instance. + */ + public static IWorkspace getWorkspace() { + return ResourcesPlugin.getWorkspace(); + } + + private void shutdownPlatform() { + try { + // Dispose core + if (CloudbeaverPlatform.instance != null) { + CloudbeaverPlatform.instance.dispose(); + } + } catch (Throwable e) { + e.printStackTrace(); + System.err.println("Internal error after shutdown process:" + e.getMessage()); //$NON-NLS-1$ + } + } + + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java new file mode 100644 index 0000000000..3d2f96de7f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebPlatformAdapterFactory.java @@ -0,0 +1,47 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * Copyright (C) 2017-2018 Alexander Fedorov (alexander.fedorov@jkiss.org) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.server; + +import org.eclipse.core.runtime.IAdapterFactory; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.runtime.ui.DBPPlatformUI; + +public class WebPlatformAdapterFactory implements IAdapterFactory { + + private static final Class[] CLASSES = new Class[] { DBPPlatform.class, DBPPlatformUI.class }; + + @Override + public T getAdapter(Object adaptableObject, Class adapterType) { + if (adaptableObject instanceof DBWorkbench) { + if (adapterType == DBPPlatform.class) { + return adapterType.cast(CloudbeaverPlatform.getInstance()); + } else if (adapterType == DBPPlatformUI.class) { + return adapterType.cast(CloudbeaverPlatformUI.INSTANCE); + } + } + return null; + } + + @Override + public Class[] getAdapterList() { + return CLASSES; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java new file mode 100644 index 0000000000..487b04102b --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebServiceConnectionsImpl.java @@ -0,0 +1,37 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.server; + +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.runtime.DBRProgressListener; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.runtime.DBServiceConnections; + +/** + * WebServiceConnectionsImpl + * + * Needed to instantiate connections thru navigator model. + */ +public class WebServiceConnectionsImpl implements DBServiceConnections { + + @Override + public void initConnection(DBRProgressMonitor monitor, DBPDataSourceContainer dataSourceContainer, DBRProgressListener onFinish) throws DBException { + dataSourceContainer.connect(monitor, true, false); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java new file mode 100644 index 0000000000..f3ea14a348 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/WebSessionMonitorJob.java @@ -0,0 +1,57 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server; + +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.dbeaver.model.runtime.AbstractJob; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; + +/** + * WebSessionMonitorJob + */ +class WebSessionMonitorJob extends AbstractJob { + private static final int MONITOR_INTERVAL = 10000; // once per 10 seconds + + private final CloudbeaverPlatform platform; + + WebSessionMonitorJob(CloudbeaverPlatform platform) { + super("Web session monitor"); + setUser(false); + setSystem(true); + this.platform = platform; + } + + @Override + protected IStatus run(DBRProgressMonitor monitor) { + if (platform.isShuttingDown()) { + return Status.OK_STATUS; + } + + platform.getSessionManager().expireIdleSessions(); + + if (!platform.isShuttingDown()) { + scheduleMonitor(); + } + return Status.OK_STATUS; + } + + void scheduleMonitor() { + schedule(MONITOR_INTERVAL); + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java new file mode 100644 index 0000000000..b27aadd27a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLConstants.java @@ -0,0 +1,78 @@ +package io.cloudbeaver.server.graphql; + +public class GraphQLConstants { + + public static final String CONTENT_TYPE_JSON_UTF8 = "application/json;charset=UTF-8"; + + public static final String SCHEMA_READ_QUERY = " __schema {\n" + + " queryType { name }\n" + + " mutationType { name }\n" + + " subscriptionType { name }\n" + + " types {\n" + + " ...FullType\n" + + " }\n" + + " directives {\n" + + " name\n" + + " description\n" + + " locations\n" + + " args {\n" + + " ...InputValue\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + " fragment FullType on __Type {\n" + + " kind\n" + + " name\n" + + " description\n" + + " fields(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " args {\n" + + " ...InputValue\n" + + " }\n" + + " type {\n" + + " ...TypeRef\n" + + " }\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " inputFields {\n" + + " ...InputValue\n" + + " }\n" + + " interfaces {\n" + + " ...TypeRef\n" + + " }\n" + + " enumValues(includeDeprecated: true) {\n" + + " name\n" + + " description\n" + + " isDeprecated\n" + + " deprecationReason\n" + + " }\n" + + " possibleTypes {\n" + + " ...TypeRef\n" + + " }\n" + + " }\n" + + " fragment InputValue on __InputValue {\n" + + " name\n" + + " description\n" + + " type { ...TypeRef }\n" + + " defaultValue\n" + + " }\n" + + " fragment TypeRef on __Type {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " ofType {\n" + + " kind\n" + + " name\n" + + " }\n" + + " }\n" + + " }"; + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java new file mode 100644 index 0000000000..eca4351f77 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLEndpoint.java @@ -0,0 +1,294 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.graphql; + +import com.google.gson.*; +import graphql.*; +import graphql.execution.AsyncExecutionStrategy; +import graphql.execution.DataFetcherExceptionHandlerResult; +import graphql.execution.ExecutionPath; +import graphql.execution.instrumentation.SimpleInstrumentation; +import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters; +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import graphql.language.SourceLocation; +import graphql.schema.DataFetcher; +import graphql.schema.GraphQLSchema; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.api.DBWService; +import io.cloudbeaver.api.DBWServiceGraphQL; +import io.cloudbeaver.server.CloudbeaverApplication; +import io.cloudbeaver.server.registry.WebServiceDescriptor; +import io.cloudbeaver.server.registry.WebServiceRegistry; +import org.jkiss.dbeaver.Log; +import org.jkiss.utils.IOUtils; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.net.URL; +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +public class GraphQLEndpoint extends HttpServlet { + + private static final Log log = Log.getLog(GraphQLEndpoint.class); + + private static final String HEADER_ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin"; + private static final String HEADER_ACCESS_CONTROL_ALLOW_HEADERS = "Access-Control-Allow-Headers"; + private static final String HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS = "Access-Control-Allow-Credentials"; + + private static final String CORE_SCHEMA_FILE_NAME = "schema/schema.graphqls"; + + private final GraphQL graphQL; + + private static Gson gson = new GsonBuilder() + .serializeNulls() + .setPrettyPrinting() + .create(); + + public GraphQLEndpoint() { + GraphQLSchema schema = buildSchema(); + + graphQL = GraphQL + .newGraphQL(schema) + .instrumentation(new WebInstrumentation()) + .queryExecutionStrategy(new WebExecutionStrategy()) + .mutationExecutionStrategy(new WebExecutionStrategy()) + .build(); + } + + private GraphQLSchema buildSchema() { + SchemaParser schemaParser = new SchemaParser(); + TypeDefinitionRegistry parsedSchema = new TypeDefinitionRegistry(); + + try (InputStream schemaStream = WebServiceUtils.openStaticResource(CORE_SCHEMA_FILE_NAME)) { + try (Reader schemaReader = new InputStreamReader(schemaStream)) { + parsedSchema.merge(schemaParser.parse(schemaReader)); + } + } catch (IOException e) { + throw new RuntimeException("Error reading core schema", e); + } + + for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) { + DBWService instance; + try { + instance = wsd.getInstance(); + } catch (Exception e) { + log.error(e); + continue; + } + if (instance instanceof DBWServiceGraphQL) { + try { + TypeDefinitionRegistry typeDefinition = ((DBWServiceGraphQL) instance).getTypeDefinition(); + if (typeDefinition != null) { + log.debug("Adding web service '" + wsd.getId() + "' GQL schema"); + parsedSchema.merge(typeDefinition); + } + } catch (DBWebException e) { + log.warn("Error obtaining web service '" + wsd.getId() + "' type definitions", e); + } + } + } + + SchemaGenerator schemaGenerator = new SchemaGenerator(); + GraphQLModel wiring = new GraphQLModel(); + return schemaGenerator.makeExecutableSchema(parsedSchema, wiring.buildRuntimeWiring()); + } + + @Override + protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + setDevelHeaders(request, response); + } + + private void setDevelHeaders(HttpServletRequest request, HttpServletResponse response) { + if (CloudbeaverApplication.getInstance().isDevelMode()) { + // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, "*"); + // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "*"); + // response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "*"); + + String referrer = request.getHeader("referer"); + + try { + URL url = new URL(referrer); + String protocol = url.getProtocol(); + String host = url.getHost(); + int port = url.getPort(); + + String origin; + + // if the port is not explicitly specified in the input, it will be -1. + if (port == -1) { + origin = String.format("%s://%s", protocol, host); + } else { + origin = String.format("%s://%s:%d", protocol, host, port); + } + + // for local machine must be defined explicitly: + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_ORIGIN, origin); + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_HEADERS, "Set-Cookie, Content-Type"); + response.setHeader(HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, "true"); + } catch (Throwable t) {} + } + } + + @Override + protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String postBody = IOUtils.readToString(request.getReader()); + JsonElement json = gson.fromJson(postBody, JsonElement.class); + if (json instanceof JsonArray) { + setDevelHeaders(request, response); + response.setContentType(GraphQLConstants.CONTENT_TYPE_JSON_UTF8); + response.getWriter().print("[\n"); + + JsonArray array = (JsonArray)json; + int reqCount = 0; + for (int i = 0; i < array.size(); i++) { + if (reqCount > 0) { + response.getWriter().print(",\n"); + } + JsonElement item = array.get(i); + if (item instanceof JsonObject) { + executeSingleQuery(request, response, (JsonObject) item); + reqCount++; + } + } + + response.getWriter().print("\n]"); + } else if (json instanceof JsonObject) { + JsonObject reqObject = (JsonObject) json; + executeSingleQuery(request, response, reqObject); + } else { + response.sendError(400, "Bad JSON request"); + } + } + + private void executeSingleQuery(HttpServletRequest request, HttpServletResponse response, JsonObject reqObject) throws IOException { + JsonElement query = reqObject.get("query"); + if (query == null) { + response.sendError(400, "Query not specified"); + return; + } + JsonElement varJSON = reqObject.get("variables"); + Map variables = varJSON == null ? null : gson.fromJson(varJSON, Map.class); + + JsonElement operNameJSON = reqObject.get("operationName"); + + executeQuery(request, response, query.getAsString(), variables, operNameJSON == null || operNameJSON instanceof JsonNull ? null : operNameJSON.getAsString()); + } + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String path = request.getPathInfo(); + if (path == null) { + path = request.getServletPath(); + } + boolean develMode = CloudbeaverApplication.getInstance().isDevelMode(); + + if (path.contentEquals("/schema.json") && develMode) { + executeQuery(request, response, GraphQLConstants.SCHEMA_READ_QUERY, null, null); + } else if (path.contentEquals("/console") && develMode) { + try (InputStream consolePageStream = WebServiceUtils.openStaticResource("static/graphiql/index.html")) { + IOUtils.copyStream(consolePageStream, response.getOutputStream()); + } + } else { + String query = request.getParameter("query"); + if (query != null) { + executeQuery(request, response, query, null, request.getParameter("operationName")); + } else { + response.sendError(400, "Bad GET request"); + } + } + } + + private void executeQuery(HttpServletRequest request, HttpServletResponse response, String query, Map variables, String operationName) throws IOException { + GraphQLContext context = new GraphQLContext.Builder() + .of("request", request) + .of("response", response) + .build(); + ExecutionInput.Builder contextBuilder = ExecutionInput.newExecutionInput() + .context(context) + .query(query); + if (variables != null) { + contextBuilder.variables(variables); + } + if (operationName != null) { + contextBuilder.operationName(operationName); + } + + ExecutionInput executionInput = contextBuilder.build(); + ExecutionResult executionResult = graphQL.execute(executionInput); + + Map resJSON = executionResult.toSpecification(); + String resString = gson.toJson(resJSON); + setDevelHeaders(request, response); + response.setContentType(GraphQLConstants.CONTENT_TYPE_JSON_UTF8); + response.getWriter().print(resString); + } + + private class WebInstrumentation extends SimpleInstrumentation { + @Override + public CompletableFuture instrumentExecutionResult(ExecutionResult executionResult, InstrumentationExecutionParameters parameters) { + return super.instrumentExecutionResult(executionResult, parameters); + } + + @Override + public DataFetcher instrumentDataFetcher(DataFetcher dataFetcher, InstrumentationFieldFetchParameters parameters) { + return dataFetcher; +// return environment -> { +// try { +// return dataFetcher.get(environment); +// } catch (Exception e) { +// log.debug(e); +// throw e; +// } +// }; + } + } + + private class WebExecutionStrategy extends AsyncExecutionStrategy { + public WebExecutionStrategy() { + super(handlerParameters -> { + Throwable exception = handlerParameters.getException(); + log.debug(exception); + + SourceLocation sourceLocation = handlerParameters.getSourceLocation(); + ExecutionPath path = handlerParameters.getPath(); + + DataFetcherExceptionHandlerResult.Builder handlerResult = DataFetcherExceptionHandlerResult.newResult(); + if (exception instanceof GraphQLError) { + if (exception instanceof DBWebException) { + ((DBWebException) exception).setPath(path.toList()); + ((DBWebException) exception).setLocations(Collections.singletonList(sourceLocation)); + } + return handlerResult.error((GraphQLError) exception).build(); + } else { + return handlerResult.error(new ExceptionWhileDataFetching(path, exception, sourceLocation)).build(); + } + }); + } + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLModel.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLModel.java new file mode 100644 index 0000000000..eb20baa5af --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/graphql/GraphQLModel.java @@ -0,0 +1,204 @@ +package io.cloudbeaver.server.graphql; + +import graphql.scalars.ExtendedScalars; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.TypeRuntimeWiring; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.api.DBWModel; +import io.cloudbeaver.api.DBWService; +import io.cloudbeaver.api.DBWServiceGraphQL; +import io.cloudbeaver.api.DBWUtils; +import io.cloudbeaver.server.CloudbeaverPlatform; +import io.cloudbeaver.server.model.WebDatabaseObjectInfo; +import io.cloudbeaver.server.model.WebPropertyFilter; +import io.cloudbeaver.server.model.session.WebSessionManager; +import io.cloudbeaver.server.model.sql.WebSQLDataFilter; +import io.cloudbeaver.server.registry.WebServiceDescriptor; +import io.cloudbeaver.server.registry.WebServiceRegistry; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +class GraphQLModel implements DBWModel { + + private static final Log log = Log.getLog(GraphQLModel.class); + + private TypeRuntimeWiring.Builder queryType; + private TypeRuntimeWiring.Builder mutationType; + private WebSessionManager sessionManager; + + GraphQLModel() { + } + + @Override + public CloudbeaverPlatform getPlatform() { + return DBWorkbench.getPlatform(CloudbeaverPlatform.class); + } + + @Override + public WebSessionManager getSessionManager() { + return sessionManager; + } + + @Override + public TypeRuntimeWiring.Builder getQueryType() { + return queryType; + } + + @Override + public TypeRuntimeWiring.Builder getMutationType() { + return mutationType; + } + + RuntimeWiring buildRuntimeWiring() { + + CloudbeaverPlatform webServiceMain = getPlatform(); + sessionManager = webServiceMain.getSessionManager(); + + RuntimeWiring.Builder builder = RuntimeWiring.newRuntimeWiring(); + builder + .scalar(ExtendedScalars.DateTime) + .scalar(ExtendedScalars.Object); + queryType = TypeRuntimeWiring.newTypeWiring("Query"); + mutationType = TypeRuntimeWiring.newTypeWiring("Mutation"); + + queryType + .dataFetcher("serverConfig", env -> webServiceMain.getServerConfig()) + + .dataFetcher("driverList", env -> sessionManager.getWebSession(getServletRequest(env)).getDriverList(env.getArgument("id"))) + .dataFetcher("dataSourceList", env -> webServiceMain.getGlobalDataSources()) + + .dataFetcher("sessionState", env -> sessionManager.getWebSession(getServletRequest(env))) + + .dataFetcher("readSessionLog", env -> sessionManager.getWebSession(getServletRequest(env)).readLog( + env.getArgument("maxEntries"), + env.getArgument("clearEntries"))) + + .dataFetcher("navNodeChildren", env -> sessionManager.getWebSession(getServletRequest(env)).getNavigatorNodeChildren( + env.getArgument("parentPath"), env.getArgument("offset"), env.getArgument("limit"), env.getArgument("onlyFolders"))) + .dataFetcher("navNodeInfo", env -> sessionManager.getWebSession(getServletRequest(env)).getNavigatorNodeInfo( + env.getArgument("nodePath"))) + .dataFetcher("navRefreshNode", env -> sessionManager.getWebSession(getServletRequest(env)).refreshNavigatorNode( + env.getArgument("nodePath") + )) + + .dataFetcher("navGetStructContainers", env -> DBWUtils.getSQLProcessor(sessionManager, env).getStructContainers( + env.getArgument("catalog") + )) + + .dataFetcher("sqlDialectInfo", env -> + DBWUtils.getSQLProcessor(sessionManager, env).getDialectInfo() + ) + .dataFetcher("sqlCompletionProposals", env -> + DBWUtils.getSQLContext(sessionManager, env).getCompletionProposals( + env.getArgument("query"), + env.getArgument("position"), + env.getArgument("maxResults") + ) + ) + ; + + mutationType + .dataFetcher("openSession", env -> sessionManager.openSession(getServletRequest(env), false)) + .dataFetcher("closeSession", env -> sessionManager.closeSession(getServletRequest(env))) + .dataFetcher("touchSession", env -> sessionManager.touchSession(getServletRequest(env))) + .dataFetcher("changeSessionLanguage", env -> { + sessionManager.getWebSession(getServletRequest(env)).setLocale(env.getArgument("locale")); + return true; + }) + + .dataFetcher("openConnection", env -> sessionManager.openConnection(getServletRequest(env), env.getArgument("config"))) + .dataFetcher("createConnection", env -> sessionManager.createConnection(getServletRequest(env), env.getArgument("config"))) + .dataFetcher("testConnection", env -> sessionManager.testConnection(getServletRequest(env), env.getArgument("config"))) + .dataFetcher("closeConnection", env -> sessionManager.closeConnection(getServletRequest(env), env.getArgument("id"))) + + .dataFetcher("sqlContextCreate", env -> DBWUtils.getSQLProcessor(sessionManager, env).createContext(env.getArgument("defaultCatalog"), env.getArgument("defaultSchema"))) + .dataFetcher("sqlContextDestroy", env -> { DBWUtils.getSQLContext(sessionManager, env).destroy(); return true; } ) + .dataFetcher("sqlContextSetDefaults", env -> { DBWUtils.getSQLContext(sessionManager, env).setDefaults(env.getArgument("defaultCatalog"), env.getArgument("defaultSchema")); return true; }) + + .dataFetcher("sqlExecuteQuery", env -> + DBWUtils.getSQLContext(sessionManager, env).executeQuery( + env.getArgument("sql"), getDataFilter(env) + )) + .dataFetcher("sqlResultClose", env -> + DBWUtils.getSQLContext(sessionManager, env).closeResult(env.getArgument("resultId"))) + + .dataFetcher("readDataFromContainer", env -> + DBWUtils.getSQLProcessor(sessionManager, env).readDataFromContainer( + DBWUtils.getSQLContext(sessionManager, env), + env.getArgument("containerNodePath"), getDataFilter(env) + )) + .dataFetcher("updateResultsData", env -> + DBWUtils.getSQLProcessor(sessionManager, env).updateResultsData( + DBWUtils.getSQLContext(sessionManager, env), + env.getArgument("resultsId"), + env.getArgument("updateRow"), + env.getArgument("updateValues") + )) + .dataFetcher("asyncSqlExecuteQuery", env -> + DBWUtils.getSQLContext(sessionManager, env).asyncExecuteQuery( + env.getArgument("sql"), getDataFilter(env) + )) + .dataFetcher("asyncTaskStatus", env -> + sessionManager.getWebSession(getServletRequest(env)).asyncTaskStatus( + env.getArgument("id")) + ) + .dataFetcher("asyncTaskCancel", env -> + sessionManager.getWebSession(getServletRequest(env)).asyncTaskCancel( + env.getArgument("id")) + ) + ; + + // Extend queries and mutations + for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) { + DBWService instance; + try { + instance = wsd.getInstance(); + } catch (Exception e) { + log.error(e); + continue; + } + if (instance instanceof DBWServiceGraphQL) { + try { + ((DBWServiceGraphQL) instance).bindWiring(this); + } catch (DBWebException e) { + log.warn("Error obtaining web service '" + wsd.getId() + "' type definitions", e); + } + } + } + + builder.type(queryType); + builder.type(mutationType); + + builder.type(TypeRuntimeWiring.newTypeWiring("DatabaseObjectInfo") + .dataFetcher("properties", env -> { + Map filterProps = env.getArgument("filter"); + WebPropertyFilter filter = filterProps == null ? null : new WebPropertyFilter(filterProps); + return ((WebDatabaseObjectInfo)env.getSource()).filterProperties(filter); + }) + ); + builder.type(TypeRuntimeWiring.newTypeWiring("AsyncTaskResult").typeResolver(env -> { + return env.getObject(); + }) + ); + + return builder.build(); + } + + private HttpServletRequest getServletRequest(DataFetchingEnvironment env) { + return DBWUtils.getServletRequest(env); + } + + /////////////////////////////////////// + // Helpers + + private static WebSQLDataFilter getDataFilter(DataFetchingEnvironment env) { + Map filterProps = env.getArgument("filter"); + return filterProps == null ? null : new WebSQLDataFilter(filterProps); + } + + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverImageServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverImageServlet.java new file mode 100644 index 0000000000..c5b5a6a848 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverImageServlet.java @@ -0,0 +1,77 @@ +package io.cloudbeaver.server.jetty; + +import org.eclipse.core.runtime.FileLocator; +import org.jkiss.dbeaver.Log; +import org.jkiss.utils.CommonUtils; +import org.jkiss.utils.IOUtils; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; + +public class CloudbeaverImageServlet extends HttpServlet { + + private static final Log log = Log.getLog(CloudbeaverImageServlet.class); + + public CloudbeaverImageServlet() { + + } + + @Override + protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + String iconId = request.getPathInfo(); + while (iconId.startsWith("/")) { + iconId = iconId.substring(1); + } + if (CommonUtils.isEmpty(iconId)) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Image ID missing"); + return; + } + if (!iconId.startsWith("platform:/plugin")) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad image ID"); + return; + } + int divPos = iconId.lastIndexOf("."); + if (divPos == -1) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad image extension"); + return; + } + String iconPath = iconId.substring(0, divPos); + String iconExt = iconId.substring(divPos + 1); + try { + String bigIconPath = iconPath + "@2x." + iconExt; + URL iconURL = FileLocator.find(new URL(bigIconPath)); + if (iconURL == null) { + iconURL = FileLocator.find(new URL(iconId)); + } + if (iconURL == null) { + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Image not found"); + return; + } + + response.setContentType("image/" + iconExt); + setExpireTime(response, 60 * 60 * 24 * 30); // Month + try (InputStream is = new BufferedInputStream(iconURL.openStream())) { + IOUtils.copyStream(is, response.getOutputStream()); + } + } catch (Exception e) { + log.error(e); + response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Error reading image " + iconId + ": " + e.getMessage()); + return; + } + } + + private void setExpireTime(HttpServletResponse response, long expiresSeconds) { + // Http 1.0 header, set a fix expires date. + response.setDateHeader("Expires", System.currentTimeMillis() + expiresSeconds * 1000); + // Http 1.1 header, set a time after now. + response.setHeader("Cache-Control", "public, max-age=" + expiresSeconds); + } + + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverJettyServer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverJettyServer.java new file mode 100644 index 0000000000..8d936ee18f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverJettyServer.java @@ -0,0 +1,90 @@ +package io.cloudbeaver.server.jetty; + +import io.cloudbeaver.server.CloudbeaverApplication; +import io.cloudbeaver.server.graphql.GraphQLEndpoint; +import org.eclipse.jetty.server.ConnectionFactory; +import org.eclipse.jetty.server.Connector; +import org.eclipse.jetty.server.HttpConnectionFactory; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.session.DefaultSessionCache; +import org.eclipse.jetty.server.session.FileSessionDataStore; +import org.eclipse.jetty.server.session.SessionHandler; +import org.eclipse.jetty.servlet.ErrorPageErrorHandler; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.utils.GeneralUtils; + +import java.io.File; + +public class CloudbeaverJettyServer { + + private static final Log log = Log.getLog(CloudbeaverJettyServer.class); + private static final String SESSION_CACHE_DIR = ".http-sessions"; + + public CloudbeaverJettyServer() { + } + + public void runServer() { + CloudbeaverApplication application = CloudbeaverApplication.getInstance(); + try { + Server server = new Server(application.getServerPort()); + + { + // Handler configuration + ServletContextHandler servletContextHandler = new ServletContextHandler(ServletContextHandler.SESSIONS); + servletContextHandler.setResourceBase(application.getContentRoot()); + servletContextHandler.setContextPath(application.getRootURI()); + servletContextHandler.addServlet(CloudbeaverStaticServlet.class, application.getRootURI()); + servletContextHandler.addServlet(CloudbeaverImageServlet.class, application.getServicesURI() + "images/*"); + servletContextHandler.addServlet(GraphQLEndpoint.class, application.getServicesURI() + "gql/*"); + servletContextHandler.addEventListener(new CloudbeaverServerContextListener()); + initSessionManager(servletContextHandler); + + server.setHandler(servletContextHandler); + + ErrorPageErrorHandler errorHandler = new ErrorPageErrorHandler(); + //errorHandler.addErrorPage(404, "/missing.html"); + servletContextHandler.setErrorHandler(errorHandler); + } + + { + // HTTP config + for(Connector y : server.getConnectors()) { + for(ConnectionFactory x : y.getConnectionFactories()) { + if(x instanceof HttpConnectionFactory) { + ((HttpConnectionFactory)x).getHttpConfiguration().setSendServerVersion(false); + } + } + } + } + + server.start(); + server.join(); + } catch (Exception e) { + log.error("Error running Jetty server", e); + } + } + + private void initSessionManager(ServletContextHandler servletContextHandler) { + // Init sessions persistence + File metadataFolder = GeneralUtils.getMetadataFolder(DBWorkbench.getPlatform().getWorkspace().getAbsolutePath()); + File sessionCacheFolder = new File(metadataFolder, SESSION_CACHE_DIR); + if (!sessionCacheFolder.exists()) { + if (!sessionCacheFolder.mkdirs()) { + log.error("Can't create http session cache directory '" + sessionCacheFolder.getAbsolutePath() + "'"); + return; + } + } + + SessionHandler sessionHandler = new SessionHandler(); + DefaultSessionCache sessionCache = new DefaultSessionCache(sessionHandler); + FileSessionDataStore sessionStore = new FileSessionDataStore(); + + sessionStore.setStoreDir(sessionCacheFolder); + sessionCache.setSessionDataStore(sessionStore); + sessionHandler.setSessionCache(sessionCache); + servletContextHandler.setSessionHandler(sessionHandler); + } + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverServerContextListener.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverServerContextListener.java new file mode 100644 index 0000000000..fa3fd02df7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverServerContextListener.java @@ -0,0 +1,44 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.jetty; + +import io.cloudbeaver.server.CloudbeaverApplication; + +import javax.servlet.ServletContextEvent; +import javax.servlet.ServletContextListener; +import javax.servlet.SessionCookieConfig; + +public class CloudbeaverServerContextListener implements ServletContextListener { + + public static final String DBEAVER_SESSION_COOKIE_NAME = "DBEAVER_SESSION_ID"; + + public void contextInitialized(ServletContextEvent sce) { + SessionCookieConfig scf = sce.getServletContext().getSessionCookieConfig(); + + scf.setComment("Cloudbeaver Session ID"); + //scf.setDomain(domain); + //scf.setHttpOnly(httpOnly); + //scf.setMaxAge(maxAge); + scf.setPath(CloudbeaverApplication.getInstance().getRootURI()); + //scf.setSecure(isSecure); + scf.setName(DBEAVER_SESSION_COOKIE_NAME); + } + + public void contextDestroyed(ServletContextEvent sce) { + + } +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverStaticServlet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverStaticServlet.java new file mode 100644 index 0000000000..1cc2e65cc5 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/jetty/CloudbeaverStaticServlet.java @@ -0,0 +1,22 @@ +package io.cloudbeaver.server.jetty; + +import org.eclipse.jetty.server.ResourceService; +import org.eclipse.jetty.servlet.DefaultServlet; + +import javax.servlet.annotation.WebServlet; + +@WebServlet(urlPatterns = "/") +public class CloudbeaverStaticServlet extends DefaultServlet { + + public CloudbeaverStaticServlet() { +// super(makeResourceService()); + super(); + } + + private static ResourceService makeResourceService() { + ResourceService resourceService = new ResourceService(); + return resourceService; + } + + +} \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CloudbeaverLauncher.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CloudbeaverLauncher.java new file mode 100644 index 0000000000..edbfc40993 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/launcher/CloudbeaverLauncher.java @@ -0,0 +1,55 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.launcher; + +import org.eclipse.core.runtime.adaptor.EclipseStarter; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceReference; + +import java.util.HashMap; +import java.util.Map; + +/** + * The activator class controls the plug-in life cycle + */ +public class CloudbeaverLauncher { + + public static DBPPlatform instantiatePlatform() throws Exception { + Map props = new HashMap<>(); + + EclipseStarter.setInitialProperties(props); + BundleContext context = EclipseStarter.startup( + new String[]{}, + null); + + ServiceReference platformRef = context.getServiceReference(DBPPlatform.class); + return context.getService(platformRef); + } + + public static void main(String[] args) throws Exception { + + Map props = new HashMap<>(); + + EclipseStarter.setInitialProperties(props); + BundleContext context = EclipseStarter.startup( + new String[]{}, + null); + //context.getBundles() + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebAsyncTaskInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebAsyncTaskInfo.java new file mode 100644 index 0000000000..59ce025f98 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebAsyncTaskInfo.java @@ -0,0 +1,100 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import org.jkiss.dbeaver.model.runtime.AbstractJob; + +/** + * Web connection info + */ +public class WebAsyncTaskInfo { + + private String id; + private String name; + private boolean running; + private Object result; + private String status; + private Throwable jobError; + + private AbstractJob job; + + public WebAsyncTaskInfo(String id, String name) { + this.id = id; + this.name = name; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public boolean isRunning() { + return running; + } + + public void setRunning(boolean running) { + this.running = running; + } + + public Object getResult() { + return result; + } + + public void setResult(Object result) { + this.result = result; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public WebServerError getError() { + return jobError == null ? null : new WebServerError(jobError); + } + + public Throwable getJobError() { + return jobError; + } + + public void setJobError(Throwable jobError) { + this.jobError = jobError; + } + + public AbstractJob getJob() { + return job; + } + + public void setJob(AbstractJob job) { + this.job = job; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebBasicObjectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebBasicObjectInfo.java new file mode 100644 index 0000000000..d48ca77ebb --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebBasicObjectInfo.java @@ -0,0 +1,66 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web connection info + */ +public class WebBasicObjectInfo { + + private String displayName; + private String description; + private long id; + private String value; + + @Property + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + @Property + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } + + @Property + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + @Property + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionConfig.java new file mode 100644 index 0000000000..547f2ab860 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionConfig.java @@ -0,0 +1,126 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import org.jkiss.dbeaver.model.data.json.JSONUtils; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.utils.CommonUtils; + +import java.util.Map; + +/** + * Web connection config + */ +public class WebConnectionConfig { + + private String dataSourceId; + + private String driverId; + private String host; + private String port; + private String serverName; + private String databaseName; + private String url; + + private String name; + private String description; + private Map properties; + private String userName; + private String userPassword; + + public WebConnectionConfig() { + } + + public WebConnectionConfig(Map params) { + dataSourceId = CommonUtils.toString(params.get("dataSourceId")); + + driverId = JSONUtils.getString(params, "driverId"); + host = JSONUtils.getString(params, "host"); + port = JSONUtils.getString(params, "port"); + serverName = JSONUtils.getString(params, "serverName"); + databaseName = JSONUtils.getString(params, "databaseName"); + url = JSONUtils.getString(params, "url"); + + name = JSONUtils.getString(params, "name"); + description = JSONUtils.getString(params, "description"); + + properties = JSONUtils.getObject(params, "properties"); + userName = JSONUtils.getString(params, "userName"); + userPassword = JSONUtils.getString(params, "userPassword"); + } + + @Property + public String getDataSourceId() { + return dataSourceId; + } + + @Property + public String getDriverId() { + return driverId; + } + + @Property + public String getName() { + return name; + } + + @Property + public String getDescription() { + return description; + } + + @Property + public String getHost() { + return host; + } + + @Property + public String getPort() { + return port; + } + + @Property + public String getServerName() { + return serverName; + } + + @Property + public String getDatabaseName() { + return databaseName; + } + + @Property + public String getUrl() { + return url; + } + + @Property + public Map getProperties() { + return properties; + } + + @Property + public String getUserName() { + return userName; + } + + @Property + public String getUserPassword() { + return userPassword; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionInfo.java new file mode 100644 index 0000000000..01a6eeeb00 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebConnectionInfo.java @@ -0,0 +1,138 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.server.CloudbeaverConstants; +import io.cloudbeaver.server.model.session.WebSession; +import io.cloudbeaver.server.model.sql.WebSQLProcessor; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web connection info + */ +public class WebConnectionInfo { + + private final WebSession session; + private DBPDataSourceContainer dataSourceContainer; + private WebServerError connectError; + private WebSQLProcessor sqlProcessor; + + private String connectTime; + private String serverVersion; + private String clientVersion; + + public WebConnectionInfo(WebSession session, DBPDataSourceContainer ds) { + this.session = session; + this.dataSourceContainer = ds; + } + + public DBPDataSourceContainer getDataSourceContainer() { + return dataSourceContainer; + } + + public DBPDataSource getDataSource() { + return dataSourceContainer.getDataSource(); + } + + @Property + public String getId() { + return dataSourceContainer.getId(); + } + + @Property + public String getDriverId() { + return WebServiceUtils.makeDriverFullId(dataSourceContainer.getDriver()); + } + + @Property + public String getName() { + return dataSourceContainer.getName(); + } + + @Property + public String getDescription() { + return dataSourceContainer.getDescription(); + } + + @Property + public String getProperties() { + return null; + } + + @Property + public boolean isConnected() { + return dataSourceContainer.isConnected(); + } + + @Property + public boolean isProvided() { + return dataSourceContainer.isProvided(); + } + + @Property + public String getConnectTime() { + return CloudbeaverConstants.ISO_DATE_FORMAT.format(dataSourceContainer.getConnectTime()); + } + + @Property + public WebServerError getConnectError() { + return connectError; + } + + public void setConnectError(Throwable connectError) { + this.connectError = connectError == null ? null : new WebServerError(connectError); + } + + public void setConnectTime(String connectTime) { + this.connectTime = connectTime; + } + + public String getServerVersion() { + return serverVersion; + } + + public void setServerVersion(String serverVersion) { + this.serverVersion = serverVersion; + } + + public String getClientVersion() { + return clientVersion; + } + + public void setClientVersion(String clientVersion) { + this.clientVersion = clientVersion; + } + + public WebSQLProcessor getSQLProcessor() throws DBWebException { + if (getDataSource() == null) { + try { + dataSourceContainer.connect(session.getProgressMonitor(), true, false); + } catch (DBException e) { + throw new DBWebException("Error connecting to database", e); + } + } + if (sqlProcessor == null) { + sqlProcessor = new WebSQLProcessor(session, this); + } + return sqlProcessor; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDataSourceConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDataSourceConfig.java new file mode 100644 index 0000000000..31db39f69f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDataSourceConfig.java @@ -0,0 +1,93 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.WebServiceUtils; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web data source configuration + */ +public class WebDataSourceConfig { + + private String id; + private String driverId; + private String name; + private String description; + + private String host; + private String server; + private String port; + private String url; + private String properties; + + public WebDataSourceConfig(DBPDataSourceContainer ds) { + this.id = ds.getId(); + this.name = ds.getName(); + this.description = ds.getDescription(); + this.driverId = WebServiceUtils.makeDriverFullId(ds.getDriver()); + this.host = ds.getConnectionConfiguration().getHostName(); + this.port = ds.getConnectionConfiguration().getHostPort(); + this.url = ds.getConnectionConfiguration().getUrl(); + } + + @Property + public String getId() { + return id; + } + + @Property + public String getDriverId() { + return driverId; + } + + @Property + public String getName() { + return name; + } + + @Property + public String getDescription() { + return description; + } + + @Property + public String getHost() { + return host; + } + + @Property + public String getServer() { + return server; + } + + @Property + public String getPort() { + return port; + } + + @Property + public String getUrl() { + return url; + } + + @Property + public String getProperties() { + return properties; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseDriverConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseDriverConfig.java new file mode 100644 index 0000000000..db15bfa7cb --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseDriverConfig.java @@ -0,0 +1,175 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.runtime.properties.PropertySourceCustom; + +import java.util.Map; + +/** + * Web driver configuration + */ +public class WebDatabaseDriverConfig { + + private final WebSession webSession; + private final DBPDriver driver; + private String id; + + public WebDatabaseDriverConfig(WebSession webSession, DBPDriver driver) { + this.webSession = webSession; + this.driver = driver; + this.id = WebServiceUtils.makeDriverFullId(driver); + } + + @Property + public String getId() { + return id; + } + + @Property + public String getName() { + return driver.getName(); + } + + @Property + public String getDescription() { + return driver.getDescription(); + } + + @Property + public String getIcon() { + return WebServiceUtils.makeIconId(driver.getIcon()); + } + + @Property + public String getIconBig() { + return WebServiceUtils.makeIconId(driver.getIconBig()); + } + + @Property + public String getProviderId() { + return driver.getProviderId(); + } + + @Property + public String getDriverClassName() { + return driver.getDriverClassName(); + } + + @Property + public String getDefaultPort() { + return driver.getDefaultPort(); + } + + @Property + public String getSampleURL() { + return driver.getSampleURL(); + } + + @Property + public String getDriverInfoURL() { + return driver.getWebURL(); + } + + @Property + public String getDriverPropertiesURL() { + return driver.getPropertiesWebURL(); + } + + @Property + public boolean getEmbedded() { + return driver.isEmbedded(); + } + + @Property + public boolean getAnonymousAccess() { + return driver.isAnonymousAccess(); + } + + @Property + public boolean getAllowsEmptyPassword() { + return driver.isAllowsEmptyPassword(); + } + + @Property + public boolean getLicenseRequired() { + return driver.isLicenseRequired(); + } + + @Property + public String getLicense() { + return driver.getLicense(); + } + + @Property + public boolean getCustom() { + return driver.isCustom(); + } + + @Property + public int getPromotedScore() { + return driver.getPromotedScore(); + } + + @Property + public Map getConnectionProperties() { + return driver.getConnectionProperties(); + } + + @Property + public Map getDefaultConnectionProperties() { + return driver.getDefaultConnectionProperties(); + } + + @Property + public WebPropertyInfo[] getDriverProperties() throws DBWebException { + try { + DBPConnectionConfiguration cfg = new DBPConnectionConfiguration(); + cfg.setUrl(driver.getSampleURL()); + cfg.setHostName("localhost"); + cfg.setHostPort(driver.getDefaultPort()); + cfg.setUrl(driver.getDataSourceProvider().getConnectionURL(driver, cfg)); + DBPPropertyDescriptor[] properties = driver.getDataSourceProvider().getConnectionProperties(webSession.getProgressMonitor(), driver, cfg); + + PropertySourceCustom propertySource = new PropertySourceCustom( + properties, + cfg.getProperties()); + + WebPropertyInfo[] result = new WebPropertyInfo[properties.length]; + for (int i = 0; i < properties.length; i++) { + result[i] = new WebPropertyInfo(webSession, properties[i], propertySource); + } + return result; + } catch (DBException e) { + throw new DBWebException("Error reading driver properties", e); + } + } + + @Property + public Map getDriverParameters() { + return driver.getDriverParameters(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseObjectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseObjectInfo.java new file mode 100644 index 0000000000..0894d4365a --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebDatabaseObjectInfo.java @@ -0,0 +1,147 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.*; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.struct.*; +import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; +import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.dbeaver.runtime.properties.PropertyCollector; +import org.jkiss.utils.CommonUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Web connection info + */ +public class WebDatabaseObjectInfo { + + private final WebSession session; + private final DBSObject object; + + public WebDatabaseObjectInfo(WebSession session, DBSObject object) { + this.session = session; + this.object = object; + } + + /////////////////////////////////// + // General properties + /////////////////////////////////// + + @Property + public String getName() { + return object.getName(); + } + + @Property + public String getDescription() { + return object.getDescription(); + } + + @Property + public String getType() { + return object.getClass().getName(); + } + + @Property + public WebPropertyInfo[] getProperties() { + return filterProperties(null); + } + + @Property + public WebPropertyInfo[] filterProperties(@Nullable WebPropertyFilter filter) { + PropertyCollector propertyCollector = new PropertyCollector(object, true); + propertyCollector.setLocale(session.getLocale()); + propertyCollector.collectProperties(); + List webProps = new ArrayList<>(); + for (DBPPropertyDescriptor prop : propertyCollector.getProperties()) { + if (filter != null && !CommonUtils.isEmpty(filter.getIds()) && !filter.getIds().contains(CommonUtils.toString(prop.getId()))) { + continue; + } + WebPropertyInfo webProperty = new WebPropertyInfo(session, prop, propertyCollector); + if (filter != null) { + if (!CommonUtils.isEmpty(filter.getFeatures()) && !webProperty.hasAnyFeature(filter.getFeatures())) { + continue; + } + if (!CommonUtils.isEmpty(filter.getCategories()) && !filter.getCategories().contains(webProperty.getCategory())) { + continue; + } + } + webProps.add(webProperty); + } + return webProps.toArray(new WebPropertyInfo[0]); + } + + /////////////////////////////////// + // Advanced + + @Property + public Integer getOrdinalPosition() { + return object instanceof DBPOrderedObject ? ((DBPOrderedObject) object).getOrdinalPosition() : null; + } + + @Property + public String getFullyQualifiedName() { + return object instanceof DBPQualifiedObject ? ((DBPQualifiedObject) object).getFullyQualifiedName(DBPEvaluationContext.UI) : getName(); + } + + @Property + public String getOverloadedName() { + return object instanceof DBPOverloadedObject ? ((DBPOverloadedObject) object).getOverloadedName() : getName(); + } + + @Property + public String getUniqueName() { + return object instanceof DBPUniqueObject ? ((DBPUniqueObject) object).getUniqueName() : getName(); + } + + @Property + public String getState() { + if (object instanceof DBPStatefulObject) { + DBSObjectState state = ((DBPStatefulObject) object).getObjectState(); + if (state != null) { + return state.getTitle(); + } + } + return null; + } + + @Property + public String[] getFeatures() { + List features = new ArrayList<>(); + if (object instanceof DBPScriptObject) features.add("script"); + if (object instanceof DBPScriptObjectExt) features.add("scriptExtended"); + if (object instanceof DBSDataContainer) features.add("dataContainer"); + if (object instanceof DBSDataManipulator) features.add("dataManipulator"); + if (object instanceof DBSEntity) features.add("entity"); + if (object instanceof DBSSchema) features.add("schema"); + if (object instanceof DBSCatalog) features.add("catalog"); + if (object instanceof DBPDataSourceContainer) features.add("dataSource"); + return features.toArray(new String[0]); + } + + @Property + public String[] getEditors() { + return null; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebNavigatorNodeInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebNavigatorNodeInfo.java new file mode 100644 index 0000000000..9413c44e27 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebNavigatorNodeInfo.java @@ -0,0 +1,134 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.dbeaver.model.meta.Association; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSObject; +import org.jkiss.dbeaver.model.struct.rdb.DBSProcedure; + +import java.util.ArrayList; +import java.util.List; + +/** + * Web connection info + */ +public class WebNavigatorNodeInfo { + private final WebSession sesion; + private final DBNNode node; + + public WebNavigatorNodeInfo(WebSession sesion, DBNNode node) { + this.sesion = sesion; + this.node = node; + } + + public DBNNode getNode() { + return node; + } + + /////////////////////////////////// + // General properties + /////////////////////////////////// + + @Property + public String getId() { + return node.getNodeItemPath(); + } + + @Property + public String getName() { + return node.getLocalizedName(sesion.getLocale()); + } + + @Property + public String getIcon() { + return node.getNodeIconDefault().getLocation(); + } + + @Property + public String getDescription() { + return node.getNodeDescription(); + } + + @Property + public String getNodeType() { + return node.getNodeType(); + } + + @Property + public boolean isFolder() { + return node instanceof DBNContainer && !(node instanceof DBNDataSource); + } + + @Property + public boolean isInline() { + return node instanceof DBNDatabaseNode && ((DBNDatabaseNode) node).getMeta().isInline(); + } + + @Property + public boolean isNavigable() { + if (node instanceof DBNDatabaseNode) { + DBNDatabaseNode databaseNode = (DBNDatabaseNode) this.node; + if (!databaseNode.getMeta().isNavigable()) { + return false; + } + } + return true; + } + + @Property + public boolean isHasChildren() { + return node.hasChildren(true); + } + + @Association + public String[] getFeatures() { + List features = new ArrayList<>(); + if (node instanceof DBNDatabaseItem) { + features.add("item"); + DBSObject object = ((DBNDatabaseItem) node).getObject(); + if (object instanceof DBSEntity || object instanceof DBSProcedure) { + features.add("leaf"); + } + + } + if (node instanceof DBNContainer) { + features.add("container"); + } + return features.toArray(new String[0]); + } + + /////////////////////////////////// + // Objects + /////////////////////////////////// + + @Property + public WebDatabaseObjectInfo getObject() { + if (node instanceof DBNDatabaseNode) { + return new WebDatabaseObjectInfo(sesion, ((DBNDatabaseNode) node).getObject()); + } + return null; + } + + @Override + public String toString() { + return node.getNodeItemPath(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyFilter.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyFilter.java new file mode 100644 index 0000000000..cc60f601c2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyFilter.java @@ -0,0 +1,71 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import java.util.List; +import java.util.Map; + +/** + * Web property filter + */ +public class WebPropertyFilter { + private List ids; + private List features; + private List categories; + private List dataTypes; + + public WebPropertyFilter() { + } + + public WebPropertyFilter(Map props) { + ids = (List) props.get("ids"); + features = (List) props.get("features"); + + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } + + public List getFeatures() { + return features; + } + + public void setFeatures(List features) { + this.features = features; + } + + public List getCategories() { + return categories; + } + + public void setCategories(List categories) { + this.categories = categories; + } + + public List getDataTypes() { + return dataTypes; + } + + public void setDataTypes(List dataTypes) { + this.dataTypes = dataTypes; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyInfo.java new file mode 100644 index 0000000000..906c0147c2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebPropertyInfo.java @@ -0,0 +1,237 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.*; +import org.jkiss.dbeaver.model.meta.IPropertyValueListProvider; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor; +import org.jkiss.dbeaver.model.preferences.DBPPropertySource; +import org.jkiss.dbeaver.runtime.properties.ObjectPropertyDescriptor; +import org.jkiss.utils.CommonUtils; + +import java.lang.reflect.Array; +import java.util.*; + +/** + * Web connection info + */ +public class WebPropertyInfo { + private WebSession session; + private DBPPropertyDescriptor property; + private DBPPropertySource propertySource; + + public WebPropertyInfo(WebSession session, DBPPropertyDescriptor property, DBPPropertySource propertySource) { + this.session = session; + this.property = property; + this.propertySource = propertySource; + } + + /////////////////////////////////// + // General properties + /////////////////////////////////// + + @Property + public String getId() { + return CommonUtils.toString(property.getId()); + } + + @Property + public String getDisplayName() { + if (property instanceof DBPNamedObjectLocalized) { + return ((DBPNamedObjectLocalized) property).getLocalizedName(session.getLocale()); + } else { + return property.getDisplayName(); + } + } + + @Property + public String getDescription() { + if (property instanceof DBPObjectWithDescriptionLocalized) { + return ((DBPObjectWithDescriptionLocalized) property).getLocalizedDescription(session.getLocale()); + } else { + return property.getDescription(); + } + } + + @Property + public String getCategory() { + return property.getCategory(); + } + + @Property + public String getType() { + return getDataType(); + } + + @Property + public String getDataType() { + Class dataType = property.getDataType(); + return dataType == null ? null : dataType.getSimpleName(); + } + + @Property + public Object getDefaultValue() throws DBException { + return getValue(); + } + + @Property + public Object getValue() throws DBException { + Object value = propertySource.getPropertyValue(session.getProgressMonitor(), property.getId()); + return value == null ? null : makePropertyValue(value); + } + + @Property + public Object[] getValidValues() { + if (property instanceof IPropertyValueListProvider) { + Object[] possibleValues = ((IPropertyValueListProvider) property).getPossibleValues(propertySource.getEditableValue()); + if (possibleValues != null) { + Object[] validValues = new Object[possibleValues.length]; + for (int i = 0; i < possibleValues.length; i++) { + validValues[i] = makePropertyValue(possibleValues[i]); + } + return validValues; + } + return null; + } + return null; + } + + @Property + public String[] getFeatures() { + List features = new ArrayList<>(); + if (property instanceof ObjectPropertyDescriptor) { + ObjectPropertyDescriptor opd = (ObjectPropertyDescriptor)property; + if (opd.isRequired()) features.add("required"); + if (opd.isSpecific()) features.add("specific"); + if (opd.isOptional()) features.add("optional"); + if (opd.isHidden()) features.add("hidden"); + if (opd.isRemote()) features.add("remote"); + + if (opd.isDateTime()) features.add("datetme"); + if (opd.isNumeric()) features.add("numeric"); + if (opd.isNameProperty()) features.add("name"); + + if (opd.isMultiLine()) features.add("multiline"); + if (opd.isExpensive()) features.add("expensive"); + if (opd.isEditPossible()) features.add("editPossible"); + if (opd.isLinkPossible()) features.add("linkPossible"); + if (opd.isViewable()) features.add("viewable"); + } + return features.toArray(new String[0]); + } + + public boolean hasFeature(@NotNull String feature) { + if (property instanceof ObjectPropertyDescriptor) { + switch (feature) { + case "required": + return property.isRequired(); + case "specific": + return ((ObjectPropertyDescriptor) property).isSpecific(); + case "optional": + return ((ObjectPropertyDescriptor) property).isOptional(); + case "hidden": + return ((ObjectPropertyDescriptor) property).isHidden(); + case "remote": + return property.isRemote(); + + case "datetme": + return ((ObjectPropertyDescriptor) property).isDateTime(); + case "numeric": + return ((ObjectPropertyDescriptor) property).isNumeric(); + case "name": + return ((ObjectPropertyDescriptor) property).isNameProperty(); + + case "multiline": + return ((ObjectPropertyDescriptor) property).isMultiLine(); + case "expensive": + return ((ObjectPropertyDescriptor) property).isExpensive(); + case "editPossible": + return ((ObjectPropertyDescriptor) property).isEditPossible(); + case "linkPossible": + return ((ObjectPropertyDescriptor) property).isLinkPossible(); + case "viewable": + return ((ObjectPropertyDescriptor) property).isViewable(); + } + } + return false; + } + + public boolean hasAnyFeature(@NotNull List features) { + for (String feature : features) { + if (hasFeature(feature)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return CommonUtils.toString(property.getId()); + } + + private Object makePropertyValue(Object value) { + if (value == null) { + return null; +// } else if (value instanceof DBSObject) { +// return new WebDatabaseObjectInfo(session, (DBSObject) value); + } else if (value instanceof DBPObject) { + Map basicInfo = new LinkedHashMap<>(); + //WebBasicObjectInfo objectInfo = new WebBasicObjectInfo(); + if (value instanceof DBPNamedObject) { + basicInfo.put("displayName", ((DBPNamedObject) value).getName()); + } else { + basicInfo.put("value", CommonUtils.toString(value)); + } + if (value instanceof DBPObjectWithDescription) { + String description = ((DBPObjectWithDescription) value).getDescription(); + if (!CommonUtils.isEmpty(description)) { + basicInfo.put("description", description); + } + } + if (value instanceof DBPObjectWithLongId) { + basicInfo.put("id", ((DBPObjectWithLongId) value).getObjectId()); + } + if (value instanceof DBPQualifiedObject) { + basicInfo.put("fullName", ((DBPQualifiedObject) value).getFullyQualifiedName(DBPEvaluationContext.UI)); + } + if (value instanceof DBPUniqueObject) { + basicInfo.put("uniqueName", ((DBPUniqueObject) value).getUniqueName()); + } + return basicInfo; + } else if (value instanceof Collection) { + List result = new ArrayList<>(); + for (Object item : (Collection)value) { + result.add(makePropertyValue(item)); + } + return result; + } else if (value.getClass().isArray()) { + List result = new ArrayList<>(); + int length = Array.getLength(value); + for (int i = 0; i < length; i++) { + result.add(Array.get(value, i)); + } + return result; + } + return CommonUtils.toString(value); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerConfig.java new file mode 100644 index 0000000000..aef4c58e99 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerConfig.java @@ -0,0 +1,119 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.registry.WebServiceDescriptor; +import io.cloudbeaver.server.registry.WebServiceRegistry; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.meta.RuntimeAction; +import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; +import org.jkiss.dbeaver.registry.language.PlatformLanguageRegistry; + +import java.util.ArrayList; +import java.util.List; + +/** + * Web server configuration + */ +public class WebServerConfig { + + private String name; + private String version; + private boolean supportsPredefinedConnections; + private boolean supportsCustomConnections; + private boolean supportsConnectionBrowser; + private boolean supportsWorkspaces; + + public WebServerConfig(String name, String version) { + this.name = name; + this.version = version; + } + + @Property + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Property + public String getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = version; + } + + @Property + public boolean isSupportsPredefinedConnections() { + return supportsPredefinedConnections; + } + + public void setSupportsPredefinedConnections(boolean supportsPredefinedConnections) { + this.supportsPredefinedConnections = supportsPredefinedConnections; + } + + @Property + public boolean isSupportsCustomConnections() { + return supportsCustomConnections; + } + + public void setSupportsCustomConnections(boolean supportsCustomConnections) { + this.supportsCustomConnections = supportsCustomConnections; + } + + @Property + public boolean isSupportsConnectionBrowser() { + return supportsConnectionBrowser; + } + + public void setSupportsConnectionBrowser(boolean supportsConnectionBrowser) { + this.supportsConnectionBrowser = supportsConnectionBrowser; + } + + @Property + public boolean isSupportsWorkspaces() { + return supportsWorkspaces; + } + + public void setSupportsWorkspaces(boolean supportsWorkspaces) { + this.supportsWorkspaces = supportsWorkspaces; + } + + @RuntimeAction + public WebServerLanguage[] getSupportedLanguages() { + List langs = PlatformLanguageRegistry.getInstance().getLanguages(); + WebServerLanguage[] webLangs = new WebServerLanguage[langs.size()]; + for (int i = 0; i < webLangs.length; i++) { + webLangs[i] = new WebServerLanguage(langs.get(i)); + } + return webLangs; + } + + @RuntimeAction + public WebServiceConfig[] getServices() { + List services = new ArrayList<>(); + for (WebServiceDescriptor wsd : WebServiceRegistry.getInstance().getWebServices()) { + services.add(new WebServiceConfig(wsd)); + } + return services.toArray(new WebServiceConfig[0]); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerError.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerError.java new file mode 100644 index 0000000000..81c67c3189 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerError.java @@ -0,0 +1,55 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import org.jkiss.dbeaver.DBException; + +import java.io.PrintWriter; +import java.io.StringWriter; + +/** + * Web driver configuration + */ +public class WebServerError { + + private String message; + private String stackTrace; + private String errorCode; + + public WebServerError(Throwable ex) { + this.message = ex.getMessage(); + StringWriter buf = new StringWriter(); + ex.printStackTrace(new PrintWriter(buf, true)); + this.stackTrace = buf.toString(); + if (ex instanceof DBException) { + errorCode = String.valueOf(((DBException) ex).getErrorCode()); + } + } + + public String getMessage() { + return message; + } + + public String getStackTrace() { + return stackTrace; + } + + public String getErrorCode() { + return errorCode; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerLanguage.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerLanguage.java new file mode 100644 index 0000000000..5c94f54b4f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerLanguage.java @@ -0,0 +1,48 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import org.jkiss.dbeaver.registry.language.PlatformLanguageDescriptor; + +/** + * Web server message + */ +public class WebServerLanguage { + + private PlatformLanguageDescriptor languageDescriptor; + + public WebServerLanguage(PlatformLanguageDescriptor languageDescriptor) { + this.languageDescriptor = languageDescriptor; + } + + public String getIsoCode() { + return languageDescriptor.getCode(); + } + + public String getDisplayName() { + return languageDescriptor.getLabel(); + } + + public String getNativeName() { + return languageDescriptor.getLocalizedName(getIsoCode()); + } + + @Override + public String toString() { + return getIsoCode(); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerMessage.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerMessage.java new file mode 100644 index 0000000000..910fb684b6 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServerMessage.java @@ -0,0 +1,60 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.CloudbeaverConstants; + +/** + * Web server message + */ +public class WebServerMessage { + + + public enum MessageType { + DEBUG, + INFO, + WARNING, + ERROR + } + + private final MessageType type; + private long time; + private String message; + + public WebServerMessage(MessageType type, String message) { + this.type = type; + this.time = System.currentTimeMillis(); + this.message = message; + } + + public MessageType getType() { + return type; + } + + public String getTime() { + return CloudbeaverConstants.ISO_DATE_FORMAT.format(time); + } + + public String getMessage() { + return message; + } + + public String getStackTrace() { + return null; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServiceConfig.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServiceConfig.java new file mode 100644 index 0000000000..0c552a9085 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/WebServiceConfig.java @@ -0,0 +1,53 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model; + +import io.cloudbeaver.server.registry.WebServiceDescriptor; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web server configuration + */ +public class WebServiceConfig { + + private final WebServiceDescriptor descriptor; + + public WebServiceConfig(WebServiceDescriptor descriptor) { + this.descriptor = descriptor; + } + + @Property + public String getId() { + return descriptor.getId(); + } + + @Property + public String getName() { + return descriptor.getLabel(); + } + + @Property + public String getDescription() { + return descriptor.getDescription(); + } + + @Property + public String getBundleVersion() { + return descriptor.getContributorBundle().getVersion().toString(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/resources/WebWorkspace.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/resources/WebWorkspace.java new file mode 100644 index 0000000000..80156aede4 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/resources/WebWorkspace.java @@ -0,0 +1,296 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.resources; + +import org.eclipse.core.resources.*; +import org.eclipse.core.runtime.*; +import org.eclipse.core.runtime.jobs.ISchedulingRule; + +import java.io.InputStream; +import java.net.URI; +import java.util.Map; + +/** + * Web workspace + */ +public class WebWorkspace implements IWorkspace { + + @Override + public void addResourceChangeListener(IResourceChangeListener listener) { + + } + + @Override + public void addResourceChangeListener(IResourceChangeListener listener, int eventMask) { + + } + + @Override + public ISavedState addSaveParticipant(Plugin plugin, ISaveParticipant participant) throws CoreException { + return null; + } + + @Override + public ISavedState addSaveParticipant(String pluginId, ISaveParticipant participant) throws CoreException { + return null; + } + + @Override + public void build(int kind, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public void build(IBuildConfiguration[] buildConfigs, int kind, boolean buildReferences, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public void checkpoint(boolean build) { + + } + + @Override + public IProject[][] computePrerequisiteOrder(IProject[] projects) { + return new IProject[0][]; + } + + @Override + public ProjectOrder computeProjectOrder(IProject[] projects) { + return null; + } + + @Override + public IStatus copy(IResource[] resources, IPath destination, boolean force, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public IStatus copy(IResource[] resources, IPath destination, int updateFlags, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public IStatus delete(IResource[] resources, boolean force, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public IStatus delete(IResource[] resources, int updateFlags, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public void deleteMarkers(IMarker[] markers) throws CoreException { + + } + + @Override + public void forgetSavedTree(String pluginId) { + + } + + @Override + public IFilterMatcherDescriptor[] getFilterMatcherDescriptors() { + return new IFilterMatcherDescriptor[0]; + } + + @Override + public IFilterMatcherDescriptor getFilterMatcherDescriptor(String filterMatcherId) { + return null; + } + + @Override + public IProjectNatureDescriptor[] getNatureDescriptors() { + return new IProjectNatureDescriptor[0]; + } + + @Override + public IProjectNatureDescriptor getNatureDescriptor(String natureId) { + return null; + } + + @Override + public Map getDanglingReferences() { + return null; + } + + @Override + public IWorkspaceDescription getDescription() { + return null; + } + + @Override + public IWorkspaceRoot getRoot() { + return null; + } + + @Override + public IResourceRuleFactory getRuleFactory() { + return null; + } + + @Override + public ISynchronizer getSynchronizer() { + return null; + } + + @Override + public boolean isAutoBuilding() { + return false; + } + + @Override + public boolean isTreeLocked() { + return false; + } + + @Override + public IProjectDescription loadProjectDescription(IPath projectDescriptionFile) throws CoreException { + return null; + } + + @Override + public IProjectDescription loadProjectDescription(InputStream projectDescriptionFile) throws CoreException { + return null; + } + + @Override + public IStatus move(IResource[] resources, IPath destination, boolean force, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public IStatus move(IResource[] resources, IPath destination, int updateFlags, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public IBuildConfiguration newBuildConfig(String projectName, String configName) { + return null; + } + + @Override + public IProjectDescription newProjectDescription(String projectName) { + return null; + } + + @Override + public void removeResourceChangeListener(IResourceChangeListener listener) { + + } + + @Override + public void removeSaveParticipant(Plugin plugin) { + + } + + @Override + public void removeSaveParticipant(String pluginId) { + + } + + @Override + public void run(ICoreRunnable action, ISchedulingRule rule, int flags, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public void run(ICoreRunnable action, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public void run(IWorkspaceRunnable action, ISchedulingRule rule, int flags, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public void run(IWorkspaceRunnable action, IProgressMonitor monitor) throws CoreException { + + } + + @Override + public IStatus save(boolean full, IProgressMonitor monitor) throws CoreException { + return null; + } + + @Override + public void setDescription(IWorkspaceDescription description) throws CoreException { + + } + + @Override + public String[] sortNatureSet(String[] natureIds) { + return new String[0]; + } + + @Override + public IStatus validateEdit(IFile[] files, Object context) { + return null; + } + + @Override + public IStatus validateFiltered(IResource resource) { + return null; + } + + @Override + public IStatus validateLinkLocation(IResource resource, IPath location) { + return null; + } + + @Override + public IStatus validateLinkLocationURI(IResource resource, URI location) { + return null; + } + + @Override + public IStatus validateName(String segment, int typeMask) { + return null; + } + + @Override + public IStatus validateNatureSet(String[] natureIds) { + return null; + } + + @Override + public IStatus validatePath(String path, int typeMask) { + return null; + } + + @Override + public IStatus validateProjectLocation(IProject project, IPath location) { + return null; + } + + @Override + public IStatus validateProjectLocationURI(IProject project, URI location) { + return null; + } + + @Override + public IPathVariableManager getPathVariableManager() { + return null; + } + + @Override + public T getAdapter(Class adapter) { + return null; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSession.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSession.java new file mode 100644 index 0000000000..69e63a0e45 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSession.java @@ -0,0 +1,548 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.session; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.WebServiceUtils; +import io.cloudbeaver.server.CloudbeaverConstants; +import io.cloudbeaver.server.CloudbeaverPlatform; +import io.cloudbeaver.server.model.*; +import io.cloudbeaver.server.model.sql.WebSQLProcessor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSourceContainer; +import org.jkiss.dbeaver.model.app.DBPDataSourceRegistry; +import org.jkiss.dbeaver.model.app.DBPPlatform; +import org.jkiss.dbeaver.model.app.DBPProject; +import org.jkiss.dbeaver.model.connection.DBPConnectionConfiguration; +import org.jkiss.dbeaver.model.connection.DBPDriver; +import org.jkiss.dbeaver.model.meta.Association; +import org.jkiss.dbeaver.model.meta.Property; +import org.jkiss.dbeaver.model.navigator.*; +import org.jkiss.dbeaver.model.runtime.*; +import org.jkiss.dbeaver.registry.DataSourceDescriptor; +import org.jkiss.dbeaver.runtime.DBWorkbench; +import org.jkiss.dbeaver.runtime.jobs.ConnectionTestJob; +import org.jkiss.dbeaver.runtime.jobs.DisconnectJob; +import org.jkiss.dbeaver.utils.RuntimeUtils; +import org.jkiss.utils.CommonUtils; + +import javax.servlet.http.HttpSession; +import java.lang.reflect.InvocationTargetException; +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Web session. + * Is the main source of data in web application + */ +public class WebSession { + + private static final Log log = Log.getLog(WebSession.class); + + public static final List EMPTY_NODE_LIST = Collections.emptyList(); + + private static final String ATTR_LOCALE = "locale"; + + private static final AtomicInteger TASK_ID = new AtomicInteger(); + + private String id; + private long createTime; + private long lastAccessTime; + + private String locale; + + private final List connections = new ArrayList<>(); + private final List progressMessages = new ArrayList<>(); + + private final Map asyncTasks = new HashMap<>(); + + private DBNModel navigatorModel; + private DBNProjectDatabases databases; + private DBRProgressMonitor progressMonitor = new SessionProgressMonitor(); + + public WebSession(HttpSession httpSession) { + this.id = httpSession.getId(); + this.createTime = System.currentTimeMillis(); + + this.updateInfo(httpSession); + + DBPPlatform platform = DBWorkbench.getPlatform(); + this.navigatorModel = new DBNModel(platform, false); + this.navigatorModel.initialize(); + + DBPProject project = platform.getWorkspace().getActiveProject(); + DBNProject projectNode = this.navigatorModel.getRoot().getProjectNode(project); + this.databases = projectNode.getDatabases(); + this.locale = Locale.getDefault().getLanguage(); + + // Add all provided datasources to the session + synchronized (connections) { + for (DBPDataSourceContainer ds : databases.getDataSourceRegistry().getDataSources()) { + if (ds.isProvided()) { + WebConnectionInfo connectionInfo = new WebConnectionInfo(this, ds); + connections.add(connectionInfo); + } + } + } + } + + @Property + public String getId() { + return id; + } + + @Property + public String getCreateTime() { + return CloudbeaverConstants.ISO_DATE_FORMAT.format(createTime); + } + + @Property + public String getLastAccessTime() { + return CloudbeaverConstants.ISO_DATE_FORMAT.format(lastAccessTime); + } + + public long getLastAccessTimeMillis() { + return lastAccessTime; + } + + @NotNull + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public DBNModel getNavigatorModel() { + return navigatorModel; + } + + /** + * Returns and clears progress messages + */ + @Association + public List getProgressMessages() { + synchronized (progressMessages) { + List copy = new ArrayList<>(progressMessages); + progressMessages.clear(); + return copy; + } + } + + public void updateInfo(HttpSession httpSession) { + this.lastAccessTime = System.currentTimeMillis(); + this.locale = CommonUtils.toString(httpSession.getAttribute(ATTR_LOCALE), this.locale); + } + + @Association + public List getConnections() { + synchronized (connections) { + return new ArrayList<>(connections); + } + } + + public WebConnectionInfo openConnectionFromTemplate(WebConnectionConfig config) throws DBWebException { + String dataSourceId = config.getDataSourceId(); + if (CommonUtils.isEmpty(dataSourceId)) { + throw new DBWebException("Only preconfigured data sources are supported yet"); + } + DBPDataSourceRegistry templateRegistry = WebServiceUtils.getDataSourceRegistry(); + DBPDataSourceContainer dataSourceTemplate = templateRegistry.getDataSource(dataSourceId); + if (dataSourceTemplate == null) { + throw new DBWebException("Datasource '" + dataSourceId + "' not found"); + } + + DBPDataSourceRegistry sessionRegistry = databases.getDataSourceRegistry(); + DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(dataSourceTemplate); + newDataSource.setSavePassword(true); + ((DataSourceDescriptor)newDataSource).setTemporary(true); + + DBPConnectionConfiguration cfg = newDataSource.getConnectionConfiguration(); + cfg.setUserName(config.getUserName()); + cfg.setUserPassword(config.getUserPassword()); + if (!CommonUtils.isEmpty(config.getName())) { + newDataSource.setName(config.getName()); + } + if (!CommonUtils.isEmpty(config.getDescription())) { + newDataSource.setDescription(config.getDescription()); + } + try { + newDataSource.connect(getProgressMonitor(), true, false); + } catch (DBException e) { + throw new DBWebException("Error connecting to database", e); + } + sessionRegistry.addDataSource(newDataSource); + + WebConnectionInfo connectionInfo = new WebConnectionInfo(this, newDataSource); + synchronized (connections) { + connections.add(connectionInfo); + } + + return connectionInfo; + } + + public WebConnectionInfo createConnection(WebConnectionConfig config) throws DBWebException { + DBPDataSourceRegistry sessionRegistry = databases.getDataSourceRegistry(); + + DBPDataSourceContainer newDataSource = makeConnectionInstance(config, sessionRegistry); + if (CommonUtils.isEmpty(newDataSource.getName())) { + newDataSource.setName(CommonUtils.notNull(config.getName(), "NewConnection")); + } + ((DataSourceDescriptor)newDataSource).setTemporary(true); + sessionRegistry.addDataSource(newDataSource); + + WebConnectionInfo connectionInfo = new WebConnectionInfo(this, newDataSource); + synchronized (connections) { + connections.add(connectionInfo); + } + + return connectionInfo; + } + + public WebConnectionInfo testConnection(WebConnectionConfig config) throws DBWebException { + DBPDataSourceRegistry sessionRegistry = databases.getDataSourceRegistry(); + + DBPDataSourceContainer newDataSource = makeConnectionInstance(config, sessionRegistry); + try { + ConnectionTestJob ct = new ConnectionTestJob(newDataSource, param -> {}); + ct.run(getProgressMonitor()); + if (ct.getConnectError() != null) { + throw new DBWebException("Connection failed", ct.getConnectError()); + } + WebConnectionInfo connectionInfo = new WebConnectionInfo(this, newDataSource); + connectionInfo.setConnectError(ct.getConnectError()); + connectionInfo.setServerVersion(ct.getServerVersion()); + connectionInfo.setClientVersion(ct.getClientVersion()); + connectionInfo.setConnectTime(RuntimeUtils.formatExecutionTime(ct.getConnectTime())); + return connectionInfo; + } catch (DBException e) { + throw new DBWebException("Error connecting to database", e); + } + } + + @NotNull + private DBPDataSourceContainer makeConnectionInstance(WebConnectionConfig config, DBPDataSourceRegistry sessionRegistry) throws DBWebException { + String driverId = config.getDriverId(); + if (CommonUtils.isEmpty(driverId)) { + throw new DBWebException("Driver not specified"); + } + DBPDriver driver = WebServiceUtils.getDriverById(driverId); + + DBPConnectionConfiguration dsConfig = new DBPConnectionConfiguration(); + + DBPDataSourceContainer newDataSource = sessionRegistry.createDataSource(driver, dsConfig); + newDataSource.setSavePassword(true); + + if (!CommonUtils.isEmpty(config.getUrl())) { + dsConfig.setUrl(config.getUrl()); + } else { + dsConfig.setHostName(config.getHost()); + dsConfig.setHostPort(config.getPort()); + dsConfig.setDatabaseName(config.getDatabaseName()); + dsConfig.setServerName(config.getServerName()); + dsConfig.setUrl(driver.getDataSourceProvider().getConnectionURL(driver, dsConfig)); + } + if (config.getProperties() != null) { + for (Map.Entry pe : config.getProperties().entrySet()) { + dsConfig.setProperty(pe.getKey(), CommonUtils.toString(pe.getValue())); + } + } + dsConfig.setUserName(config.getUserName()); + dsConfig.setUserPassword(config.getUserPassword()); + newDataSource.setName(config.getName()); + newDataSource.setDescription(config.getDescription()); + return newDataSource; + } + + public boolean closeConnection(String connectionID) throws DBWebException { + + WebConnectionInfo connectionInfo = getWebConnectionInfo(connectionID); + + boolean disconnected = false; + if (connectionInfo.isConnected()) { + try { + connectionInfo.getDataSourceContainer().disconnect(getProgressMonitor()); + disconnected = true; + } catch (DBException e) { + log.error("Error closing connection", e); + } + // Disconnect in async mode? + //new DisconnectJob(connectionInfo.getDataSource()).schedule(); + } + databases.getDataSourceRegistry().removeDataSource(connectionInfo.getDataSourceContainer()); + + synchronized (connections) { + connections.remove(connectionInfo); + } + + return disconnected; + } + + @NotNull + private WebConnectionInfo getWebConnectionInfo(String connectionID) throws DBWebException { + WebConnectionInfo connectionInfo = null; + synchronized (connections) { + for (WebConnectionInfo ci : connections) { + if (ci.getId().equals(connectionID)) { + connectionInfo = ci; + break; + } + } + } + if (connectionInfo == null) { + throw new DBWebException("Connection '" + connectionID + "' not found"); + } + return connectionInfo; + } + + void close() { + try { + List conCopy; + synchronized (this.connections) { + conCopy = new ArrayList<>(this.connections); + this.connections.clear(); + } + + for (WebConnectionInfo connectionInfo : conCopy) { + if (connectionInfo.isConnected()) { + new DisconnectJob(connectionInfo.getDataSourceContainer()).schedule(); + } + } + + if (this.navigatorModel != null) { + this.navigatorModel.dispose(); + this.navigatorModel = null; + } + this.databases = null; + } catch (Throwable e) { + log.error(e); + } + } + + public List getNavigatorNodeChildren(String parentPath, Integer offset, Integer limit, Boolean onlyFolders) throws DBWebException { + try { + DBRProgressMonitor monitor = getProgressMonitor(); + + DBNNode parentNode = CommonUtils.isEmpty(parentPath) || "/".equals(parentPath) ? databases : navigatorModel.getNodeByPath(monitor, parentPath); + if (parentNode == null) { + throw new DBWebException("Node '" + parentPath + "' not found"); + } + if (!parentNode.hasChildren(true)) { + return EMPTY_NODE_LIST; + } + DBNNode[] nodeChildren = parentNode.getChildren(monitor); + if (nodeChildren == null) { + return EMPTY_NODE_LIST; + } + List result = new ArrayList<>(); + for (DBNNode node : nodeChildren) { + if (!CommonUtils.toBoolean(onlyFolders) || node instanceof DBNContainer) { + result.add(new WebNavigatorNodeInfo(this, node)); + } + } + return result; + } catch (DBException e) { + throw new DBWebException("Error getting navigator nodes", e); + } + } + + @NotNull + public WebNavigatorNodeInfo getNavigatorNodeInfo(String nodePath) throws DBWebException { + try { + DBRProgressMonitor monitor = getProgressMonitor(); + + DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); + if (node == null) { + throw new DBWebException("Navigator node '" + nodePath + "' not found"); + } + return new WebNavigatorNodeInfo(this, node); + } catch (DBException e) { + throw new DBWebException("Error getting navigator node '" + nodePath + "'", e); + } + } + + public boolean refreshNavigatorNode(String nodePath) throws DBWebException { + try { + DBRProgressMonitor monitor = getProgressMonitor(); + + DBNNode node = navigatorModel.getNodeByPath(monitor, nodePath); + if (node == null) { + throw new DBWebException("Navigator node '" + nodePath + "' not found"); + } + node.refreshNode(monitor, this); + return true; + } catch (DBException e) { + throw new DBWebException("Error refreshing navigator node '" + nodePath + "'", e); + } + } + + public DBRProgressMonitor getProgressMonitor() { + return progressMonitor; + } + + public WebSQLProcessor getSQLProcessor(String connectionId) throws DBWebException { + return getWebConnectionInfo(connectionId).getSQLProcessor(); + } + + public List getDriverList(@Nullable String driverId) { + List result = new ArrayList<>(); + for (DBPDriver driver : CloudbeaverPlatform.getInstance().getApplicableDrivers()) { + if (driverId == null || driverId.equals(WebServiceUtils.makeDriverFullId(driver))) { + result.add(new WebDatabaseDriverConfig(this, driver)); + } + } + + return result; + } + + /////////////////////////////////////////////////////// + // Async model + + public WebAsyncTaskInfo getAsyncTask(String taskId, String taskName, boolean create) { + synchronized (asyncTasks) { + WebAsyncTaskInfo taskInfo = asyncTasks.get(taskId); + if (taskInfo == null && create) { + taskInfo = new WebAsyncTaskInfo(taskId, taskName); + asyncTasks.put(taskId, taskInfo); + } + return taskInfo; + } + } + + public WebAsyncTaskInfo asyncTaskStatus(String taskId) throws DBWebException { + synchronized (asyncTasks) { + WebAsyncTaskInfo taskInfo = asyncTasks.get(taskId); + if (taskInfo == null) { + throw new DBWebException("Task '" + taskId + "' not found"); + } + taskInfo.setRunning(taskInfo.getJob() != null && !taskInfo.getJob().isFinished()); + if (!taskInfo.isRunning()) { + asyncTasks.remove(taskId); + } + return taskInfo; + } + } + + public boolean asyncTaskCancel(String taskId) throws DBWebException { + WebAsyncTaskInfo taskInfo; + synchronized (asyncTasks) { + taskInfo = asyncTasks.get(taskId); + if (taskInfo == null) { + throw new DBWebException("Task '" + taskId + "' not found"); + } + } + AbstractJob job = taskInfo.getJob(); + if (job != null) { + job.cancel(); + } + return true; + } + + public WebAsyncTaskInfo createAndRunAsyncTask(String taskName, DBRRunnableWithResult runnable) { + int taskId = TASK_ID.incrementAndGet(); + WebAsyncTaskInfo asyncTask = getAsyncTask(String.valueOf(taskId), taskName, true); + + AbstractJob job = new AbstractJob(taskName) { + @Override + protected IStatus run(DBRProgressMonitor monitor) { + TaskProgressMonitor taskMonitor = new TaskProgressMonitor(monitor, asyncTask); + try { + runnable.run(taskMonitor); + asyncTask.setResult(runnable.getResult()); + asyncTask.setStatus("Finished"); + asyncTask.setRunning(false); + } catch (InvocationTargetException e) { + asyncTask.setJobError(e.getTargetException()); + } catch (InterruptedException e) { + asyncTask.setJobError(e); + } + return Status.OK_STATUS; + } + }; + + asyncTask.setJob(job); + asyncTask.setRunning(true); + job.schedule(); + return asyncTask; + } + + public List readLog(Integer maxEntries, Boolean clearLog) { + synchronized (progressMessages) { + List messages = new ArrayList<>(); + int entryCount = CommonUtils.toInt(maxEntries); + if (entryCount == 0 || entryCount >= progressMessages.size()) { + messages.addAll(progressMessages); + if (CommonUtils.toBoolean(clearLog)) { + progressMessages.clear(); + } + } else { + messages.addAll(progressMessages.subList(0, maxEntries)); + if (CommonUtils.toBoolean(clearLog)) { + progressMessages.removeAll(messages); + } + } + return messages; + } + } + + /////////////////////////////////////////////////////// + // Utils + + private class SessionProgressMonitor extends BaseProgressMonitor { + @Override + public void beginTask(String name, int totalWork) { + synchronized (progressMessages) { + progressMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); + } + } + + @Override + public void subTask(String name) { + synchronized (progressMessages) { + progressMessages.add(new WebServerMessage(WebServerMessage.MessageType.INFO, name)); + } + } + } + + private class TaskProgressMonitor extends ProxyProgressMonitor { + + private final WebAsyncTaskInfo asyncTask; + + public TaskProgressMonitor(DBRProgressMonitor original, WebAsyncTaskInfo asyncTask) { + super(original); + this.asyncTask = asyncTask; + } + + @Override + public void beginTask(String name, int totalWork) { + super.beginTask(name, totalWork); + asyncTask.setStatus(name); + } + + @Override + public void subTask(String name) { + super.subTask(name); + asyncTask.setStatus(name); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSessionManager.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSessionManager.java new file mode 100644 index 0000000000..3c36b6a331 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/session/WebSessionManager.java @@ -0,0 +1,149 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.session; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.CloudbeaverPlatform; +import io.cloudbeaver.server.model.WebConnectionConfig; +import io.cloudbeaver.server.model.WebConnectionInfo; +import org.jkiss.code.NotNull; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.runtime.DBWorkbench; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import java.util.*; + +/** + * Various constants + */ +public class WebSessionManager { + + private static final Log log = Log.getLog(WebSessionManager.class); + + private static WebSessionManager instance; + + public synchronized static WebSessionManager getInstance() { + if (instance == null) { + instance = new WebSessionManager(); + } + return instance; + } + + private final Map sessionMap = new HashMap<>(); + + public WebSessionManager() { + } + + public WebSession openSession(@NotNull HttpServletRequest request, boolean errorOnNoFound) throws DBWebException { + HttpSession session = getOrCreateHttpSession(request); + return getWebSession(session, errorOnNoFound); + } + + public boolean closeSession(@NotNull HttpServletRequest request) { + HttpSession session = request.getSession(); + if (session != null) { + WebSession webSession; + synchronized (sessionMap) { + webSession = sessionMap.remove(session.getId()); + } + if (webSession != null) { + log.debug("> Close session '" + session.getId() + "'"); + webSession.close(); + return true; + } + } + return false; + } + + public boolean touchSession(@NotNull HttpServletRequest request) throws DBWebException { + WebSession webSession = openSession(request, false); + webSession.updateInfo(request.getSession()); + return true; + } + + public WebSession getWebSession(@NotNull HttpServletRequest request) throws DBWebException { + return openSession(request, true); + } + + private HttpSession getOrCreateHttpSession(@NotNull HttpServletRequest request) { + HttpSession session = request.getSession(); + if (session == null) { + session = request.getSession(true); + log.debug("New session: " + session.getId()); + } + return session; + } + + public WebSession getWebSession(HttpSession httpSession, boolean errorOnNoFound) throws DBWebException { + String sessionId = httpSession.getId(); + WebSession webSession; + synchronized (sessionMap) { + webSession = sessionMap.get(sessionId); + if (webSession == null) { + webSession = new WebSession(httpSession); + sessionMap.put(sessionId, webSession); + + if (!httpSession.isNew() && errorOnNoFound) { + throw new DBWebException("Session has expired", DBWebException.ERROR_CODE_SESSION_EXPIRED); + } + + log.debug("> New web session '" + webSession.getId() + "'"); + } else { + webSession.updateInfo(httpSession); + } + } + return webSession; + } + + public WebConnectionInfo openConnection(HttpServletRequest servletRequest, Map config) throws DBWebException { + return getWebSession(servletRequest).openConnectionFromTemplate(new WebConnectionConfig(config)); + } + + public WebConnectionInfo createConnection(HttpServletRequest servletRequest, Map config) throws DBWebException { + return getWebSession(servletRequest).createConnection(new WebConnectionConfig(config)); + } + + public WebConnectionInfo testConnection(HttpServletRequest servletRequest, Map config) throws DBWebException { + return getWebSession(servletRequest).testConnection(new WebConnectionConfig(config)); + } + + public boolean closeConnection(HttpServletRequest servletRequest, String connectionId) throws DBWebException { + return getWebSession(servletRequest).closeConnection(connectionId); + } + + public void expireIdleSessions() { + long maxSessionIdleTime = DBWorkbench.getPlatform(CloudbeaverPlatform.class).getApplication().getMaxSessionIdleTime(); + + List expiredList = new ArrayList<>(); + synchronized (sessionMap) { + for (Iterator iterator = sessionMap.values().iterator(); iterator.hasNext(); ) { + WebSession session = iterator.next(); + long idleMillis = System.currentTimeMillis() - session.getLastAccessTimeMillis(); + if (idleMillis >= maxSessionIdleTime) { + iterator.remove(); + expiredList.add(session); + } + } + } + + for (WebSession session : expiredList) { + log.debug("> Expire session '" + session.getId() + "'"); + session.close(); + } + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebExecutionSource.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebExecutionSource.java new file mode 100644 index 0000000000..6d5e7fb0f2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebExecutionSource.java @@ -0,0 +1,27 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.impl.AbstractExecutionSource; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; + +class WebExecutionSource extends AbstractExecutionSource { + public WebExecutionSource(DBSDataContainer dataContainer, DBCExecutionContext executionContext, Object controller) { + super(dataContainer, executionContext, controller); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionContext.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionContext.java new file mode 100644 index 0000000000..842d85b20f --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionContext.java @@ -0,0 +1,115 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.*; +import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionContext; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionRequest; + +import java.util.Map; + +/** + * Web SQL dialect. + */ +public class WebSQLCompletionContext implements SQLCompletionContext { + + private static final Log log = Log.getLog(WebSQLCompletionContext.class); + + private final WebSQLContextInfo sqlContext; + + public WebSQLCompletionContext(WebSQLContextInfo context) { + this.sqlContext = context; + } + + @Override + public DBPDataSource getDataSource() { + return sqlContext.getProcessor().getConnection().getDataSource(); + } + + @Override + public DBCExecutionContext getExecutionContext() { + return DBUtils.getDefaultContext( + sqlContext.getProcessor().getConnection().getDataSource(), + false); + } + + @Override + public SQLSyntaxManager getSyntaxManager() { + return sqlContext.getProcessor().getSyntaxManager(); + } + + @Override + public boolean isUseFQNames() { + return false; + } + + @Override + public boolean isReplaceWords() { + return false; + } + + @Override + public boolean isShowServerHelp() { + return false; + } + + @Override + public boolean isUseShortNames() { + return false; + } + + @Override + public int getInsertCase() { + return PROPOSAL_CASE_DEFAULT; + } + + @Override + public boolean isSearchProcedures() { + return false; + } + + @Override + public boolean isSearchInsideNames() { + return false; + } + + @Override + public boolean isSortAlphabetically() { + return false; + } + + @Override + public boolean isSearchGlobally() { + return false; + } + + @Override + public boolean isHideDuplicates() { + return false; + } + + @Override + public SQLCompletionProposalBase createProposal(@NotNull SQLCompletionRequest request, @NotNull String displayString, @NotNull String replacementString, int cursorPosition, @Nullable DBPImage image, @NotNull DBPKeywordType proposalType, @Nullable String description, @Nullable DBPNamedObject object, @NotNull Map params) { + return new SQLCompletionProposalBase(this, request.getWordDetector(), displayString, replacementString, cursorPosition, image, proposalType, description, object, params); + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionProposal.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionProposal.java new file mode 100644 index 0000000000..b599cffcfe --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLCompletionProposal.java @@ -0,0 +1,69 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPImage; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; + +/** + * Web SQL dialect. + */ +public class WebSQLCompletionProposal { + + private static final Log log = Log.getLog(WebSQLCompletionProposal.class); + + private SQLCompletionProposalBase proposal; + + public WebSQLCompletionProposal(SQLCompletionProposalBase proposal) { + this.proposal = proposal; + } + + public String getDisplayString() { + return proposal.getDisplayString(); + } + + public String getType() { + return proposal.getProposalType().name(); + } + + public String getReplacementString() { + return proposal.getReplacementString(); + } + + public int getReplacementOffset() { + return proposal.getReplacementOffset(); + } + + public int getReplacementLength() { + return proposal.getReplacementLength(); + } + + public int getScore() { + return proposal.getProposalScore(); + } + + public String getIcon() { + DBPImage image = proposal.getObjectImage(); + return image == null ? null : image.getLocation(); + } + + public String getNodePath() { + return null; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLContextInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLContextInfo.java new file mode 100644 index 0000000000..c3733120d2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLContextInfo.java @@ -0,0 +1,255 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.model.WebAsyncTaskInfo; +import org.eclipse.jface.text.Document; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.data.DBDAttributeBinding; +import org.jkiss.dbeaver.model.exec.DBCExecutionContext; +import org.jkiss.dbeaver.model.exec.DBExecUtils; +import org.jkiss.dbeaver.model.impl.struct.ContextDefaultObjectsReader; +import org.jkiss.dbeaver.model.meta.RuntimeAction; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.runtime.DBRRunnableWithResult; +import org.jkiss.dbeaver.model.sql.SQLQuery; +import org.jkiss.dbeaver.model.sql.SQLScriptElement; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionAnalyzer; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionProposalBase; +import org.jkiss.dbeaver.model.sql.completion.SQLCompletionRequest; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.model.struct.DBSObject; +import org.jkiss.dbeaver.model.struct.DBSObjectContainer; +import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; +import org.jkiss.dbeaver.model.struct.rdb.DBSSchema; +import org.jkiss.utils.CommonUtils; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * WebSQLContextInfo. + */ +public class WebSQLContextInfo { + + private static final Log log = Log.getLog(WebSQLContextInfo.class); + + private WebSQLProcessor processor; + private String id; + private DBSCatalog defaultCatalog; + private String defaultSchema; + private Map resultInfoMap = new HashMap<>(); + + private AtomicInteger resultId = new AtomicInteger(); + + public WebSQLContextInfo(WebSQLProcessor processor, String id, String catalogName, String schemaName) { + this.processor = processor; + this.id = id; + + setContextDefaults(catalogName, schemaName); + } + + private void setContextDefaults(String catalogName, String schemaName) { + DBPDataSource dataSource = this.processor.getConnection().getDataSource(); + + DBSObject defaultObject = null; + if (CommonUtils.isEmpty(catalogName) && CommonUtils.isEmpty(schemaName)) { + ContextDefaultObjectsReader defaultObjectsReader = new ContextDefaultObjectsReader( + dataSource, + DBUtils.getDefaultContext(dataSource, false) + ); + defaultObjectsReader.setReadNodes(false); + try { + defaultObjectsReader.run(this.processor.getWebSession().getProgressMonitor()); + } catch (InvocationTargetException e) { + log.error("Error reading context defaults", e.getTargetException()); + } catch (InterruptedException e) { + // ignore + } + + catalogName = defaultObjectsReader.getDefaultCatalogName(); + defaultObject = defaultObjectsReader.getDefaultObject(); + } + + if (defaultObject instanceof DBSCatalog) { + this.defaultCatalog = (DBSCatalog) defaultObject; + } else if (defaultObject instanceof DBSSchema) { + this.defaultCatalog = DBUtils.getParentOfType(DBSCatalog.class, defaultObject); + this.defaultSchema = defaultObject.getName(); + } else { + if (defaultObject == null && (!CommonUtils.isEmpty(catalogName) || !CommonUtils.isEmpty(schemaName))) { + String childObjectName = CommonUtils.toString(catalogName, schemaName); + DBSObjectContainer objectContainer = DBUtils.getAdapter(DBSObjectContainer.class, dataSource); + if (objectContainer != null) { + try { + DBSObject childObject = objectContainer.getChild(this.processor.getWebSession().getProgressMonitor(), childObjectName); + if (childObject == null) { + log.debug("Can't find child '" + childObjectName + "'"); + } else if (childObject instanceof DBSCatalog) { + defaultCatalog = (DBSCatalog) childObject; + } else if (childObject instanceof DBSSchema) { + this.defaultCatalog = DBUtils.getParentOfType(DBSCatalog.class, childObject); + this.defaultSchema = childObject.getName(); + } + } catch (DBException e) { + log.error("Error reading default object '" + childObjectName + "'", e); + } + } + } + } + + if (defaultSchema == null && schemaName != null) { + defaultSchema = schemaName; + } + } + + public WebSQLProcessor getProcessor() { + return processor; + } + + public String getId() { + return id; + } + + public String getDefaultCatalog() { + return defaultCatalog == null ? null : defaultCatalog.getName(); + } + + public void setDefaults(String catalogName, String schemaName) throws DBWebException { + String oldCatalogName = defaultCatalog == null ? null : defaultCatalog.getName(); + setContextDefaults(catalogName, schemaName); + try { + DBExecUtils.setExecutionContextDefaults( + processor.getWebSession().getProgressMonitor(), + processor.getConnection().getDataSource(), + processor.getExecutionContext(), + defaultCatalog == null ? null : defaultCatalog.getName(), + oldCatalogName, + schemaName); + } catch (DBException e) { + throw new DBWebException("Error ", e); + } + } + + @RuntimeAction + public String getDefaultSchema() { + if (defaultSchema == null) { + DBCExecutionContext defaultContext = DBUtils.getDefaultContext( + defaultCatalog != null ? defaultCatalog : processor.getConnection().getDataSource(), false); + DBSObject ao = DBUtils.getActiveInstanceObject(defaultContext); + if (ao instanceof DBSSchema) { + defaultSchema = ao.getName(); + } else { + defaultSchema = ""; + } + } + return CommonUtils.isEmpty(defaultSchema) ? null : defaultSchema; + } + + @NotNull + public WebSQLExecuteInfo executeQuery(@NotNull String sql, @Nullable WebSQLDataFilter filter) throws DBException { + return processor.executeQuery(this, sql, filter); + } + + @NotNull + public WebSQLResultsInfo saveResult(@NotNull DBSDataContainer dataContainer, @NotNull DBDAttributeBinding[] attributes) { + WebSQLResultsInfo resultInfo = new WebSQLResultsInfo( + dataContainer, + String.valueOf(resultId.incrementAndGet()) + ); + resultInfo.setAttributes(attributes); + resultInfoMap.put(resultInfo.getId(), resultInfo); + return resultInfo; + } + + @NotNull + public WebSQLResultsInfo getResults(@NotNull String resultId) throws DBWebException { + WebSQLResultsInfo resultsInfo = resultInfoMap.get(resultId); + if (resultsInfo == null) { + throw new DBWebException("Results '" + resultId + "' not found in context '" + getId() + "'@'" + processor.getConnection().getId() + "'"); + } + return resultsInfo; + } + + @NotNull + public Boolean closeResult(@NotNull String resultId) { + return resultInfoMap.remove(resultId) != null; + } + + public @NotNull WebSQLCompletionProposal[] getCompletionProposals(@NotNull String query, Integer position, Integer maxResults) throws DBWebException { + try { + DBPDataSource dataSource = processor.getConnection().getDataSourceContainer().getDataSource(); + Document document = new Document(); + document.set(query); + SQLScriptElement activeQuery = new SQLQuery(dataSource, query); + SQLCompletionRequest request = new SQLCompletionRequest( + new WebSQLCompletionContext(this), + document, + position == null ? 0 : position, + activeQuery, + false + ); + SQLCompletionAnalyzer analyzer = new SQLCompletionAnalyzer(request); + analyzer.runAnalyzer(processor.getWebSession().getProgressMonitor()); + List proposals = analyzer.getProposals(); + if (maxResults == null) maxResults = 200; + if (proposals.size() > maxResults) { + proposals = proposals.subList(0, maxResults); + } + + WebSQLCompletionProposal[] result = new WebSQLCompletionProposal[proposals.size()]; + for (int i = 0; i < proposals.size(); i++) { + result[i] = new WebSQLCompletionProposal(proposals.get(i)); + } + return result; + } catch (DBException e) { + throw new DBWebException("Error processing SQL proposals", e); + } + } + + /////////////////////////////////////////////////////// + // Async model + + @NotNull + public WebAsyncTaskInfo asyncExecuteQuery(@NotNull String sql, @Nullable WebSQLDataFilter filter) throws DBException { + DBRRunnableWithResult runnable = new DBRRunnableWithResult() { + @Override + public void run(DBRProgressMonitor monitor) throws InvocationTargetException, InterruptedException { + try { + result = processor.processQuery(monitor, WebSQLContextInfo.this, sql, filter); + } catch (Throwable e) { + throw new InvocationTargetException(e); + } + } + }; + return processor.getWebSession().createAndRunAsyncTask("SQL execute", runnable); + } + + public void destroy() { + resultInfoMap.clear(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilter.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilter.java new file mode 100644 index 0000000000..60b0f75a42 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilter.java @@ -0,0 +1,114 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.model.data.DBDAttributeConstraint; +import org.jkiss.dbeaver.model.data.DBDDataFilter; +import org.jkiss.dbeaver.model.exec.DBCLogicalOperator; +import org.jkiss.utils.CommonUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Web SQL data filter. + */ +public class WebSQLDataFilter { + + private static final int DEFAULT_ROWS_NUMBER = 200; + private static final int MAX_ROWS_NUMBER = 100000; + + + private int offset; + private int limit ; + private String where; + private List constraints = new ArrayList<>(); + + public WebSQLDataFilter() { + this.offset = 0; + this.limit = DEFAULT_ROWS_NUMBER; + } + + public WebSQLDataFilter(Map filterProps) { + this.offset = CommonUtils.toInt(filterProps.get("offset")); + this.limit = CommonUtils.toInt(filterProps.get("limit")); + if (this.limit <= 0) { + this.limit = DEFAULT_ROWS_NUMBER; + } else if (this.limit > MAX_ROWS_NUMBER) { + this.limit = MAX_ROWS_NUMBER; + } + this.where = CommonUtils.toString(filterProps.get("where"), null); + Object constraints = filterProps.get("constraints"); + if (constraints instanceof Collection) { + for (Object constrItem : (Collection)constraints) { + if (constrItem instanceof Map) { + this.constraints.add( + new WebSQLDataFilterConstraint((Map)constrItem)); + } + } + } + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public int getLimit() { + return limit; + } + + public void setLimit(int limit) { + this.limit = limit; + } + + public List getConstraints() { + return constraints; + } + + public DBDDataFilter makeDataFilter() { + DBDDataFilter dataFilter = new DBDDataFilter(); + dataFilter.setWhere(where); + if (!CommonUtils.isEmpty(constraints)) { + List dbdConstraints = new ArrayList<>(); + for (WebSQLDataFilterConstraint webConstr : constraints) { + DBDAttributeConstraint dbConstraint = new DBDAttributeConstraint(webConstr.getAttribute(), -1); + if (webConstr.getOrderPosition() != null) { + dbConstraint.setOrderPosition(webConstr.getOrderPosition()); + } + if (webConstr.getOrderAsc() != null) { + dbConstraint.setOrderDescending(!webConstr.getOrderAsc()); + } + dbConstraint.setCriteria(webConstr.getCriteria()); + if (webConstr.getOperator() != null) { + dbConstraint.setOperator(CommonUtils.valueOf(DBCLogicalOperator.class, webConstr.getOperator())); + } + if (webConstr.getValue() != null) { + dbConstraint.setValue(webConstr.getValue()); + } + dbdConstraints.add(dbConstraint); + } + dataFilter.addConstraints(dbdConstraints); + } + return dataFilter; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilterConstraint.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilterConstraint.java new file mode 100644 index 0000000000..fdefe1f9fa --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDataFilterConstraint.java @@ -0,0 +1,71 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.utils.CommonUtils; + +import java.util.Map; + +/** + * Web SQL data filter constraint. + */ +public class WebSQLDataFilterConstraint { + + private String attribute; + private Integer orderPosition; + private Boolean orderAsc; + + private String criteria; + private String operator; + private Object value; + + public WebSQLDataFilterConstraint(Map map) { + this.attribute = CommonUtils.toString(map.get("attribute")); + this.orderPosition = map.containsKey("orderPosition") ? + CommonUtils.toInt(map.get("orderPosition")) + 1 : // Use position + 1 because 0 means no ordering (because of legacy compatibility) + null; + this.orderAsc = map.containsKey("orderAsc") ? CommonUtils.toBoolean(map.get("orderAsc")) : null; + + this.criteria = CommonUtils.toString(map.get("criteria"), null); + this.operator = CommonUtils.toString(map.get("operator"), null); + this.value = CommonUtils.toString(map.get("value"), null); + } + + public String getAttribute() { + return attribute; + } + + public Integer getOrderPosition() { + return orderPosition; + } + + public Boolean getOrderAsc() { + return orderAsc; + } + + public String getCriteria() { + return criteria; + } + + public String getOperator() { + return operator; + } + + public Object getValue() { + return value; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDialectInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDialectInfo.java new file mode 100644 index 0000000000..5df0cd5ce1 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLDialectInfo.java @@ -0,0 +1,83 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.sql.SQLDialect; +import org.jkiss.utils.Pair; + +/** + * Web SQL dialect. + */ +public class WebSQLDialectInfo { + + private static final Log log = Log.getLog(WebSQLDialectInfo.class); + + private final DBPDataSource dataSource; + private final SQLDialect dialect; + + public WebSQLDialectInfo(DBPDataSource dataSource, SQLDialect dialect) { + this.dataSource = dataSource; + this.dialect = dialect; + } + + public String getName() { + return dialect.getDialectName(); + } + + public String[] getDataTypes() { + return dialect.getDataTypes(dataSource).toArray(new String[0]); + } + + public String[] getFunctions() { + return dialect.getFunctions(dataSource).toArray(new String[0]); + } + + public String[] getReservedWords() { + return dialect.getReservedWords().toArray(new String[0]); + } + + public String[][] getQuoteStrings() { + return dialect.getIdentifierQuoteStrings(); + } + + public String[] getSingleLineComments() { + return dialect.getSingleLineComments(); + } + + public String[][] getMultiLineComments() { + Pair mlComments = dialect.getMultiLineComments(); + if (mlComments == null) { + return new String[0][]; + } + return new String[][] { { mlComments.getFirst(), mlComments.getSecond() } }; + } + + public String getCatalogSeparator() { + return dialect.getCatalogSeparator(); + } + + public String getStructSeparator() { + return String.valueOf(dialect.getStructSeparator()); + } + + public String getScriptDelimiter() { + return dialect.getScriptDelimiter(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLExecuteInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLExecuteInfo.java new file mode 100644 index 0000000000..5395434329 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLExecuteInfo.java @@ -0,0 +1,59 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * WebSQLExecuteInfo. + */ +public class WebSQLExecuteInfo { + + private static final Log log = Log.getLog(WebSQLExecuteInfo.class); + + private String statusMessage; + private long duration; + private WebSQLQueryResults[] results; + + @Property + public String getStatusMessage() { + return statusMessage; + } + + public void setStatusMessage(String statusMessage) { + this.statusMessage = statusMessage; + } + + @Property + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + @Property + public WebSQLQueryResults[] getResults() { + return results; + } + + public void setResults(WebSQLQueryResults[] results) { + this.results = results; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLProcessor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLProcessor.java new file mode 100644 index 0000000000..59b217fe2e --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLProcessor.java @@ -0,0 +1,488 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.server.model.WebConnectionInfo; +import io.cloudbeaver.server.model.WebDatabaseObjectInfo; +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.DBUtils; +import org.jkiss.dbeaver.model.data.*; +import org.jkiss.dbeaver.model.exec.*; +import org.jkiss.dbeaver.model.impl.data.DBDValueError; +import org.jkiss.dbeaver.model.impl.struct.ContextDefaultObjectsReader; +import org.jkiss.dbeaver.model.meta.RuntimeAction; +import org.jkiss.dbeaver.model.navigator.DBNDatabaseItem; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.sql.SQLDialect; +import org.jkiss.dbeaver.model.sql.SQLSyntaxManager; +import org.jkiss.dbeaver.model.sql.SQLUtils; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.model.struct.DBSDataManipulator; +import org.jkiss.dbeaver.model.struct.DBSEntity; +import org.jkiss.dbeaver.model.struct.DBSObject; +import org.jkiss.dbeaver.model.struct.rdb.DBSCatalog; +import org.jkiss.utils.CommonUtils; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Web SQL processor. + */ +public class WebSQLProcessor { + + private static final Log log = Log.getLog(WebSQLProcessor.class); + + private static final int MAX_RESULTS_COUNT = 100; + + private final WebSession webSession; + private final WebConnectionInfo connection; + private final SQLSyntaxManager syntaxManager; + private final Map contexts = new LinkedHashMap<>(); + + private AtomicInteger contextId = new AtomicInteger(); + + public WebSQLProcessor(@NotNull WebSession webSession, @NotNull WebConnectionInfo connection) { + this.webSession = webSession; + this.connection = connection; + + syntaxManager = new SQLSyntaxManager(); + syntaxManager.init(connection.getDataSource()); + } + + public WebConnectionInfo getConnection() { + return connection; + } + + WebSession getWebSession() { + return webSession; + } + + SQLSyntaxManager getSyntaxManager() { + return syntaxManager; + } + + DBCExecutionContext getExecutionContext() { + return DBUtils.getDefaultContext(connection.getDataSource(), false); + } + + private DBCExecutionContext getExecutionContext(@NotNull DBSDataContainer dataContainer) { + return DBUtils.getDefaultContext(dataContainer, false); + } + + @RuntimeAction + @NotNull + public WebSQLDialectInfo getDialectInfo() throws DBWebException { + DBPDataSource dataSource = connection.getDataSourceContainer().getDataSource(); + SQLDialect dialect = SQLUtils.getDialectFromDataSource(dataSource); + return new WebSQLDialectInfo(dataSource, dialect); + } + + @NotNull + public WebSQLContextInfo createContext(String defaultCatalog, String defaultSchema) { + String contextId = String.valueOf(this.contextId.incrementAndGet()); + WebSQLContextInfo contextInfo = new WebSQLContextInfo(this, contextId, defaultCatalog, defaultSchema); + synchronized (contexts) { + contexts.put(contextId, contextInfo); + } + return contextInfo; + } + + @Nullable + public WebSQLContextInfo getContext(@NotNull String contextId) { + synchronized (contexts) { + return contexts.get(contextId); + } + } + + @NotNull + public WebSQLContextInfo[] getContexts() { + synchronized (contexts) { + return contexts.values().toArray(new WebSQLContextInfo[0]); + } + } + + public void destroyContext(@NotNull String contextId) { + WebSQLContextInfo contextInfo; + synchronized (contexts) { + contextInfo = contexts.get(contextId); + } + if (contextInfo != null) { + contextInfo.destroy(); + } + } + + @RuntimeAction + @NotNull + public WebSQLExecuteInfo executeQuery(WebSQLContextInfo contextInfo, @NotNull String sql, @Nullable WebSQLDataFilter filter) throws DBWebException { + return processQuery(webSession.getProgressMonitor(), contextInfo, sql, filter); + } + + @NotNull + WebSQLExecuteInfo processQuery(DBRProgressMonitor monitor, WebSQLContextInfo contextInfo, @NotNull String sql, @Nullable WebSQLDataFilter filter) throws DBWebException { + if (filter == null) { + // Use default filter + filter = new WebSQLDataFilter(); + } + long startTime = System.currentTimeMillis(); + WebSQLExecuteInfo executeInfo = new WebSQLExecuteInfo(); + + DBSDataContainer dataContainer = new WebSQLQueryDataContainer(connection.getDataSource(), sql); + + DBCExecutionContext context = getExecutionContext(dataContainer); + + { + DBDDataFilter dataFilter = filter.makeDataFilter(); + if (dataFilter.hasFilters()) { + sql = context.getDataSource().getSQLDialect().addFiltersToQuery( + context.getDataSource(), sql, dataFilter); + } + } + + final WebSQLDataFilter dataFilter = filter; + final String sqlQuery = sql; + + try { + DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { + try (DBCSession session = context.openSession(monitor, DBCExecutionPurpose.USER, "Execute SQL")) { + try (DBCStatement dbStat = session.prepareStatement(DBCStatementType.SCRIPT, sqlQuery, false, false, false)) { + dbStat.setLimit(dataFilter.getOffset(), dataFilter.getLimit()); + boolean hasResultSet = dbStat.executeStatement(); + fillQueryResults(contextInfo, dataContainer, dbStat, hasResultSet, executeInfo, dataFilter); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + } + }); + } catch (DBException e) { + throw new DBWebException("Error executing query", e); + } + executeInfo.setDuration(System.currentTimeMillis() - startTime); + if (executeInfo.getResults().length == 0) { + executeInfo.setStatusMessage("No Data"); + } else { + executeInfo.setStatusMessage("Success"); + } + + return executeInfo; + } + + @RuntimeAction + @NotNull + public WebSQLExecuteInfo readDataFromContainer(@NotNull WebSQLContextInfo contextInfo, @NotNull String containerPath, @Nullable WebSQLDataFilter filter) throws DBWebException { + try { + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + + DBSDataContainer dataContainer = getDataContainerByNodePath(monitor, containerPath, DBSDataContainer.class); + + if (filter == null) { + // Use default filter + filter = new WebSQLDataFilter(); + } + + return readDataFromContainer(contextInfo, monitor, dataContainer, filter); + } catch (DBException e) { + if (e instanceof DBWebException) throw (DBWebException) e; + throw new DBWebException("Error reading data from '" + containerPath + "'", e); + } + } + + @NotNull + private WebSQLExecuteInfo readDataFromContainer(@NotNull WebSQLContextInfo contextInfo, @NotNull DBRProgressMonitor monitor, @NotNull DBSDataContainer dataContainer, @NotNull WebSQLDataFilter filter) throws DBException { + + WebSQLExecuteInfo executeInfo = new WebSQLExecuteInfo(); + + DBCExecutionContext executionContext = getExecutionContext(dataContainer); + DBDDataFilter dataFilter = filter.makeDataFilter(); + DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Read data from container")) { + try (WebDataReceiver dataReceiver = new WebDataReceiver(contextInfo, dataContainer)) { + DBCStatistics statistics = dataContainer.readData( + new WebExecutionSource(dataContainer, executionContext, this), + session, + dataReceiver, + dataFilter, + filter.getOffset(), + filter.getLimit(), + DBSDataContainer.FLAG_NONE, + filter.getLimit()); + executeInfo.setDuration(statistics.getTotalTime()); + + WebSQLQueryResults results = new WebSQLQueryResults(); + results.setResultSet(dataReceiver.getResultSet()); + executeInfo.setResults(new WebSQLQueryResults[]{results}); + + executeInfo.setStatusMessage(dataReceiver.getResultSet().getRows().length + " row(s) fetched"); + } catch (DBCException e) { + throw new InvocationTargetException(e); + } + } + }); + return executeInfo; + } + + @RuntimeAction + public WebSQLExecuteInfo updateResultsData( + @NotNull WebSQLContextInfo contextInfo, + @NotNull String resultsId, + @NotNull List updateRow, + @NotNull Map updateValues) throws DBWebException + { + WebSQLResultsInfo resultsInfo = contextInfo.getResults(resultsId); + + DBDRowIdentifier rowIdentifier = resultsInfo.getDefaultRowIdentifier(); + if (rowIdentifier == null) { + throw new DBWebException("Can't detect row identifier for results '" + resultsId + "'"); + } + DBSEntity dataContainer = rowIdentifier.getEntity(); + if (!(dataContainer instanceof DBSDataManipulator)) { + throw new DBWebException("Data container '" + dataContainer.getName() + "' is not editable"); + } + DBSDataManipulator dataManipulator = (DBSDataManipulator) dataContainer; + + DBRProgressMonitor monitor = webSession.getProgressMonitor(); + WebSQLExecuteInfo result = new WebSQLExecuteInfo(); + + try { + DBExecUtils.tryExecuteRecover(monitor, connection.getDataSource(), param -> { + try { + DBCExecutionContext executionContext = getExecutionContext(dataManipulator); + try (DBCSession session = executionContext.openSession(monitor, DBCExecutionPurpose.USER, "Update data in container")) { + WebExecutionSource executionSource = new WebExecutionSource(dataManipulator, executionContext, this); + + DBDAttributeBinding[] allAttributes = resultsInfo.getAttributes(); + DBDAttributeBinding[] keyAttributes = rowIdentifier.getAttributes().toArray(new DBDAttributeBinding[0]); + DBDAttributeBinding[] updateAttributes = new DBDAttributeBinding[updateValues.size()]; + // Final row is what we return back + Object[] finalRow = updateRow.toArray(); + + int index = 0; + for (String indexStr : updateValues.keySet()) { + int attrIndex = CommonUtils.toInt(indexStr, -1); + updateAttributes[index++] = allAttributes[attrIndex]; + } + + Object[] rowValues = new Object[updateAttributes.length + keyAttributes.length]; + for (int i = 0; i < updateAttributes.length; i++) { + DBDAttributeBinding updateAttribute = updateAttributes[i]; + Object cellRawValue = updateValues.get(String.valueOf(updateAttribute.getOrdinalPosition())); + Object realCellValue = updateAttribute.getValueHandler().getValueFromObject(session, updateAttribute, cellRawValue, false, true); + rowValues[i] = realCellValue; + finalRow[updateAttribute.getOrdinalPosition()] = WebSQLUtils.makeWebCellValue(monitor, null, realCellValue); + } + for (int i = 0; i < keyAttributes.length; i++) { + DBDAttributeBinding keyAttribute = keyAttributes[i]; + Object cellValueRaw = updateRow.get(keyAttribute.getOrdinalPosition()); + rowValues[updateAttributes.length + i] = keyAttribute.getValueHandler().getValueFromObject(session, keyAttribute, cellValueRaw, false, true); + } + + DBSDataManipulator.ExecuteBatch updateBatch = dataManipulator.updateData(session, updateAttributes, keyAttributes, null, executionSource); + updateBatch.add(rowValues); + DBCStatistics statistics = updateBatch.execute(session); + + // Returns fake resultset with updated row values + WebSQLQueryResultSet updatedResultSet = new WebSQLQueryResultSet(); + updatedResultSet.setResultsInfo(resultsInfo); + updatedResultSet.setColumns(resultsInfo.getAttributes()); + updatedResultSet.setRows(new Object[][]{finalRow}); + + WebSQLQueryResults updateResults = new WebSQLQueryResults(); + updateResults.setUpdateRowCount((int) statistics.getRowsUpdated()); + updateResults.setResultSet(updatedResultSet); + result.setDuration(statistics.getExecuteTime()); + result.setResults(new WebSQLQueryResults[]{updateResults}); + } + } catch (Exception e) { + throw new InvocationTargetException(e); + } + }); + } catch (DBException e) { + throw new DBWebException("Error updating data", e); + } + return result; + } + + @NotNull + private T getDataContainerByNodePath(DBRProgressMonitor monitor, @NotNull String containerPath, Class type) throws DBException { + DBNNode node = webSession.getNavigatorModel().getNodeByPath(monitor, containerPath); + if (node == null) { + throw new DBWebException("Container node '" + containerPath + "' not found"); + } + if (!(node instanceof DBNDatabaseItem)) { + throw new DBWebException("Container node '" + containerPath + "' is not a database item"); + } + DBSObject object = ((DBNDatabaseItem) node).getObject(); + if (!type.isInstance(object)) { + throw new DBWebException("Container node '" + containerPath + "' doesn't implement " + type.getName()); + } + return type.cast(object); + } + + + private void fillQueryResults(WebSQLContextInfo contextInfo, @NotNull DBSDataContainer dataContainer, @NotNull DBCStatement dbStat, boolean hasResultSet, @NotNull WebSQLExecuteInfo executeInfo, @NotNull WebSQLDataFilter filter) throws DBCException { + + List resultList = new ArrayList<>(); + for (int i = 0; i < MAX_RESULTS_COUNT; i++) { + WebSQLQueryResults results = new WebSQLQueryResults(); + if (hasResultSet) { + DBCResultSet resultSet = dbStat.openResultSet(); + if (resultSet == null) { + break; + } + try (WebDataReceiver dataReceiver = new WebDataReceiver(contextInfo, dataContainer)) { + readResultSet(dbStat.getSession(), resultSet, filter, dataReceiver); + results.setResultSet(dataReceiver.getResultSet()); + } + } else { + int updateRowCount = dbStat.getUpdateRowCount(); + if (updateRowCount >= 0) { + results.setUpdateRowCount(updateRowCount); + } else { + break; + } + } + resultList.add(results); + hasResultSet = dbStat.nextResults(); + } + + executeInfo.setResults(resultList.toArray(new WebSQLQueryResults[0])); + } + + private void readResultSet(@NotNull DBCSession session, @NotNull DBCResultSet dbResult, @NotNull WebSQLDataFilter filter, @NotNull WebDataReceiver dataReceiver) throws DBCException { + dataReceiver.fetchStart(session, dbResult, filter.getOffset(), filter.getLimit()); + int rowCount = 0; + while (dbResult.nextRow()) { + if (rowCount > filter.getLimit()) { + break; + } + + dataReceiver.fetchRow(session, dbResult); + rowCount++; + } + dataReceiver.fetchEnd(session, dbResult); + } + + public WebStructContainers getStructContainers(String catalog) throws DBWebException { + DBPDataSource dataSource = connection.getDataSource(); + ContextDefaultObjectsReader reader = new ContextDefaultObjectsReader(dataSource, getExecutionContext()); + reader.setReadNodes(false); + try { + reader.run(webSession.getProgressMonitor()); + } catch (InvocationTargetException e) { + throw new DBWebException("Error reading context defaults", e.getTargetException()); + } catch (InterruptedException e) { + // ignore + } + WebStructContainers structContainers = new WebStructContainers(); + for (DBSObject node : reader.getObjectList()) { + if (!dataSource.getContainer().getNavigatorSettings().isShowSystemObjects() && DBUtils.isSystemObject(node)) { + continue; + } + List objectInfos = node instanceof DBSCatalog ? structContainers.getCatalogList() : structContainers.getSchemaList(); + objectInfos.add(new WebDatabaseObjectInfo(webSession, node)); + } + return structContainers; + } + + /////////////////////////////////////////////////////// + // Utils + + private class WebDataReceiver implements DBDDataReceiver { + private final WebSQLContextInfo contextInfo; + private DBSDataContainer dataContainer; + private WebSQLQueryResultSet webResultSet = new WebSQLQueryResultSet(); + + private DBDAttributeBindingMeta[] bindings; + private List rows = new ArrayList<>(); + + public WebDataReceiver(WebSQLContextInfo contextInfo, DBSDataContainer dataContainer) { + this.contextInfo = contextInfo; + this.dataContainer = dataContainer; + } + + public WebSQLQueryResultSet getResultSet() { + return webResultSet; + } + + @Override + public void fetchStart(DBCSession session, DBCResultSet dbResult, long offset, long maxRows) throws DBCException { + DBCResultSetMetaData meta = dbResult.getMeta(); + List attributes = meta.getAttributes(); + bindings = new DBDAttributeBindingMeta[attributes.size()]; + for (int i = 0; i < attributes.size(); i++) { + DBCAttributeMetaData attrMeta = attributes.get(i); + bindings[i] = new DBDAttributeBindingMeta(dataContainer, dbResult.getSession(), attrMeta); + } + } + + @Override + public void fetchRow(DBCSession session, DBCResultSet resultSet) throws DBCException { + Object[] row = new Object[bindings.length]; + + for (int i = 0; i < bindings.length; i++) { + DBDAttributeBindingMeta binding = bindings[i]; + try { + Object cellValue = binding.getValueHandler().fetchValueObject( + resultSet.getSession(), + resultSet, + binding.getMetaAttribute(), + i); + row[i] = WebSQLUtils.makeWebCellValue(session.getProgressMonitor(), binding, cellValue); + } catch (Throwable e) { + row[i] = new DBDValueError(e); + } + } + + rows.add(row); + } + + @Override + public void fetchEnd(DBCSession session, DBCResultSet resultSet) throws DBCException { + webResultSet.setRows(rows.toArray(new Object[0][])); + + DBSEntity entity = dataContainer instanceof DBSEntity ? (DBSEntity) dataContainer : null; + + try { + DBExecUtils.bindAttributes(session, entity, resultSet, bindings, rows); + } catch (DBException e) { + log.error("Error binding attributes", e); + } + + webResultSet.setColumns(bindings); + + WebSQLResultsInfo resultsInfo = contextInfo.saveResult(dataContainer, bindings); + webResultSet.setResultsInfo(resultsInfo); + } + + @Override + public void close() { + rows.clear(); + } + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryDataContainer.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryDataContainer.java new file mode 100644 index 0000000000..4b1961d578 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryDataContainer.java @@ -0,0 +1,91 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBPDataSource; +import org.jkiss.dbeaver.model.data.DBDDataFilter; +import org.jkiss.dbeaver.model.data.DBDDataReceiver; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.exec.DBCExecutionSource; +import org.jkiss.dbeaver.model.exec.DBCSession; +import org.jkiss.dbeaver.model.exec.DBCStatistics; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; +import org.jkiss.dbeaver.model.struct.DBSObject; + +/** + * Web SQL query data container. + */ +public class WebSQLQueryDataContainer implements DBSDataContainer { + + private static final Log log = Log.getLog(WebSQLQueryDataContainer.class); + + private DBPDataSource dataSource; + private String query; + + public WebSQLQueryDataContainer(DBPDataSource dataSource, String query) { + this.dataSource = dataSource; + this.query = query; + } + + @Nullable + @Override + public DBSObject getParentObject() { + return dataSource; + } + + @NotNull + @Override + public String getName() { + return query; + } + + @Override + public boolean isPersisted() { + return false; + } + + @Nullable + @Override + public String getDescription() { + return null; + } + + @Nullable + @Override + public DBPDataSource getDataSource() { + return dataSource; + } + + @Override + public int getSupportedFeatures() { + return DATA_SELECT | DATA_FILTER | DATA_COUNT; + } + + @NotNull + @Override + public DBCStatistics readData(@NotNull DBCExecutionSource source, @NotNull DBCSession session, @NotNull DBDDataReceiver dataReceiver, @Nullable DBDDataFilter dataFilter, long firstRow, long maxRows, long flags, int fetchSize) throws DBCException { + return null; + } + + @Override + public long countData(@NotNull DBCExecutionSource source, @NotNull DBCSession session, @Nullable DBDDataFilter dataFilter, long flags) throws DBCException { + return 0; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultColumn.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultColumn.java new file mode 100644 index 0000000000..e3d7c3fd56 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultColumn.java @@ -0,0 +1,92 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.DBValueFormatting; +import org.jkiss.dbeaver.model.data.DBDAttributeBinding; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web SQL query resultset. + */ +public class WebSQLQueryResultColumn { + + private static final Log log = Log.getLog(WebSQLQueryResultColumn.class); + + private final DBDAttributeBinding attrMeta; + + public WebSQLQueryResultColumn(DBDAttributeBinding attrMeta) { + this.attrMeta = attrMeta; + } + + @Property + public Integer getPosition() { + return attrMeta.getOrdinalPosition(); + } + + @Property + public String getName() { + return attrMeta.getName(); + } + + @Property + public String getLabel() { + return attrMeta.getLabel(); + } + + @Property + public String getIcon() { + return DBValueFormatting.getObjectImage(attrMeta).getLocation(); + } + + @Property + public String getEntityName() { + return attrMeta.getMetaAttribute().getEntityName(); + } + + @Property + public String getDataKind() { + return attrMeta.getDataKind().name(); + } + + @Property + public String getTypeName() { + return attrMeta.getTypeName(); + } + + @Property + public String getFullTypeName() { + return attrMeta.getFullTypeName(); + } + + @Property + public long getMaxLength() { + return attrMeta.getMaxLength(); + } + + @Property + public Integer getScale() { + return attrMeta.getScale(); + } + + @Property + public Integer getPrecision() { + return attrMeta.getPrecision(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultSet.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultSet.java new file mode 100644 index 0000000000..4b5a50e975 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResultSet.java @@ -0,0 +1,82 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.DBDAttributeBinding; +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web SQL query resultset. + */ +public class WebSQLQueryResultSet { + + private static final Log log = Log.getLog(WebSQLQueryResultSet.class); + + private WebSQLQueryResultColumn[] columns; + private Object[][] rows; + private boolean hasMoreData; + private WebSQLResultsInfo resultsInfo; + + @Property + public String getId() { + return resultsInfo == null ? null : resultsInfo.getId(); + } + + @Property + public WebSQLQueryResultColumn[] getColumns() { + return columns; + } + + public void setColumns(WebSQLQueryResultColumn[] columns) { + this.columns = columns; + } + + public void setColumns(DBDAttributeBinding[] bindings) { + WebSQLQueryResultColumn[] columns = new WebSQLQueryResultColumn[bindings.length]; + for (int i = 0; i < bindings.length; i++) { + columns[i] = new WebSQLQueryResultColumn(bindings[i]); + } + this.columns = columns; + } + + @Property + public Object[][] getRows() { + return rows; + } + + public void setRows(Object[][] rows) { + this.rows = rows; + } + + @Property + public boolean isHasMoreData() { + return hasMoreData; + } + + public void setHasMoreData(boolean hasMoreData) { + this.hasMoreData = hasMoreData; + } + + public void setResultsInfo(WebSQLResultsInfo resultsInfo) { + this.resultsInfo = resultsInfo; + } + + public WebSQLResultsInfo getResultsInfo() { + return resultsInfo; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResults.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResults.java new file mode 100644 index 0000000000..b6e68d8c4c --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLQueryResults.java @@ -0,0 +1,46 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.dbeaver.model.meta.Property; + +/** + * Web SQL query results. + */ +public class WebSQLQueryResults { + + private Integer updateRowCount; + private WebSQLQueryResultSet resultSet; + + @Property + public Integer getUpdateRowCount() { + return updateRowCount; + } + + public void setUpdateRowCount(int updateRowCount) { + this.updateRowCount = updateRowCount; + } + + @Property + public WebSQLQueryResultSet getResultSet() { + return resultSet; + } + + public void setResultSet(WebSQLQueryResultSet resultSet) { + this.resultSet = resultSet; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLResultsInfo.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLResultsInfo.java new file mode 100644 index 0000000000..d7d03d4385 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLResultsInfo.java @@ -0,0 +1,70 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import org.jkiss.code.NotNull; +import org.jkiss.code.Nullable; +import org.jkiss.dbeaver.model.data.DBDAttributeBinding; +import org.jkiss.dbeaver.model.data.DBDRowIdentifier; +import org.jkiss.dbeaver.model.struct.DBSDataContainer; + +/** + * Web query results info. + */ +public class WebSQLResultsInfo { + + @NotNull + private final DBSDataContainer dataContainer; + @NotNull + private final String id; + private DBDAttributeBinding[] attributes; + + public WebSQLResultsInfo(@NotNull DBSDataContainer dataContainer, @NotNull String id) { + this.dataContainer = dataContainer; + this.id = id; + } + + @NotNull + public String getId() { + return id; + } + + @NotNull + public DBSDataContainer getDataContainer() { + return dataContainer; + } + + public DBDAttributeBinding[] getAttributes() { + return attributes; + } + + public void setAttributes(DBDAttributeBinding[] attributes) { + this.attributes = attributes; + } + + @Nullable + public DBDRowIdentifier getDefaultRowIdentifier() { + for (DBDAttributeBinding column : attributes) { + DBDRowIdentifier rowIdentifier = column.getRowIdentifier(); + if (rowIdentifier != null) { + return rowIdentifier; + } + } + return null; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLUtils.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLUtils.java new file mode 100644 index 0000000000..969e7b98c7 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebSQLUtils.java @@ -0,0 +1,94 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import io.cloudbeaver.server.CloudbeaverConstants; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.data.*; +import org.jkiss.dbeaver.model.exec.DBCException; +import org.jkiss.dbeaver.model.gis.DBGeometry; +import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor; +import org.jkiss.dbeaver.model.struct.DBSAttributeBase; +import org.jkiss.dbeaver.model.struct.DBSTypedObject; +import org.jkiss.dbeaver.utils.ContentUtils; + +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Web SQL utils. + */ +public class WebSQLUtils { + + private static final Log log = Log.getLog(WebSQLUtils.class); + + public static Object makeWebCellValue(DBRProgressMonitor monitor, DBSTypedObject type, Object cellValue) throws DBCException { + if (cellValue instanceof Date) { + return CloudbeaverConstants.ISO_DATE_FORMAT.format(cellValue); + } + if (cellValue instanceof DBDValue) { + DBDValue dbValue = (DBDValue) cellValue; + if (dbValue.isNull()) { + return null; + } + if (dbValue instanceof DBDComplexValue) { + return serializeComplexValue(monitor, (DBDComplexValue)dbValue); + } else if (dbValue instanceof DBGeometry) { + return serializeGeometryValue((DBGeometry)dbValue); + } else if (dbValue instanceof DBDContent) { + return serializeContentValue(monitor, (DBDContent)dbValue); + } else if (dbValue instanceof DBDDocument) { + return serializeDocumentValue((DBDDocument)dbValue); + } + } + return cellValue; + } + + private static Object serializeComplexValue(DBRProgressMonitor monitor, DBDComplexValue value) throws DBCException { + if (value instanceof DBDCollection) { + DBDCollection collection = (DBDCollection) value; + int size = collection.getItemCount(); + Object[] items = new Object[size]; + for (int i = 0; i < size; i++) { + items[i] = makeWebCellValue(monitor, collection.getComponentType(), collection.getItem(i)); + } + return items; + } else if (value instanceof DBDComposite) { + DBDComposite composite = (DBDComposite)value; + Map map = new LinkedHashMap<>(); + for (DBSAttributeBase attr : composite.getAttributes()) { + map.put(attr.getName(), makeWebCellValue(monitor, attr, composite.getAttributeValue(attr))); + } + return map; + } + return value.toString(); + } + + private static Object serializeContentValue(DBRProgressMonitor monitor, DBDContent value) throws DBCException { + return ContentUtils.getContentStringValue(monitor, value); + } + + private static Object serializeDocumentValue(DBDDocument value) { + return value; + } + + private static Object serializeGeometryValue(DBGeometry value) { + return value.toString(); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebStructContainers.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebStructContainers.java new file mode 100644 index 0000000000..e4d1c7dac2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/model/sql/WebStructContainers.java @@ -0,0 +1,50 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.model.sql; + +import io.cloudbeaver.server.model.WebDatabaseObjectInfo; +import org.jkiss.dbeaver.Log; + +import java.util.ArrayList; +import java.util.List; + +/** + * WebStructContainers + */ +public class WebStructContainers { + + private static final Log log = Log.getLog(WebStructContainers.class); + + private List catalogList = new ArrayList<>(); + private List schemaList = new ArrayList<>(); + + public List getCatalogList() { + return catalogList; + } + + public void setCatalogList(List catalogList) { + this.catalogList = catalogList; + } + + public List getSchemaList() { + return schemaList; + } + + public void setSchemaList(List schemaList) { + this.schemaList = schemaList; + } +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebDriverRegistry.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebDriverRegistry.java new file mode 100644 index 0000000000..5463ba92da --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebDriverRegistry.java @@ -0,0 +1,67 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.registry; + +import io.cloudbeaver.WebServiceUtils; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.Log; +import org.jkiss.dbeaver.model.connection.DBPDriver; + +import java.util.HashSet; +import java.util.Set; + +public class WebDriverRegistry { + + private static final Log log = Log.getLog(WebDriverRegistry.class); + + private static final String EXTENSION_ID = "io.cloudbeaver.driver"; //$NON-NLS-1$ + private static final String TAG_DRIVER = "driver"; //$NON-NLS-1$ + + private static WebDriverRegistry instance = null; + + public synchronized static WebDriverRegistry getInstance() { + if (instance == null) { + instance = new WebDriverRegistry(); + instance.loadExtensions(Platform.getExtensionRegistry()); + } + return instance; + } + + private final Set webDrivers = new HashSet<>(); + + private WebDriverRegistry() { + } + + private void loadExtensions(IExtensionRegistry registry) { + { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(EXTENSION_ID); + for (IConfigurationElement ext : extConfigs) { + // Load webServices + if (TAG_DRIVER.equals(ext.getName())) { + this.webDrivers.add(ext.getAttribute("id")); + } + } + } + } + + public boolean isDriverEnabled(DBPDriver driver) { + return webDrivers.contains(WebServiceUtils.makeDriverFullId(driver)); + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceDescriptor.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceDescriptor.java new file mode 100644 index 0000000000..20a2d58873 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceDescriptor.java @@ -0,0 +1,69 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.cloudbeaver.server.registry; + +import io.cloudbeaver.api.DBWService; +import org.eclipse.core.runtime.IConfigurationElement; +import org.jkiss.dbeaver.DBException; +import org.jkiss.dbeaver.model.impl.AbstractContextDescriptor; + +/** + * WebServiceDescriptor + */ +public class WebServiceDescriptor extends AbstractContextDescriptor { + + public static final String EXTENSION_ID = "io.cloudbeaver.service"; //$NON-NLS-1$ + + private final String id; + private final String label; + private final String description; + private final ObjectType implClass; + + private DBWService instance; + + public WebServiceDescriptor(IConfigurationElement config) + { + super(config); + this.id = config.getAttribute("id"); + this.label = config.getAttribute("label"); + this.description = config.getAttribute("description"); + this.implClass = new ObjectType(config.getAttribute("class")); + } + + public String getId() { + return id; + } + + public String getLabel() { + return label; + } + + public String getDescription() { + return description; + } + + public DBWService getInstance() + throws DBException + { + if (instance == null) { + instance = implClass.createInstance(DBWService.class); + } + return instance; + } + +} diff --git a/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceRegistry.java b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceRegistry.java new file mode 100644 index 0000000000..2b01053388 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/src/io/cloudbeaver/server/registry/WebServiceRegistry.java @@ -0,0 +1,80 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.server.registry; + +import io.cloudbeaver.api.DBWService; +import org.eclipse.core.runtime.IConfigurationElement; +import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; +import org.jkiss.dbeaver.Log; + +import java.util.ArrayList; +import java.util.List; + +public class WebServiceRegistry { + + private static final Log log = Log.getLog(WebServiceRegistry.class); + + private static final String TAG_SERVICE = "service"; //$NON-NLS-1$ + + private static WebServiceRegistry instance = null; + + public synchronized static WebServiceRegistry getInstance() { + if (instance == null) { + instance = new WebServiceRegistry(); + instance.loadExtensions(Platform.getExtensionRegistry()); + } + return instance; + } + + private final List webServices = new ArrayList<>(); + private DBWService[] webServiceInstances; + + private WebServiceRegistry() { + } + + private void loadExtensions(IExtensionRegistry registry) { + { + IConfigurationElement[] extConfigs = registry.getConfigurationElementsFor(WebServiceDescriptor.EXTENSION_ID); + for (IConfigurationElement ext : extConfigs) { + // Load webServices + if (TAG_SERVICE.equals(ext.getName())) { + this.webServices.add( + new WebServiceDescriptor(ext)); + } + } + } + List instances = new ArrayList<>(); + for (WebServiceDescriptor wsd : webServices) { + try { + DBWService instance = wsd.getInstance(); + instances.add(instance); + } catch (Exception e) { + log.error("Error instantiating web service '" + wsd.getId() + "'", e); + } + } + webServiceInstances = instances.toArray(new DBWService[0]); + } + + public List getWebServices() { + return webServices; + } + + public DBWService[] getWebServiceInstances() { + return webServiceInstances; + } +} diff --git a/server/bundles/io.cloudbeaver.server/static/graphiql/index.html b/server/bundles/io.cloudbeaver.server/static/graphiql/index.html new file mode 100644 index 0000000000..9df8596ed8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.server/static/graphiql/index.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + +
Loading...
+ + + \ No newline at end of file diff --git a/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF new file mode 100644 index 0000000000..3dc9c397c2 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/META-INF/MANIFEST.MF @@ -0,0 +1,23 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Vendor: %Bundle-Vendor +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: io.cloudbeaver.service.metadata;singleton:=true +Bundle-Version: 1.0.3.qualifier +Bundle-Release-Date: 20200420 +Bundle-RequiredExecutionEnvironment: JavaSE-1.8 +Bundle-ActivationPolicy: lazy +Bundle-ClassPath: . +Require-Bundle: org.eclipse.core.runtime, + org.eclipse.core.expressions, + org.apache.commons.logging, + org.eclipse.core.commands, + org.eclipse.core.resources, + io.cloudbeaver.server, + com.google.gson, + javax.servlet, + org.jkiss.bundle.graphql.java, + org.jkiss.dbeaver.model, + org.jkiss.dbeaver.model.sql +Bundle-Localization: OSGI-INF/l10n/bundle +Automatic-Module-Name: io.cloudbeaver.service.metadata diff --git a/server/bundles/io.cloudbeaver.service.metadata/OSGI-INF/l10n/bundle.properties b/server/bundles/io.cloudbeaver.service.metadata/OSGI-INF/l10n/bundle.properties new file mode 100644 index 0000000000..d87bca20aa --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/OSGI-INF/l10n/bundle.properties @@ -0,0 +1,2 @@ +Bundle-Vendor = DBeaver Corp +Bundle-Name = Cloudbeaver Web Service - Metadata diff --git a/server/bundles/io.cloudbeaver.service.metadata/build.properties b/server/bundles/io.cloudbeaver.service.metadata/build.properties new file mode 100644 index 0000000000..91bb65c8b8 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/build.properties @@ -0,0 +1,7 @@ +source.. = src/ +output.. = target/classes/ +bin.includes = .,\ + META-INF/,\ + OSGI-INF/,\ + schema/,\ + plugin.xml diff --git a/server/bundles/io.cloudbeaver.service.metadata/plugin.xml b/server/bundles/io.cloudbeaver.service.metadata/plugin.xml new file mode 100644 index 0000000000..2a7c318db9 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/plugin.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/server/bundles/io.cloudbeaver.service.metadata/pom.xml b/server/bundles/io.cloudbeaver.service.metadata/pom.xml new file mode 100644 index 0000000000..9fda95b755 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/pom.xml @@ -0,0 +1,16 @@ + + + 4.0.0 + + io.cloudbeaver + bundles + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.service.metadata + 1.0.3-SNAPSHOT + eclipse-plugin + + diff --git a/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls b/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls new file mode 100644 index 0000000000..0fde710b52 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls @@ -0,0 +1,8 @@ +# Metadata queries + +extend type Query { + + # Get child nodes + metadataGetNodeDDL(nodeId: ID!, options: Object): String + +} diff --git a/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceMetadata.java b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceMetadata.java new file mode 100644 index 0000000000..abfd63ddb9 --- /dev/null +++ b/server/bundles/io.cloudbeaver.service.metadata/src/io/cloudbeaver/service/metadata/WebServiceMetadata.java @@ -0,0 +1,80 @@ +/* + * DBeaver - Universal Database Manager + * Copyright (C) 2010-2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.cloudbeaver.service.metadata; + +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import io.cloudbeaver.DBWebException; +import io.cloudbeaver.api.DBWModel; +import io.cloudbeaver.api.DBWServiceGraphQL; +import io.cloudbeaver.api.DBWUtils; +import io.cloudbeaver.server.model.WebNavigatorNodeInfo; +import io.cloudbeaver.server.model.session.WebSession; +import org.jkiss.dbeaver.model.DBPScriptObject; +import org.jkiss.dbeaver.model.navigator.DBNDatabaseNode; +import org.jkiss.dbeaver.model.navigator.DBNNode; +import org.jkiss.dbeaver.model.struct.DBSObject; + +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Web service implementation + */ +public class WebServiceMetadata implements DBWServiceGraphQL { + + private static final String METADATA_SCHEMA_FILE_NAME = "schema/service.metadata.graphqls"; + + @Override + public TypeDefinitionRegistry getTypeDefinition() throws DBWebException { + try (InputStream schemaStream = getClass().getClassLoader().getResourceAsStream(METADATA_SCHEMA_FILE_NAME)) { + try (Reader schemaReader = new InputStreamReader(schemaStream)) { + return new SchemaParser().parse(schemaReader); + } + } catch (IOException e) { + throw new RuntimeException("Error reading core schema", e); + } + } + + @Override + public void bindWiring(DBWModel model) throws DBWebException { + model.getQueryType().dataFetcher("metadataGetNodeDDL", env -> { + WebSession webSession = model.getSessionManager().getWebSession(DBWUtils.getServletRequest(env)); + WebNavigatorNodeInfo node = webSession.getNavigatorNodeInfo(env.getArgument("nodeId")); + DBNNode dbNode = node.getNode(); + if (dbNode instanceof DBNDatabaseNode) { + DBSObject object = ((DBNDatabaseNode) dbNode).getObject(); + if (object instanceof DBPScriptObject) { + Map options = env.getArgument("options"); + if (options == null) { + options = new LinkedHashMap<>(); + } + return ((DBPScriptObject) object).getObjectDefinitionText(webSession.getProgressMonitor(), options); + } else { + throw new DBWebException("Object '" + node.getId() + "' doesn't support DDL"); + } + } else { + throw new DBWebException("Node '" + node.getId() + "' is not database node"); + } + }); + + } +} diff --git a/server/bundles/pom.xml b/server/bundles/pom.xml new file mode 100644 index 0000000000..df278d1f30 --- /dev/null +++ b/server/bundles/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + + bundles + pom + + + io.cloudbeaver.server + io.cloudbeaver.service.metadata + + io.cloudbeaver.resources.drivers.base + io.cloudbeaver.connect.provider.sample + + + diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/build.properties b/server/features/io.cloudbeaver.ce.drivers.feature/build.properties new file mode 100644 index 0000000000..64f93a9f0b --- /dev/null +++ b/server/features/io.cloudbeaver.ce.drivers.feature/build.properties @@ -0,0 +1 @@ +bin.includes = feature.xml diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml new file mode 100644 index 0000000000..efbfbfb0dc --- /dev/null +++ b/server/features/io.cloudbeaver.ce.drivers.feature/feature.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml new file mode 100644 index 0000000000..bd189d7f40 --- /dev/null +++ b/server/features/io.cloudbeaver.ce.drivers.feature/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + io.cloudbeaver + features + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.ce.drivers.feature + 1.0.23-SNAPSHOT + eclipse-feature + diff --git a/server/features/io.cloudbeaver.sample.feature/build.properties b/server/features/io.cloudbeaver.sample.feature/build.properties new file mode 100644 index 0000000000..b3a611b5c9 --- /dev/null +++ b/server/features/io.cloudbeaver.sample.feature/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/server/features/io.cloudbeaver.sample.feature/feature.properties b/server/features/io.cloudbeaver.sample.feature/feature.properties new file mode 100644 index 0000000000..41957c3c57 --- /dev/null +++ b/server/features/io.cloudbeaver.sample.feature/feature.properties @@ -0,0 +1,4 @@ +featureName=Cloudbeaver - Samples +providerName=DBeaver Corp +description=Cloudbeaver samples feature +copyright=(c) DBeaver Corp, 2020 diff --git a/server/features/io.cloudbeaver.sample.feature/feature.xml b/server/features/io.cloudbeaver.sample.feature/feature.xml new file mode 100644 index 0000000000..b543fdd1b5 --- /dev/null +++ b/server/features/io.cloudbeaver.sample.feature/feature.xml @@ -0,0 +1,22 @@ + + + + + %description + + + + %copyright + + + + %license + + + + + diff --git a/server/features/io.cloudbeaver.sample.feature/pom.xml b/server/features/io.cloudbeaver.sample.feature/pom.xml new file mode 100644 index 0000000000..a48f86b006 --- /dev/null +++ b/server/features/io.cloudbeaver.sample.feature/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + io.cloudbeaver + features + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.sample.feature + 1.0.3-SNAPSHOT + eclipse-feature + diff --git a/server/features/io.cloudbeaver.server.feature/build.properties b/server/features/io.cloudbeaver.server.feature/build.properties new file mode 100644 index 0000000000..b3a611b5c9 --- /dev/null +++ b/server/features/io.cloudbeaver.server.feature/build.properties @@ -0,0 +1,2 @@ +bin.includes = feature.xml,\ + feature.properties diff --git a/server/features/io.cloudbeaver.server.feature/feature.properties b/server/features/io.cloudbeaver.server.feature/feature.properties new file mode 100644 index 0000000000..1d34a3f3f5 --- /dev/null +++ b/server/features/io.cloudbeaver.server.feature/feature.properties @@ -0,0 +1,207 @@ +featureName=Cloudbeaver Server +providerName=DBeaver Corp +description=Cloudbeaver server feature +copyright=(c) DBeaver Corp, 2019 +licenseURL=../../docs/licenses/dbeaver_license.txt +license=\ + Apache License\n\ + Version 2.0, January 2004\n\ + http://www.apache.org/licenses/\n\ +\n\ + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\ +\n\ + 1. Definitions.\n\ +\n\ + "License" shall mean the terms and conditions for use, reproduction,\n\ + and distribution as defined by Sections 1 through 9 of this document.\n\ +\n\ + "Licensor" shall mean the copyright owner or entity authorized by\n\ + the copyright owner that is granting the License.\n\ +\n\ + "Legal Entity" shall mean the union of the acting entity and all\n\ + other entities that control, are controlled by, or are under common\n\ + control with that entity. For the purposes of this definition,\n\ + "control" means (i) the power, direct or indirect, to cause the\n\ + direction or management of such entity, whether by contract or\n\ + otherwise, or (ii) ownership of fifty percent (50%) or more of the\n\ + outstanding shares, or (iii) beneficial ownership of such entity.\n\ +\n\ + "You" (or "Your") shall mean an individual or Legal Entity\n\ + exercising permissions granted by this License.\n\ +\n\ + "Source" form shall mean the preferred form for making modifications,\n\ + including but not limited to software source code, documentation\n\ + source, and configuration files.\n\ +\n\ + "Object" form shall mean any form resulting from mechanical\n\ + transformation or translation of a Source form, including but\n\ + not limited to compiled object code, generated documentation,\n\ + and conversions to other media types.\n\ +\n\ + "Work" shall mean the work of authorship, whether in Source or\n\ + Object form, made available under the License, as indicated by a\n\ + copyright notice that is included in or attached to the work\n\ + (an example is provided in the Appendix below).\n\ +\n\ + "Derivative Works" shall mean any work, whether in Source or Object\n\ + form, that is based on (or derived from) the Work and for which the\n\ + editorial revisions, annotations, elaborations, or other modifications\n\ + represent, as a whole, an original work of authorship. For the purposes\n\ + of this License, Derivative Works shall not include works that remain\n\ + separable from, or merely link (or bind by name) to the interfaces of,\n\ + the Work and Derivative Works thereof.\n\ +\n\ + "Contribution" shall mean any work of authorship, including\n\ + the original version of the Work and any modifications or additions\n\ + to that Work or Derivative Works thereof, that is intentionally\n\ + submitted to Licensor for inclusion in the Work by the copyright owner\n\ + or by an individual or Legal Entity authorized to submit on behalf of\n\ + the copyright owner. For the purposes of this definition, "submitted"\n\ + means any form of electronic, verbal, or written communication sent\n\ + to the Licensor or its representatives, including but not limited to\n\ + communication on electronic mailing lists, source code control systems,\n\ + and issue tracking systems that are managed by, or on behalf of, the\n\ + Licensor for the purpose of discussing and improving the Work, but\n\ + excluding communication that is conspicuously marked or otherwise\n\ + designated in writing by the copyright owner as "Not a Contribution."\n\ +\n\ + "Contributor" shall mean Licensor and any individual or Legal Entity\n\ + on behalf of whom a Contribution has been received by Licensor and\n\ + subsequently incorporated within the Work.\n\ +\n\ + 2. Grant of Copyright License. Subject to the terms and conditions of\n\ + this License, each Contributor hereby grants to You a perpetual,\n\ + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n\ + copyright license to reproduce, prepare Derivative Works of,\n\ + publicly display, publicly perform, sublicense, and distribute the\n\ + Work and such Derivative Works in Source or Object form.\n\ +\n\ + 3. Grant of Patent License. Subject to the terms and conditions of\n\ + this License, each Contributor hereby grants to You a perpetual,\n\ + worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n\ + (except as stated in this section) patent license to make, have made,\n\ + use, offer to sell, sell, import, and otherwise transfer the Work,\n\ + where such license applies only to those patent claims licensable\n\ + by such Contributor that are necessarily infringed by their\n\ + Contribution(s) alone or by combination of their Contribution(s)\n\ + with the Work to which such Contribution(s) was submitted. If You\n\ + institute patent litigation against any entity (including a\n\ + cross-claim or counterclaim in a lawsuit) alleging that the Work\n\ + or a Contribution incorporated within the Work constitutes direct\n\ + or contributory patent infringement, then any patent licenses\n\ + granted to You under this License for that Work shall terminate\n\ + as of the date such litigation is filed.\n\ +\n\ + 4. Redistribution. You may reproduce and distribute copies of the\n\ + Work or Derivative Works thereof in any medium, with or without\n\ + modifications, and in Source or Object form, provided that You\n\ + meet the following conditions:\n\ +\n\ + (a) You must give any other recipients of the Work or\n\ + Derivative Works a copy of this License; and\n\ +\n\ + (b) You must cause any modified files to carry prominent notices\n\ + stating that You changed the files; and\n\ +\n\ + (c) You must retain, in the Source form of any Derivative Works\n\ + that You distribute, all copyright, patent, trademark, and\n\ + attribution notices from the Source form of the Work,\n\ + excluding those notices that do not pertain to any part of\n\ + the Derivative Works; and\n\ +\n\ + (d) If the Work includes a "NOTICE" text file as part of its\n\ + distribution, then any Derivative Works that You distribute must\n\ + include a readable copy of the attribution notices contained\n\ + within such NOTICE file, excluding those notices that do not\n\ + pertain to any part of the Derivative Works, in at least one\n\ + of the following places: within a NOTICE text file distributed\n\ + as part of the Derivative Works; within the Source form or\n\ + documentation, if provided along with the Derivative Works; or,\n\ + within a display generated by the Derivative Works, if and\n\ + wherever such third-party notices normally appear. The contents\n\ + of the NOTICE file are for informational purposes only and\n\ + do not modify the License. You may add Your own attribution\n\ + notices within Derivative Works that You distribute, alongside\n\ + or as an addendum to the NOTICE text from the Work, provided\n\ + that such additional attribution notices cannot be construed\n\ + as modifying the License.\n\ +\n\ + You may add Your own copyright statement to Your modifications and\n\ + may provide additional or different license terms and conditions\n\ + for use, reproduction, or distribution of Your modifications, or\n\ + for any such Derivative Works as a whole, provided Your use,\n\ + reproduction, and distribution of the Work otherwise complies with\n\ + the conditions stated in this License.\n\ +\n\ + 5. Submission of Contributions. Unless You explicitly state otherwise,\n\ + any Contribution intentionally submitted for inclusion in the Work\n\ + by You to the Licensor shall be under the terms and conditions of\n\ + this License, without any additional terms or conditions.\n\ + Notwithstanding the above, nothing herein shall supersede or modify\n\ + the terms of any separate license agreement you may have executed\n\ + with Licensor regarding such Contributions.\n\ +\n\ + 6. Trademarks. This License does not grant permission to use the trade\n\ + names, trademarks, service marks, or product names of the Licensor,\n\ + except as required for reasonable and customary use in describing the\n\ + origin of the Work and reproducing the content of the NOTICE file.\n\ +\n\ + 7. Disclaimer of Warranty. Unless required by applicable law or\n\ + agreed to in writing, Licensor provides the Work (and each\n\ + Contributor provides its Contributions) on an "AS IS" BASIS,\n\ + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n\ + implied, including, without limitation, any warranties or conditions\n\ + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n\ + PARTICULAR PURPOSE. You are solely responsible for determining the\n\ + appropriateness of using or redistributing the Work and assume any\n\ + risks associated with Your exercise of permissions under this License.\n\ +\n\ + 8. Limitation of Liability. In no event and under no legal theory,\n\ + whether in tort (including negligence), contract, or otherwise,\n\ + unless required by applicable law (such as deliberate and grossly\n\ + negligent acts) or agreed to in writing, shall any Contributor be\n\ + liable to You for damages, including any direct, indirect, special,\n\ + incidental, or consequential damages of any character arising as a\n\ + result of this License or out of the use or inability to use the\n\ + Work (including but not limited to damages for loss of goodwill,\n\ + work stoppage, computer failure or malfunction, or any and all\n\ + other commercial damages or losses), even if such Contributor\n\ + has been advised of the possibility of such damages.\n\ +\n\ + 9. Accepting Warranty or Additional Liability. While redistributing\n\ + the Work or Derivative Works thereof, You may choose to offer,\n\ + and charge a fee for, acceptance of support, warranty, indemnity,\n\ + or other liability obligations and/or rights consistent with this\n\ + License. However, in accepting such obligations, You may act only\n\ + on Your own behalf and on Your sole responsibility, not on behalf\n\ + of any other Contributor, and only if You agree to indemnify,\n\ + defend, and hold each Contributor harmless for any liability\n\ + incurred by, or claims asserted against, such Contributor by reason\n\ + of your accepting any such warranty or additional liability.\n\ +\n\ + END OF TERMS AND CONDITIONS\n\ +\n\ + APPENDIX: How to apply the Apache License to your work.\n\ +\n\ + To apply the Apache License to your work, attach the following\n\ + boilerplate notice, with the fields enclosed by brackets "[]"\n\ + replaced with your own identifying information. (Don't include\n\ + the brackets!) The text should be enclosed in the appropriate\n\ + comment syntax for the file format. We also recommend that a\n\ + file or class name and description of purpose be included on the\n\ + same "printed page" as the copyright notice for easier\n\ + identification within third-party archives.\n\ +\n\ + Copyright [yyyy] [name of copyright owner]\n\ +\n\ + Licensed under the Apache License, Version 2.0 (the "License");\n\ + you may not use this file except in compliance with the License.\n\ + You may obtain a copy of the License at\n\ +\n\ + http://www.apache.org/licenses/LICENSE-2.0\n\ +\n\ + Unless required by applicable law or agreed to in writing, software\n\ + distributed under the License is distributed on an "AS IS" BASIS,\n\ + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n\ + See the License for the specific language governing permissions and\n\ + limitations under the License.\n\ diff --git a/server/features/io.cloudbeaver.server.feature/feature.xml b/server/features/io.cloudbeaver.server.feature/feature.xml new file mode 100644 index 0000000000..87a92770b4 --- /dev/null +++ b/server/features/io.cloudbeaver.server.feature/feature.xml @@ -0,0 +1,49 @@ + + + + + %description + + + + %copyright + + + + %license + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/features/io.cloudbeaver.server.feature/pom.xml b/server/features/io.cloudbeaver.server.feature/pom.xml new file mode 100644 index 0000000000..b4100ae700 --- /dev/null +++ b/server/features/io.cloudbeaver.server.feature/pom.xml @@ -0,0 +1,14 @@ + + + 4.0.0 + + io.cloudbeaver + features + 1.0.0-SNAPSHOT + ../ + + io.cloudbeaver.server.feature + 7.0.3-SNAPSHOT + eclipse-feature + diff --git a/server/features/pom.xml b/server/features/pom.xml new file mode 100644 index 0000000000..113bc0ba58 --- /dev/null +++ b/server/features/pom.xml @@ -0,0 +1,22 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + + features + pom + + + io.cloudbeaver.server.feature + io.cloudbeaver.ce.drivers.feature + + io.cloudbeaver.sample.feature + + + diff --git a/server/pom.xml b/server/pom.xml new file mode 100644 index 0000000000..c737b8b94f --- /dev/null +++ b/server/pom.xml @@ -0,0 +1,54 @@ + + + + 4.0.0 + io.cloudbeaver + cloudbeaver + pom + Cloudbeaver + 1.0.0-SNAPSHOT + + + org.jkiss.dbeaver + dbeaver + 1.0.0-SNAPSHOT + ../../dbeaver/ + + + + Cloudbeaver + + + + bundles + features + + + product + + + + + + + org.eclipse.tycho + tycho-packaging-plugin + ${tycho-version} + + + + dbeaver.com + dbeaver.com + + false + + + + + + + + + diff --git a/server/product/pom.xml b/server/product/pom.xml new file mode 100644 index 0000000000..927d308ede --- /dev/null +++ b/server/product/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../ + + product + pom + + + web-server + provided-sample + + + diff --git a/server/product/provided-sample/DBeaverWebProvidedSample.product b/server/product/provided-sample/DBeaverWebProvidedSample.product new file mode 100644 index 0000000000..aa7785e97d --- /dev/null +++ b/server/product/provided-sample/DBeaverWebProvidedSample.product @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/server/product/provided-sample/pom.xml b/server/product/provided-sample/pom.xml new file mode 100644 index 0000000000..f4f4d8b9dd --- /dev/null +++ b/server/product/provided-sample/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../../ + + 7.0.1-SNAPSHOT + provided-sample + eclipse-repository + Cloudbeaver Provided Sample Product + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + all + all + all + + + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + + io.cloudbeaver.provided.sample.product + cloudbeaver-provided-sample-${dbeaver-version} + + + + + + materialize-products + + materialize-products + + + + archive-products + + + + + + + + + + diff --git a/server/product/web-server/CloudbeaverServer.product b/server/product/web-server/CloudbeaverServer.product new file mode 100644 index 0000000000..4ec6216a47 --- /dev/null +++ b/server/product/web-server/CloudbeaverServer.product @@ -0,0 +1,50 @@ + + + + + + + + + + https://dbeaver.io/about/ + + DBeaver - Universal Database Manager + Copyright (C) 2010-2020 DBeaver Corp and others + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + + + + + + + + + + + + + + + + + + + diff --git a/server/product/web-server/CloudbeaverServer.product.launch b/server/product/web-server/CloudbeaverServer.product.launch new file mode 100644 index 0000000000..26b8b6d9bb --- /dev/null +++ b/server/product/web-server/CloudbeaverServer.product.launch @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/server/product/web-server/pom.xml b/server/product/web-server/pom.xml new file mode 100644 index 0000000000..7d9e73bbbb --- /dev/null +++ b/server/product/web-server/pom.xml @@ -0,0 +1,64 @@ + + + 4.0.0 + + io.cloudbeaver + cloudbeaver + 1.0.0-SNAPSHOT + ../../ + + 7.0.3-SNAPSHOT + web-server + eclipse-repository + Cloudbeaver Server Product + + + + + org.eclipse.tycho + target-platform-configuration + ${tycho-version} + + + + all + all + all + + + + + + + org.eclipse.tycho + tycho-p2-director-plugin + ${tycho-version} + + + + io.cloudbeaver.product + cloudbeaver-server-${dbeaver-version} + + + + + + materialize-products + + materialize-products + + + + archive-products + + + + + + + + + + diff --git a/server/product/web-server/run.cmd b/server/product/web-server/run.cmd new file mode 100644 index 0000000000..289ed27939 --- /dev/null +++ b/server/product/web-server/run.cmd @@ -0,0 +1 @@ +java -jar plugins/org.eclipse.equinox.launcher_1.5.600.v20191014-2022.jar -product io.cloudbeaver.server.product -data C:/data/cloudbeaver/workspace diff --git a/webapp/.eslintignore b/webapp/.eslintignore new file mode 100644 index 0000000000..7098874c7d --- /dev/null +++ b/webapp/.eslintignore @@ -0,0 +1,3 @@ +node_modules +**/*.js +**/next-env.d.ts diff --git a/webapp/.eslintrc.js b/webapp/.eslintrc.js new file mode 100644 index 0000000000..ce8712d16d --- /dev/null +++ b/webapp/.eslintrc.js @@ -0,0 +1,135 @@ +module.exports = { + parser: '@typescript-eslint/parser', + extends: [ + 'airbnb-base', + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', + // 'plugin:prettier/recommended', + // 'prettier/@typescript-eslint', + ], + + plugins: [ + 'import', + '@typescript-eslint', + 'eslint-plugin-import-helpers', + // 'prettier', + 'jest', + ], + + settings: { + 'import/parsers': { + '@typescript-eslint/parser': ['.ts', '.tsx'], + }, + 'import/resolver': { + typescript: { + alwaysTryTypes: true, + }, + }, + typescript: { + directory: './', + }, + react: { + createClass: 'createReactClass', + pragma: 'React', + version: 'detect', + }, + }, + + rules: { + // 'prettier/prettier': 'warn', + 'arrow-parens': ['error', 'as-needed', { 'requireForBlockBody': true }], + 'curly': ['error', 'all'], + 'class-methods-use-this': 'off', + 'default-case': 'off', + 'react/display-name': 'off', + 'react/jsx-no-literals': 'off', + 'react/react-in-jsx-scope': 'off', + 'brace-style': 'off', // prefer 1tbs or stroustrup styles, avoid allman style + 'operator-linebreak': 'warn', + 'consistent-return': 'off', + 'comma-dangle': ['warn', { // the rule enforces consistent use of trailing commas in object and array literals + 'arrays': 'always-multiline', + 'objects': 'always-multiline', + 'imports': 'only-multiline', + 'exports': 'only-multiline', + 'functions': 'only-multiline' + }], + 'eqeqeq': 'warn', + 'radix': 'off', + 'no-constant-condition': 'off', + 'no-console': 'off', + 'no-dupe-class-members': 'off', + 'no-unreachable': 'off', + 'no-unused-expressions': 'off', + 'no-nested-ternary': 'warn', + 'no-empty': 'off', + 'no-return-assign': 'off', + 'no-continue': 'off', + 'no-shadow': 'off', + 'no-restricted-syntax': 'off', + 'no-underscore-dangle': 'off', + 'no-undef': 'off', + 'no-plusplus': 'off', + 'no-param-reassign': 'off', + 'no-await-in-loop': 'off', + 'no-restricted-globals': 'off', + 'no-useless-constructor': 'off', + 'max-classes-per-file': 'off', + 'max-len': [ + 'error', + { + code: 120, + ignoreTrailingComments: true, + ignoreStrings: true + } + ], + 'lines-between-class-members': 'off', + 'require-atomic-updates': 'off', + 'prefer-destructuring': 'off', + 'prefer-arrow-callback': [ + 'error', + { + 'allowNamedFunctions': true + } + ], + 'padded-blocks': 'off', + '@typescript-eslint/no-empty-interface': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-unused-vars': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/triple-slash-reference': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'import/named': 'off', + 'import/export': 'off', + 'import/extensions': 'off', + 'import/no-unresolved': 'off', + 'import/no-extraneous-dependencies': 'warn', + 'import/prefer-default-export': 'off', + 'import/no-cycle': 'off', + 'import-helpers/order-imports': [ + 'warn', + { + newlinesBetween: 'always', + groups: ['module', '/^@/', '/^~/', ['parent', 'sibling', 'index']], + alphabetize: { order: 'asc', ignoreCase: true }, + }, + ], + 'indent': [ + 'warn', 2, { + 'FunctionDeclaration': { 'parameters': 'first' }, + 'FunctionExpression': { 'parameters': 'first' }, + 'CallExpression': { 'arguments': 'first' }, + 'ArrayExpression': 1, + 'SwitchCase': 1 + } + ], + }, + + env: { + 'jest/globals': true, + }, +} diff --git a/webapp/.gitignore b/webapp/.gitignore new file mode 100644 index 0000000000..ebd7268137 --- /dev/null +++ b/webapp/.gitignore @@ -0,0 +1,4 @@ +.idea +node_modules + +yarn-error.log diff --git a/webapp/.prettierrc.js b/webapp/.prettierrc.js new file mode 100644 index 0000000000..515be6cdc5 --- /dev/null +++ b/webapp/.prettierrc.js @@ -0,0 +1,9 @@ +// we use prettier only for graphQL-codegen + +module.exports = { + useTabs: false, + tabWidth: 2, + trailingComma: 'all', + singleQuote: false, + semi: true, +} diff --git a/webapp/configs/babel-rollup.config.js b/webapp/configs/babel-rollup.config.js new file mode 100644 index 0000000000..04f99be204 --- /dev/null +++ b/webapp/configs/babel-rollup.config.js @@ -0,0 +1,10 @@ +module.exports = { + presets: [ + '@babel/preset-react', + ], + plugins: [ + '@babel/plugin-syntax-dynamic-import', + 'react-require', + 'reshadow/babel', + ], +} diff --git a/webapp/configs/babel.config.js b/webapp/configs/babel.config.js new file mode 100644 index 0000000000..aa436143dd --- /dev/null +++ b/webapp/configs/babel.config.js @@ -0,0 +1,17 @@ +module.exports = { + presets: [ + "@babel/preset-env", + "@babel/preset-react", + ['@babel/preset-typescript', { isTSX: true, allExtensions: true }], + ], + plugins: [ + '@babel/plugin-transform-runtime', + '@babel/plugin-syntax-dynamic-import', + 'babel-plugin-transform-typescript-metadata', + '@babel/plugin-proposal-optional-chaining', + ['@babel/plugin-proposal-decorators', { legacy: true }], + ['@babel/plugin-proposal-class-properties', { loose: true }], + 'react-require', + 'reshadow/babel', + ], +} diff --git a/webapp/configs/rollup.config.js b/webapp/configs/rollup.config.js new file mode 100644 index 0000000000..2b609aecef --- /dev/null +++ b/webapp/configs/rollup.config.js @@ -0,0 +1,80 @@ +import babelPlugin from 'rollup-plugin-babel'; +const path = require('path'); +import postcssPlugin from 'rollup-plugin-postcss'; +import nodeResolve from 'rollup-plugin-node-resolve'; +import commonjs from 'rollup-plugin-commonjs'; +import peerDepsExternal from 'rollup-plugin-peer-deps-external'; +import includePaths from 'rollup-plugin-includepaths'; +import typescriptPlugin from 'rollup-plugin-typescript2'; + +const includePathOptions = { + include: {}, + paths: ['src'], + external: [ + + ], + extensions: ['.ts', '.tsx', '.js'] +}; + +export const defaultBuilderOptions = { + babelConfig: path.join(__dirname, './babel-rollup.config.js'), + typescriptConfig: '', // tbd +} + +export function configBuilder(options = defaultBuilderOptions) { + + return { + input: 'src/index.ts', + output: [ + { + dir: 'dist', + format: 'esm', + }, + ], + external: [ + // externals beyond peer dependencies + ], + plugins: [ + includePaths(includePathOptions), + nodeResolve(), + commonjs({ + sourceMap: false, + }), + typescriptPlugin({ + tsconfig: 'tsconfig.build.json', + useTsconfigDeclarationDir: true, + }), + babelPlugin({ + exclude: 'node_modules/**', + extensions: ['.js', '.ts', '.tsx'], + configFile: options.babelConfig, + }), + postcssPlugin({ + extract: false, + // enable CSS modules for .module.css .module.scss files + autoModules: true, + // modules: { + // generateScopedName: "[local]___[hash:base64:5]" + // }, + inject: true, + use: { + sass: { + includePaths: ['node_modules', '../../node_modules'], + } + }, + // this option is added in dkrupenya/rollup-plugin-postcss#develop + // todo to be converted to custom loader + firstPlugin: require('reshadow/postcss'), + plugins: [ + require('postcss-preset-env')({ stage: 0 }), + // todo move 'reshadow/postcss' to custom loader to run it before postcss-loader + // require('reshadow/postcss'), + ] + }), + peerDepsExternal(), + ], + }; + +} + +export default configBuilder(defaultBuilderOptions); diff --git a/webapp/configs/webpack/build-plugin.js b/webapp/configs/webpack/build-plugin.js new file mode 100644 index 0000000000..df115404ab --- /dev/null +++ b/webapp/configs/webpack/build-plugin.js @@ -0,0 +1,23 @@ +const merge = require('webpack-merge'); +const {resolve} = require('path'); +var PeerDepsExternalsPlugin = require('peer-deps-externals-webpack-plugin'); + +const commonConfig = require('./common'); + +module.exports = (env, argv) => merge(commonConfig(env, argv), { + mode: argv.mode || 'development', + entry: resolve(__dirname, '../../src/index.ts'), // todo + output: { + filename: 'index.js', + path: resolve(__dirname, '../../dist'), + library: 'basicConnectionPlugin', // todo pass from package.js + libraryTarget: 'umd', + }, + externals: [ + /^@dbeaver\/.+$/ + ], + devtool: 'source-map', + plugins: [ + new PeerDepsExternalsPlugin(), // allow to treat all peerDependencies as externals + ], +}); diff --git a/webapp/configs/webpack/common.js b/webapp/configs/webpack/common.js new file mode 100644 index 0000000000..6a9cd92af7 --- /dev/null +++ b/webapp/configs/webpack/common.js @@ -0,0 +1,198 @@ +// shared config (dev and prod) +const webpack = require('webpack'); +const ModuleDependencyWarning = require("webpack/lib/ModuleDependencyWarning"); +const path = require('path'); +const CircularDependencyPlugin = require('circular-dependency-plugin'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + + +// Temporary solution to remove wrong warning messages (it's was fixed https://github.com/microsoft/TypeScript/pull/35200 in typescript 3.8) +class IgnoreNotFoundExportPlugin { + apply(compiler) { + const messageRegExp = /export '.*'( \(reexported as '.*'\))? was not found in/ + function doneHook(stats) { + stats.compilation.warnings = stats.compilation.warnings.filter(function (warn) { + if (warn instanceof ModuleDependencyWarning && messageRegExp.test(warn.message)) { + return false + } + return true; + }) + } + if (compiler.hooks) { + compiler.hooks.done.tap("IgnoreNotFoundExportPlugin", doneHook) + } else { + compiler.plugin("done", doneHook) + } + } +} + +module.exports = (env, argv) => { + + function generateStyleLoaders(options = {hasModule: false, hasReshadow: false}) { + // options.hasModule + const modules = options.hasModule + ? { + mode: 'local', + localIdentName: '[local]___[hash:base64:5]', + } + : undefined; + + // options.hasReshadow + const postCssPlugins = [ + require('postcss-preset-env')({ stage: 0 }), + require('@csstools/postcss-sass')({ + includePaths: ['node_modules', path.resolve('../../node_modules')], + }), + ]; + if (options.hasReshadow) { + postCssPlugins.push(require('reshadow/postcss')); + } + + // 'use' clause in webpack rules + return [ + // 'style-loader', + { + loader: MiniCssExtractPlugin.loader, + options: { + hot: argv.mode !== 'production', + }, + }, + { + loader: 'css-loader', + options: { + importLoaders: 1, + modules: modules, + } + }, + { + loader: 'postcss-loader', + options: { + syntax: 'postcss-scss', + plugins: postCssPlugins, + }, + }, + ]; + } + + return { + resolve: { + extensions: ['.ts', '.tsx', '.js', '.jsx'], + }, + module: { + rules: [ + { + test: /\.js$/, + use: [ + { + loader: 'babel-loader', + options: { + configFile: path.join(__dirname, '../babel.config.js') + }, + }, + 'source-map-loader' + ], + exclude: /node_modules/, + }, + { + test: /\.tsx?$/, + use: [ + { + loader: 'babel-loader', + options: { + configFile: path.join(__dirname, '../babel.config.js') + }, + }, + { + loader: 'eslint-loader', + // uncomment to fix after rule changes + // options: { + // fix: true, + // }, + } + ], + }, + { + test: /\.(css|scss|sass)$/, + oneOf: [ + // css-module files ( should have *.module mask ) + { + test: /\.module\.(css|scss|sass)$/, + use: generateStyleLoaders({hasModule: true, hasReshadow: true}), + }, + // not css-module files - 3 ways to treat them + { + test: /\.raw\.(css|scss|sass)$/, + use: generateStyleLoaders({hasModule: false, hasReshadow: false}), + }, + { + include: /node_modules/, + use: [ + // 'style-loader', + { + loader: MiniCssExtractPlugin.loader, + options: { + hot: argv.mode !== 'production', + }, + }, + 'css-loader', + ] + }, + { + use: generateStyleLoaders({hasModule: false, hasReshadow: true}), + } + ] + }, + { + test: /\.(png|jpe?g|gif|svg|eot|ttf|woff|woff2)$/i, + loader: 'url-loader', + options: { + limit: 8192, + }, + }, + ], + }, + optimization: { + minimize: false, + namedModules: true, + concatenateModules: false, + }, + plugins: [ + new IgnoreNotFoundExportPlugin(), + new webpack.ProgressPlugin({ + entries: true, + modules: true, + modulesCount: 100, + profile: true, + handler: (percentage, message, ...args) => { + // custom logic + } + }), + new CircularDependencyPlugin({ + // exclude detection of files based on a RegExp + exclude: /a\.js|node_modules/, + // include specific files based on a RegExp + // include: /dir/, + // add errors to webpack instead of warnings + failOnError: false, + // allow import cycles that include an asyncronous import, + // e.g. via import(/* webpackMode: "weak" */ './file.js') + allowAsyncCycles: false, + // set the current working directory for displaying module paths + cwd: process.cwd(), + }), + new MiniCssExtractPlugin({ + // Options similar to the same options in webpackOptions.output + // all options are optional + filename: argv.mode !== 'production' ? '[name].css' : '[name].[hash].css', + chunkFilename: argv.mode !== 'production' ? '[name].css' : '[name].[hash].css', + ignoreOrder: false, // Enable to remove warnings about conflicting order + }), + // new webpack.optimize.LimitChunkCountPlugin({ + // maxChunks: 1, + // }), + ], + performance: { + hints: false, + }, +}; +}; diff --git a/webapp/lerna.json b/webapp/lerna.json new file mode 100644 index 0000000000..82f5d3e383 --- /dev/null +++ b/webapp/lerna.json @@ -0,0 +1,8 @@ +{ + "packages": [ + "packages/*" + ], + "version": "independent", + "npmClient": "yarn", + "useWorkspaces": true +} diff --git a/webapp/package.json b/webapp/package.json new file mode 100644 index 0000000000..b51f848d1a --- /dev/null +++ b/webapp/package.json @@ -0,0 +1,113 @@ +{ + "name": "dbeaver-web", + "private": true, + "workspaces": { + "packages": [ + "packages/*" + ], + "nohoist": [ + ] + }, + "version": "1.0.0", + "description": "", + "scripts": { + "bootstrap": "lerna bootstrap", + "start": "lerna run start --stream", + "build": "lerna run build --stream", + "build-plugin": "lerna run build-plugin --stream --scope @dbeaver/core --scope @dbeaver/basic-connection-plugin", + "tsc": "lerna run tsc --stream", + "watch": "lerna run watch --stream", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "7.8.4" + }, + "devDependencies": { + "@babel/cli": "^7.7.4", + "@babel/core": "^7.7.4", + "@babel/plugin-proposal-decorators": "^7.7.4", + "@babel/plugin-proposal-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-transform-runtime": "^7.8.3", + "@babel/preset-env": "^7.7.4", + "@babel/preset-react": "^7.7.4", + "@babel/preset-typescript": "^7.7.4", + "@csstools/postcss-sass": "^4.0.0", + "@graphql-codegen/add": "^1.8.1", + "@graphql-codegen/cli": "^1.8.1", + "@graphql-codegen/near-operation-file-preset": "^1.8.0", + "@graphql-codegen/typescript": "^1.8.1", + "@graphql-codegen/typescript-graphql-files-modules": "^1.8.0", + "@graphql-codegen/typescript-graphql-request": "^1.8.1", + "@graphql-codegen/typescript-operations": "^1.8.1", + "@types/graphql": "^14.5.0", + "@types/jest": "^24.0.23", + "@types/node": "^12.7.8", + "@types/react": "^16.9.14", + "@types/react-dom": "^16.9.4", + "@typescript-eslint/eslint-plugin": "^2.5.0", + "@typescript-eslint/parser": "^2.8.0", + "babel-loader": "^8.0.6", + "babel-plugin-react-require": "^3.1.1", + "babel-plugin-transform-typescript-metadata": "^0.2.2", + "circular-dependency-plugin": "^5.2.0", + "copy-webpack-plugin": "^5.1.0", + "css-loader": "^3.2.1", + "eslint": "^6.6.0", + "eslint-config-airbnb-base": "^14.0.0", + "eslint-config-prettier": "^6.5.0", + "eslint-import-resolver-typescript": "^2.0.0", + "eslint-loader": "^3.0.3", + "eslint-plugin-import": "^2.18.2", + "eslint-plugin-import-helpers": "^1.0.2", + "eslint-plugin-jest": "^22.21.0", + "eslint-plugin-jsx-a11y": "^6.2.3", + "eslint-plugin-prettier": "^3.1.1", + "eslint-plugin-react": "^7.16.0", + "eslint-plugin-react-hooks": "^1.7.0", + "extract-css-chunks-webpack-plugin": "^4.7.1", + "file-loader": "^5.0.2", + "fork-ts-checker-webpack-plugin": "^4.0.5", + "graphql": "^14.5.8", + "html-webpack-plugin": "^3.2.0", + "http-proxy-middleware": "^0.20.0", + "image-webpack-loader": "^6.0.0", + "jest": "^24.9.0", + "lerna": "^3.19.0", + "lint-staged": "^9.4.1", + "mini-css-extract-plugin": "^0.9.0", + "node-sass": "^4.13.0", + "peer-deps-externals-webpack-plugin": "^1.0.4", + "postcss-extend-rule": "^3.0.0", + "postcss-import": "^12.0.1", + "postcss-loader": "^3.0.0", + "postcss-scss": "^2.0.0", + "prettier": "^1.19.1", + "prettier-eslint": "^9.0.0", + "raw-loader": "^3.1.0", + "react-hot-loader": "^4.12.18", + "rimraf": "^3.0.0", + "rollup": "^2.0.6", + "rollup-plugin-babel": "^4.4.0", + "rollup-plugin-commonjs": "^10.1.0", + "rollup-plugin-includepaths": "^0.2.3", + "rollup-plugin-node-resolve": "^5.2.0", + "rollup-plugin-peer-deps-external": "^2.2.2", + "rollup-plugin-postcss": "dkrupenya/rollup-plugin-postcss#develop", + "rollup-plugin-typescript2": "^0.26.0", + "sass-loader": "^8.0.0", + "source-map-loader": "^0.2.4", + "string-replace-webpack-plugin": "^0.1.3", + "style-loader": "^1.0.1", + "typescript": "^3.7.2", + "uglifyjs-webpack-plugin": "^2.2.0", + "url-loader": "^3.0.0", + "webpack": "^4.41.2", + "webpack-cli": "^3.3.10", + "webpack-dev-middleware": "^3.7.2", + "webpack-dev-server": "^3.9.0", + "webpack-merge": "^4.2.2" + } +} diff --git a/webapp/packages/_plugin-template/.gitignore b/webapp/packages/_plugin-template/.gitignore new file mode 100644 index 0000000000..d443c6b30b --- /dev/null +++ b/webapp/packages/_plugin-template/.gitignore @@ -0,0 +1,18 @@ + +# dependencies +/node_modules + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/_plugin-template/package.json b/webapp/packages/_plugin-template/package.json new file mode 100644 index 0000000000..70eb4ac571 --- /dev/null +++ b/webapp/packages/_plugin-template/package.json @@ -0,0 +1,31 @@ +{ + "name": "@dbeaver/plugin-template-name", + "version": "0.1.0", + "description": "", + "main": "dist/index.js", + "scripts": { + "build-plugin": "npm run clean-dist && webpack -p --config=../../configs/webpack/build-plugin.js", + "clean-dist": "rimraf dist/*", + "rollup": "npm run clean-dist && rollup --config ../../configs/rollup.config.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "Apache-2.0", + "peerDependencies": { + "@dbeaver/core": "^0.1.0", + "@material/button": "^4.0.0", + "@material/elevation": "^4.0.0", + "@material/ripple": "^4.0.0", + "@material/theme": "^4.0.0", + "@material/typography": "^4.0.0", + "graphql": "^14.5.8", + "graphql-request": "^1.8.2", + "graphql-tag": "^2.10.1", + "mobx": "^5.15.0", + "mobx-react": "^6.1.4", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "reakit": "^1.0.0-beta.11", + "reshadow": "0.0.1-alpha.73", + "rxjs": "^6.5.4" + } +} diff --git a/webapp/packages/_plugin-template/src/index.ts b/webapp/packages/_plugin-template/src/index.ts new file mode 100644 index 0000000000..627a85709c --- /dev/null +++ b/webapp/packages/_plugin-template/src/index.ts @@ -0,0 +1,6 @@ +import { manifest } from './manifest'; + + +// All Services and Components that is provided by this plugin should be exported here + +export default manifest; diff --git a/webapp/packages/_plugin-template/src/manifest.ts b/webapp/packages/_plugin-template/src/manifest.ts new file mode 100644 index 0000000000..7df2d1dbb8 --- /dev/null +++ b/webapp/packages/_plugin-template/src/manifest.ts @@ -0,0 +1,23 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { IServiceInjector, PluginManifest } from '@dbeaver/core/di'; + +export const manifest: PluginManifest = { + info: { + name: '___template plugin_____', + }, + + providers: [ + // services here + ], + + initialize(services: IServiceInjector): void { + // init phase here + }, +}; diff --git a/webapp/packages/_plugin-template/tsconfig.build.json b/webapp/packages/_plugin-template/tsconfig.build.json new file mode 100644 index 0000000000..dd700023ea --- /dev/null +++ b/webapp/packages/_plugin-template/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declarationDir": "dist/types", + "declaration": true, + "noEmit": false, + "outDir": "dist/js" + }, + "exclude": [ + "node_modules", + "@dbeaver/**" + ], + "include": ["src/**/*"] +} diff --git a/webapp/packages/_plugin-template/tsconfig.json b/webapp/packages/_plugin-template/tsconfig.json new file mode 100644 index 0000000000..6b4c45f333 --- /dev/null +++ b/webapp/packages/_plugin-template/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "@dbeaver/**" + ] +} diff --git a/webapp/packages/ag-grid-plugin/.gitignore b/webapp/packages/ag-grid-plugin/.gitignore new file mode 100644 index 0000000000..d443c6b30b --- /dev/null +++ b/webapp/packages/ag-grid-plugin/.gitignore @@ -0,0 +1,18 @@ + +# dependencies +/node_modules + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/ag-grid-plugin/package.json b/webapp/packages/ag-grid-plugin/package.json new file mode 100644 index 0000000000..ac7bea96e5 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/package.json @@ -0,0 +1,40 @@ +{ + "name": "@dbeaver/ag-grid-plugin", + "version": "0.1.0", + "description": "", + "main": "dist/index.js", + "types": "dist/types", + "scripts": { + "build-plugin": "npm run clean-dist && webpack -p --config=../../configs/webpack/build-plugin.js", + "clean-dist": "rimraf dist/*", + "rollup": "npm run clean-dist && rollup --config ../../configs/rollup.config.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "Apache-2.0", + "dependencies": { + "@ag-grid-community/core": "^22.1.0", + "@ag-grid-community/all-modules": "^22.1.0", + "@ag-grid-enterprise/all-modules": "^22.1.0", + "ag-grid-community": "^22.1.0", + "ag-grid-enterprise": "^22.1.0", + "ag-grid-react": "^22.1.0" + }, + "peerDependencies": { + "@dbeaver/core": "^0.1.0", + "@material/button": "^4.0.0", + "@material/elevation": "^4.0.0", + "@material/ripple": "^4.0.0", + "@material/theme": "^4.0.0", + "@material/typography": "^4.0.0", + "graphql": "^14.5.8", + "graphql-request": "^1.8.2", + "graphql-tag": "^2.10.1", + "mobx": "^5.15.0", + "mobx-react": "^6.1.4", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "reakit": "^1.0.0-beta.11", + "reshadow": "0.0.1-alpha.73", + "rxjs": "^6.5.4" + } +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridContext.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridContext.ts new file mode 100644 index 0000000000..84a11aaf53 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridContext.ts @@ -0,0 +1,15 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { TableSelection } from './TableSelection/TableSelection'; + +export type AgGridContext = { + selection: TableSelection; + onEditSave(): void; + onEditCancel(): void; +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTable.tsx b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTable.tsx new file mode 100644 index 0000000000..dd8c8457b9 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTable.tsx @@ -0,0 +1,81 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Module } from 'ag-grid-community'; +import { AgGridReactProps } from 'ag-grid-react'; +import { observer, Observer } from 'mobx-react'; +import { PropsWithChildren } from 'react'; +import styled from 'reshadow'; + +import '../styles/base.raw.scss'; // it leads to encapsulation of ag-gris style in main.css +import { ComplexLoader, Loader } from '@dbeaver/core/blocks'; +import { useController } from '@dbeaver/core/di'; +import { useStyles } from '@dbeaver/core/theming'; + +import { RangeSelectionModule } from '../modules/RangeSelection/rangeSelectionModule'; +import { AgGridTableController } from './AgGridTableController'; +import { IAgGridModel } from './IAgGridModel'; +import { LoadingCell } from './LoadingCell'; +import { PlainTextEditor } from './PlainTextEditor/PlainTextEditor'; +import { agGridStyles } from './styles'; +import { TableColumnHeader } from './TableColumnHeader/TableColumnHeader'; + +export type AgGridTableProps = PropsWithChildren< + AgGridReactProps & { + tableModel: IAgGridModel; + className?: string; + }> + +async function loader() { + // const styles = await import('../styles/base.scss'); + const { AgGridReact } = await import('ag-grid-react'); + const { AllCommunityModules } = await import('@ag-grid-community/all-modules'); + + return { AgGridReact, AllCommunityModules: [...AllCommunityModules, RangeSelectionModule] }; +} + +const agGridComponents = { + agColumnHeader: TableColumnHeader, + plainTextEditor: PlainTextEditor, + loadingCellRenderer: LoadingCell, +}; + +export const AgGridTable = observer(function AgGridTable({ + tableModel, + className, + ...rest +}: AgGridTableProps) { + const styles = useStyles(agGridStyles); + const controller = useController(AgGridTableController, tableModel); + + return ( + } + > + {({ AgGridReact, AllCommunityModules }) => ( + + {() => styled(styles)( + + + + )} + + )} + + ); +}); diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTableController.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTableController.ts new file mode 100644 index 0000000000..0bc834069e --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/AgGridTableController.ts @@ -0,0 +1,281 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + GridApi, + ColumnApi, + GridReadyEvent, + IDatasource, + IGetRowsParams, + ColDef, + ValueGetterParams, + GridOptions, + CellEditingStoppedEvent, +} from 'ag-grid-community'; +import { RowDataTransaction } from 'ag-grid-community/dist/lib/interfaces/rowDataTransaction'; +import { computed, observable } from 'mobx'; + +import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di'; + +import { AgGridContext } from './AgGridContext'; +import { + AgGridRow, IAgGridActions, IAgGridCol, IAgGridModel, +} from './IAgGridModel'; +import { RowSelection } from './TableSelection/RowSelection'; +import { TableSelection } from './TableSelection/TableSelection'; + +@injectable() +export class AgGridTableController implements IInitializableController, IDestructibleController { + @observable refreshId = 0; + + private readonly datasource: IDatasource = { + getRows: this.getRows.bind(this), + }; + + private readonly selection = new TableSelection(); + + /** + * contains properties to pass to ag-grid + */ + private readonly context: AgGridContext = { + selection: this.selection, + onEditSave: this.onEditSave.bind(this), + onEditCancel: this.onEditCancel.bind(this), + } + + /** + * ag-grid options that is set and not changed during AgGridComponent lifetime + */ + private readonly gridOptions: GridOptions = { + defaultColDef: defaultColumnDef, + + rowHeight: 28, + rowModelType: 'infinite', + cacheBlockSize: undefined, // to be set during init phase + + datasource: this.datasource, + + context: this.context, + // maxBlocksInCache: 1, + + onGridReady: this.handleGridReady.bind(this), + onBodyScroll: this.handleBodyScroll.bind(this), + + onCellEditingStopped: this.handleCellEditingStopped.bind(this), + }; + + @observable columns: ColDef[] = []; + + /** + * use this object to dynamically change ag-grid properties + */ + @computed get dynamicOptions() { + return { + enableRangeSelection: !!this.selection, + }; + } + + private api?: GridApi; + private columnApi?: ColumnApi; + private gridModel!: IAgGridModel; + private resizeTask?: any; + + /** + * this actions will be passed outside data grid to ba called + */ + private actions: IAgGridActions = { + changeChunkSize: this.changeChunkSize.bind(this), + resetData: this.resetData.bind(this), + updateCellValue: this.updateCellValue.bind(this), + updateRowValue: this.updateRowValue.bind(this), + getSelectedRows: this.getSelectedRows.bind(this), + }; + + init(gridModel: IAgGridModel) { + gridModel.actions = this.actions; + this.gridModel = gridModel; + this.gridOptions.cacheBlockSize = gridModel.chunkSize; + if (gridModel.initialColumns?.length) { + this.columns = mapDataToColumns(gridModel.initialColumns); + } + } + + destruct(): void { + this.gridModel.actions = null; + } + + getGridOptions() { + return this.gridOptions; + } + + private refresh() { + this.refreshId++; + } + + /** + * Part of Ag-grid IDataSource + * Called by ag-grid when user scroll table and new portion of data is required + * @param params + */ + private async getRows(params: IGetRowsParams) { + const { + startRow, + endRow, + successCallback, + failCallback, + } = params; + + try { + const length = endRow - startRow; + const requestedData = await this.gridModel.onRequestData(startRow, length); + // update columns only once after first data fetching + if (!this.columns.length) { + this.columns = mapDataToColumns(requestedData.columns || []); + } + successCallback( + this.cloneRows(requestedData.rows), + requestedData.isFullyLoaded ? startRow + requestedData.rows.length : -1 // use -1 to tell ag-grid that we have more data + ); + } catch (e) { + failCallback(); + } + } + + private changeChunkSize(chunkSize: number): void { + this.gridOptions.cacheBlockSize = chunkSize; + // ag-grid is not able to change ca + this.refresh(); + } + + private handleCellEditingStopped(event: CellEditingStoppedEvent) { + if (this.gridModel.onCellEditingStopped) { + this.gridModel.onCellEditingStopped(event.rowIndex, parseInt(event.column.getColId()), event.value); + } + } + + private onEditSave() { + if (this.gridModel.onEditSave) { + this.gridModel.onEditSave(); + } + } + + private onEditCancel() { + if (this.gridModel.onEditCancel) { + this.gridModel.onEditCancel(); + } + } + + private handleBodyScroll() { + if (this.resizeTask !== undefined) { + clearTimeout(this.resizeTask); + } + this.resizeTask = setTimeout(() => this.resizeIndexColumn(), 50); + } + + private resizeIndexColumn() { + if (this.columnApi) { + this.columnApi.autoSizeColumns([INDEX_COLUMN_DEF.field!]); + } + } + + private handleGridReady(params: GridReadyEvent) { + this.api = params.api; + this.columnApi = params.columnApi; + this.setInitialRow(this.gridModel.initialRows); + } + + /* Actions */ + + private resetData(columns?: IAgGridCol[], rows?: AgGridRow[]): void { + this.selection.clear(); + if (this.api) { + // only purgeInfiniteCache() doesn't work when cache is empty. + // probably it thinks that nothing to delete - nothing to refresh + this.api.refreshInfiniteCache(); // it will mark internal state for reload + this.api.purgeInfiniteCache(); // it will reset internal state + this.columns = columns ? mapDataToColumns(columns) : []; + this.setInitialRow(rows); + } + } + + private setInitialRow(initialRows?: AgGridRow[]): void { + if (!initialRows || !initialRows.length) { + return; + } + const transaction: RowDataTransaction = { + addIndex: 0, + add: initialRows || [], + }; + this.api!.updateRowData(transaction); + } + + private updateCellValue(rowNumber: number, colNumber: number, value: any): void { + if (this.api) { + this.api + .getRowNode(`${rowNumber}`) + .setDataValue(`${colNumber}`, value); + } + } + + private updateRowValue(rowNumber: number, newRow: any[]): void { + if (this.api) { + this.api + .getRowNode(`${rowNumber}`) + .setData([...newRow]); + } + } + + private getSelectedRows(): RowSelection[] { + return this.selection.getSelectedRows(); + } + + private cloneRows(rows: AgGridRow[]): AgGridRow[] { + return rows.map(row => [...row]); + } +} + +const defaultColumnDef: ColDef = { + sortable: true, + filter: true, + resizable: true, + editable: true, + cellEditor: 'plainTextEditor', +}; + +export const INDEX_COLUMN_DEF: ColDef = { + headerName: '#', + field: `${Number.MAX_SAFE_INTEGER}`, + valueGetter: 'node.id', + width: 70, + pinned: 'left', + suppressNavigable: true, + suppressMenu: true, + editable: false, + cellRenderer: row => row.rowIndex + 1, +}; + +function mapDataToColumns(columns: IAgGridCol[]): ColDef[] { + if (!columns.length) { + return []; + } + return [ + INDEX_COLUMN_DEF, + ...columns.map(v => ({ + headerName: v.label, + field: `${v.position}`, + valueGetter: v.dataKind === 'OBJECT' ? getObjectValue : undefined, + headerComponentParams: { + icon: v.icon, + }, + })), + ]; +} + +function getObjectValue({ data, colDef }: ValueGetterParams) { + return data[colDef.field || 'node.id'].value; +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/IAgGridModel.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/IAgGridModel.ts new file mode 100644 index 0000000000..9e01b48a0e --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/IAgGridModel.ts @@ -0,0 +1,47 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { RowSelection } from './TableSelection/RowSelection'; + +export type AgGridRow = any[]; + + +export interface IAgGridModel { + initialRows: AgGridRow[]; + initialColumns: IAgGridCol[]; + chunkSize: number; + enableRangeSelection?: boolean; + + actions: IAgGridActions | null; + // hooks + onRequestData(rowOffset: number, count: number): Promise; + onCellEditingStopped?(rowNumber: number, colNumber: number, value: any): void; + onEditSave(): void; + onEditCancel(): void; +} + +export interface IAgGridActions { + changeChunkSize(chunkSize: number): void; + resetData(columns?: IAgGridCol[], rows?: AgGridRow[]): void; + updateCellValue(rowNumber: number, colNumber: number, value: any): void; + updateRowValue(rowNumber: number, value: any[]): void; + getSelectedRows(): RowSelection[]; +} + +export interface IAgGridCol { + icon?: string; + label?: string; + position?: number; + dataKind?: string; +} + +export interface IRequestedData { + rows: AgGridRow[]; + columns?: IAgGridCol[]; + isFullyLoaded: boolean; +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/LoadingCell.tsx b/webapp/packages/ag-grid-plugin/src/AgGridTable/LoadingCell.tsx new file mode 100644 index 0000000000..9bd9b2a7d0 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/LoadingCell.tsx @@ -0,0 +1,15 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export function LoadingCell() { + return ( +
+ Loading... +
+ ); +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx b/webapp/packages/ag-grid-plugin/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx new file mode 100644 index 0000000000..3ccd39060b --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/PlainTextEditor/PlainTextEditor.tsx @@ -0,0 +1,94 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + forwardRef, useImperativeHandle, useState, useCallback, useRef, useEffect, +} from 'react'; +import styled, { css } from 'reshadow'; + +import { ICellEditorParams, ICellEditorComp } from '@ag-grid-community/core'; +import { InlineEditor, InlineEditorControls } from '@dbeaver/core/app'; + +import { AgGridContext } from '../AgGridContext'; + +const KEY_BACKSPACE = 8; +const KEY_DELETE = 46; + +const styles = css` + editor { + position: absolute; + left: 0; + top: -1px; + bottom: -1px; + width: 100%; + } +`; + +export const PlainTextEditor = forwardRef, ICellEditorParams>( + function PlainTextEditor(props, ref) { + const inputRef = useRef(null); + const [, forceUpdate] = useState(); + const [initValue] = useState(() => { + if (props.keyPress === KEY_BACKSPACE || props.keyPress === KEY_DELETE) { + return ''; + } + if (props.charPress) { + return props.charPress; + } + + return props.value; + }); + + const value = useRef(initValue); + const special = useRef(value.current); + + useImperativeHandle(ref, () => ({ + getValue: () => special.current, + }), []); + + useEffect(() => inputRef.current?.focus(), []); + + const context: AgGridContext = props.context; + + const handleSave = useCallback(() => { + special.current = value.current; + props.stopEditing(); + + // we use this setTimeout to update changes before saving + setTimeout(() => { + context.onEditSave(); + }, 1); + }, [props.stopEditing]); + + const handleReject = useCallback(() => props.api?.stopEditing(true), [props.api?.stopEditing]); + const handleChange = useCallback((newValue: string) => { + value.current = newValue; + forceUpdate(newValue); + }, []); + + const isLastColumn = props.columnApi?.getAllGridColumns().slice(-1)[0] === props.column; + const isLastRow = props.rowIndex > 0 && ((props.api?.getInfiniteRowCount() || -1) - 1 === props.rowIndex); + let controlsPosition: InlineEditorControls = 'right'; + + if (isLastColumn) { + controlsPosition = isLastRow ? 'top' : 'bottom'; + } + + return styled(styles)( + + + + ); + } +); diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/TableColumnHeader/TableColumnHeader.tsx b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableColumnHeader/TableColumnHeader.tsx new file mode 100644 index 0000000000..3c0d9f7456 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableColumnHeader/TableColumnHeader.tsx @@ -0,0 +1,74 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useCallback } from 'react'; +import styled, { css } from 'reshadow'; + +import { AgEvent, GridApi, Column } from '@ag-grid-community/core'; +import { StaticImage } from '@dbeaver/core/blocks'; + +type HeaderProps = { + api: GridApi; + reactContainer: HTMLElement; + column: Column; + enableMenu: boolean; + enableSorting: boolean; + menuIcon: string; + displayName: string; + showColumnMenu: (button?: HTMLDivElement) => void; + setSort: (order: string, shiftKey: boolean) => void; + icon?: string; +} + +const headerStyles = css` + table-header { + display: flex; + align-items: center; + align-content: center; + width: 100%; + } + icon { + height: 16px; + } + StaticImage { + height: 100%; + } + name { + margin-left: 8px; + text-transform: uppercase; + font-weight: 400; + } +`; + +export interface IAgColumnClickEvent extends AgEvent{ + columnIndex: number; + isMultiple: boolean; +} + +export const COLUMN_CLICK_EVENT_TYPE = 'column-click'; + +export function TableColumnHeader(props: HeaderProps) { + const handleClick = useCallback((e: React.MouseEvent) => { + const event: IAgColumnClickEvent = Object.freeze({ + type: COLUMN_CLICK_EVENT_TYPE, + columnIndex: parseInt(props.column.getColId()), + isMultiple: e.ctrlKey, + }); + + props.api.dispatchEvent(event); + }, []); + + return styled(headerStyles)( + + + + + {props.displayName} + + ); +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/RowSelection.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/RowSelection.ts new file mode 100644 index 0000000000..de08f3a769 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/RowSelection.ts @@ -0,0 +1,37 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +export class RowSelection { + columns = new Set() + constructor(readonly rowId: number) {} + + add(columnIndexList: number[]) { + for (const columnId of columnIndexList) { + this.columns.add(columnId); + } + } + + remove(columnIndexList: number[]) { + for (const columnId of columnIndexList) { + this.columns.delete(columnId); + } + } + + replace(columnIndexList: number[]) { + this.columns.clear(); + this.add(columnIndexList); + } + + isSelected(columnIndex: number) { + return this.columns.has(columnIndex); + } + + isRangeSelected(columnIndexList: number[]) { + return columnIndexList.every(column => this.columns.has(column)); + } +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/TableSelection.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/TableSelection.ts new file mode 100644 index 0000000000..892797088d --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/TableSelection/TableSelection.ts @@ -0,0 +1,107 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { RowSelection } from './RowSelection'; + +export class TableSelection { + private selectedMap = new Map() + + getSelectedRows(): RowSelection[] { + return Array + .from(this.selectedMap.keys()) + .sort((a, b) => a - b) + .map(rowId => this.selectedMap.get(rowId)!); + } + + clear() { + this.selectedMap.clear(); + } + + selectCell(rowId: number, columnIndex: number, isMultiple: boolean, ignoreSelected = false) { + if (!isMultiple) { + this.clear(); + } + + const rowSelection = this.selectedMap.get(rowId); + + if (!ignoreSelected && rowSelection?.isSelected(columnIndex)) { + this.unselectColumn( + this.selectedMap, + rowSelection, + [columnIndex] + ); + } else { + this.selectColumn(rowId, rowSelection, [columnIndex]); + } + } + + selectRange( + startPosition: number, + endPosition: number, + columns: number[], + isMultiple: boolean + ) { + const firstRow = Math.min(startPosition, endPosition); + const lastRow = Math.max(startPosition, endPosition); + const isSelected = this.isRangeSelected(firstRow, lastRow, columns); + + if (!isMultiple) { + this.clear(); + } + + for (let rowId = firstRow; rowId <= lastRow; rowId++) { + const rowSelection = this.selectedMap.get(rowId); + if (isSelected) { + if (rowSelection) { + this.unselectColumn( + this.selectedMap, + rowSelection, + columns + ); + } + } else { + this.selectColumn(rowId, rowSelection, columns); + } + } + } + + isCellSelected(rowId: number, columnIndex: number): boolean { + const current = this.selectedMap.get(rowId); + + return current?.isSelected(columnIndex) || false; + } + + isRangeSelected(startPosition: number, endPosition: number, columns: number[]): boolean { + const start = Math.min(startPosition, endPosition); + const end = Math.max(startPosition, endPosition); + + for (let rowId = start; rowId <= end; rowId++) { + const rowSelection = this.selectedMap.get(rowId); + if (!rowSelection?.isRangeSelected(columns)) { + return false; + } + } + return true; + } + + private unselectColumn(source: Map, rowSelection: RowSelection, columnIndexList: number[]) { + rowSelection.remove(columnIndexList); + if (rowSelection.columns.size === 0) { + source.delete(rowSelection.rowId); + } + } + + private selectColumn(rowId: number, rowSelection: RowSelection | undefined = undefined, columnIndexList: number[]) { + if (!rowSelection) { + rowSelection = new RowSelection(rowId); + this.selectedMap.set(rowId, rowSelection); + } + + rowSelection.add(columnIndexList); + } +} diff --git a/webapp/packages/ag-grid-plugin/src/AgGridTable/styles.ts b/webapp/packages/ag-grid-plugin/src/AgGridTable/styles.ts new file mode 100644 index 0000000000..467a178040 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/AgGridTable/styles.ts @@ -0,0 +1,16 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { ThemeSelector } from '@dbeaver/core/src/theming'; + +export const agGridStyles: ThemeSelector = async (theme) => { + const styles = theme === 'dark' + ? await import('../styles/themes/dark.module.scss') + : await import('../styles/themes/light.module.scss'); + return styles.default; +}; diff --git a/webapp/packages/ag-grid-plugin/src/index.ts b/webapp/packages/ag-grid-plugin/src/index.ts new file mode 100644 index 0000000000..f09f42aa6f --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/index.ts @@ -0,0 +1,10 @@ +import { manifest } from './manifest'; + +export * from './AgGridTable/AgGridTable'; + +// interfaces +export * from './AgGridTable/IAgGridModel'; + +// All Services and Components that is provided by this plugin should be exported here + +export default manifest; diff --git a/webapp/packages/ag-grid-plugin/src/manifest.ts b/webapp/packages/ag-grid-plugin/src/manifest.ts new file mode 100644 index 0000000000..108c99c4c6 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/manifest.ts @@ -0,0 +1,14 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { PluginManifest } from '@dbeaver/core/di'; + +export const manifest: PluginManifest = { + info: { name: 'AgGrid Table' }, + providers: [], +}; diff --git a/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeController.ts b/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeController.ts new file mode 100644 index 0000000000..c7ff742250 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeController.ts @@ -0,0 +1,473 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + Autowired, + Bean, + CellPosition, + ColumnApi, + Events, + EventService, + GridApi, + GridOptionsWrapper, + GridPanel, + IRangeController, + CellRangeParams, + CellRange, + RangeSelectionChangedEvent, + RowPosition, + PostConstruct, + CellClickedEvent, + ColumnController, + Column, + IRowModel, + MouseEventService, + _, + AgGridEvent, + CellPositionUtils, + RowPositionUtils, + ValueService, +} from '@ag-grid-community/core'; + +import { INDEX_COLUMN_DEF } from '../../AgGridTable/AgGridTableController'; +import { + IAgColumnClickEvent, COLUMN_CLICK_EVENT_TYPE, +} from '../../AgGridTable/TableColumnHeader/TableColumnHeader'; +import { TableSelection } from '../../AgGridTable/TableSelection/TableSelection'; + +interface CellKeyDownEvent extends AgGridEvent{ + event: KeyboardEvent; +} + +const EVENT_KEY_CODE = { + C: 67, +}; + +interface TemporarySelectionRange { + firstRow: number; + lastRow: number; + columns: number[]; + isMultiple: boolean; +} + +@Bean('rangeController') +export class RangeController implements IRangeController { + @Autowired('cellPositionUtils') public cellPositionUtils!: CellPositionUtils; + @Autowired('rowPositionUtils') public rowPositionUtils!: RowPositionUtils; + @Autowired('valueService') private valueService!: ValueService; + + @Autowired('rowModel') private rowModel!: IRowModel; + @Autowired('columnController') private columnController!: ColumnController; + @Autowired('mouseEventService') private mouseEventService!: MouseEventService; + @Autowired('columnApi') private columnApi!: ColumnApi; + @Autowired('gridApi') private gridApi!: GridApi; + @Autowired('gridOptionsWrapper') private gridOptionsWrapper!: GridOptionsWrapper; + @Autowired('eventService') private eventService!: EventService; + + private lastSelectedCell?: CellPosition + private isDragging = false; + private isDraggingMultiple = false; + private startDraggingCell: CellPosition | null = null + private endDraggingCell: CellPosition | null = null + private temporaryRange: TemporarySelectionRange | null = null + private selection?: TableSelection + + registerGridComp(gridPanel: GridPanel): void { } + + @PostConstruct + private init(): void { + this.selection = this.gridOptionsWrapper.getContext()?.selection; + + if (this.selection) { + this.eventService.addEventListener(Events.EVENT_CELL_KEY_DOWN, this.handleCellCopy.bind(this)); + } + + this.eventService.addEventListener(COLUMN_CLICK_EVENT_TYPE, this.selectColumn.bind(this)); + this.eventService.addEventListener(Events.EVENT_CELL_CLICKED, this.selectRow.bind(this)); + } + + private handleCellCopy(event: CellKeyDownEvent) { + if (this.gridApi.getEditingCells().length > 0) { + return; + } + this.handleDocumentCopy(event.event); + } + + private handleDocumentCopy(event: KeyboardEvent) { + if (event.keyCode === EVENT_KEY_CODE.C && event.ctrlKey) { + this.copySelectedData(); + } + } + + private copySelectedData() { + const selectedRows = this.selection!.getSelectedRows(); + const selectedColumns = new Set(); + + for (const selectedRow of selectedRows) { + for (const columnId of selectedRow.columns) { + selectedColumns.add(columnId); + } + } + + const columns = this.columnController + .getAllDisplayedColumns() + .filter(column => selectedColumns.has(parseInt(column.getColId()))); + + let data = ''; + for (const selectedRow of selectedRows) { + for (const column of columns) { + if (column !== columns[0]) { + data += '\t'; + } + if (selectedRow.isSelected(parseInt(column.getColId()))) { + const rowNode = this.rowPositionUtils.getRowNode({ + rowIndex: selectedRow.rowId, + rowPinned: undefined, + }); + const value = this.valueService.getValue(column, rowNode); + data += value; + } + } + data += '\r\n'; + } + this.copyData(data); + } + + private copyData(data: string) { + const shadowElement = document.createElement('textarea'); + shadowElement.value = data; + shadowElement.style.position = 'absolute'; + shadowElement.style.overflow = 'hidden'; + shadowElement.style.width = '0'; + shadowElement.style.height = '0'; + shadowElement.style.top = '0'; + shadowElement.style.left = '0'; + + document.body.appendChild(shadowElement); + shadowElement.select(); + document.execCommand('copy'); + document.body.removeChild(shadowElement); + } + + private selectColumn(event: IAgColumnClickEvent) { + if (!this.selection) { + return; + } + const lastRowId = this.gridApi.getInfiniteRowCount() - 1 - (this.rowModel.isLastRowFound() ? 0 : 1); + if (event.columnIndex === parseInt(INDEX_COLUMN_DEF.field!)) { + this.selection.selectRange( + 0, + lastRowId, + this.getColumnsWithoutIndex(), + event.isMultiple + ); + } else { + this.selection.selectRange(0, lastRowId, [event.columnIndex], event.isMultiple); + } + this.dispatchChangedEvent(false, true); + } + + private selectRow(event: CellClickedEvent) { + if (!this.selection || (event.event as MouseEvent)?.shiftKey) { + return; + } + + if (event.column.getColId() === INDEX_COLUMN_DEF.field) { + this.selection.selectRange( + event.rowIndex, + event.rowIndex, + this.getColumnsWithoutIndex(), + (event.event as MouseEvent)?.ctrlKey + ); + this.dispatchChangedEvent(false, true); + } + } + + setRangeToCell(cell: CellPosition, appendRange = false): void { + if (!this.selection) { + return; + } + this.lastSelectedCell = cell; + if (!this.gridOptionsWrapper.isEnableRangeSelection() + || cell.column.getColDef().field === INDEX_COLUMN_DEF.field) { + return; + } + this.selection.selectCell(cell.rowIndex, parseInt(cell.column.getColId()), appendRange); + this.dispatchChangedEvent(false, true); + } + + getCellRangeCount(cell: CellPosition): number { + const columnIndex = parseInt(cell.column.getColId()); + if (this.isCellInTemporaryRange(cell.rowIndex, columnIndex)) { + const isRangeSelected = this.selection?.isRangeSelected( + this.temporaryRange!.firstRow, + this.temporaryRange!.lastRow, + this.temporaryRange!.columns, + ); + return isRangeSelected ? 0 : 1; + } + if (this.selection?.isCellSelected(cell.rowIndex, parseInt(cell.column.getColId()))) { + return 1; + } + + return 0; + } + + extendLatestRangeToCell(position: CellPosition): void { + if (!this.selection) { + return; + } + const startRow = this.lastSelectedCell !== undefined ? this.lastSelectedCell.rowIndex : position.rowIndex; + const endRow = position.rowIndex; + let columns = this.getColumnsBetween((this.lastSelectedCell || position).column, position.column); + const isRowsSelection = columns.includes(parseInt(INDEX_COLUMN_DEF.field!)); + + if (isRowsSelection) { + columns = this.getColumnsWithoutIndex(); + } + + this.selection.selectRange( + startRow, + endRow, + columns, + false + ); + this.dispatchChangedEvent(false, true); + this.lastSelectedCell = position; + } + + getCellRanges(): CellRange[] { + return []; + } + + isCellInAnyRange(cell: CellPosition): boolean { + return false; + } + + isCellInSpecificRange(cell: CellPosition, range: CellRange): boolean { + return false; + } + + isEmpty(): boolean { + return true; + } + + removeAllCellRanges(silent?: boolean): void { + this.selection?.clear(); + + if (!silent) { + this.dispatchChangedEvent(false, true); + } + } + + isMoreThanOneCell(): boolean { + return false; + } + + updateRangeEnd(cellRange: CellRange, cellPosition: CellPosition, silent?: boolean) { + console.log('updateRangeEnd'); + } + + private dispatchChangedEvent(started: boolean, finished: boolean): void { + const event: RangeSelectionChangedEvent = Object.freeze({ + type: Events.EVENT_RANGE_SELECTION_CHANGED, + api: this.gridApi, + columnApi: this.columnApi, + started, + finished, + }); + + this.eventService.dispatchEvent(event); + } + + private getColumnsBetween(firstColumn: Column, secondColumn: Column): number[] { + if (firstColumn === secondColumn) { + return [parseInt(firstColumn.getColId())]; + } + + const columns = this.columnController.getAllDisplayedColumns(); + const firstOffset = columns.indexOf(firstColumn); + const secondOffset = columns.indexOf(secondColumn); + + if (firstOffset < 0 || secondOffset < 0) { + return []; + } + + const firstIndex = Math.min(firstOffset, secondOffset); + const lastIndex = Math.max(firstOffset, secondOffset); + + return columns.slice(firstIndex, lastIndex + 1).map(column => parseInt(column.getColId())); + } + + private getColumnsWithoutIndex() { + return this.columnController + .getAllDisplayedColumns() + .map(column => parseInt(column.getColId())) + .filter(column => column !== parseInt(INDEX_COLUMN_DEF.field!)); + } + + getRangeStartRow(cellRange: CellRange): RowPosition { + return { rowIndex: 0, rowPinned: undefined }; + } + + getRangeEndRow(cellRange: CellRange): RowPosition { + return { rowIndex: 0, rowPinned: undefined }; + } + + onDragStart(mouseEvent: MouseEvent): void { + if (!this.selection) { + return; + } + + const startDraggingCell = this.mouseEventService.getCellPositionForEvent(mouseEvent); + + if (_.missing(startDraggingCell)) { + return; + } + + if (startDraggingCell.column.getColId() !== INDEX_COLUMN_DEF.field) { + this.selection.selectCell( + startDraggingCell.rowIndex, + parseInt(startDraggingCell.column.getColId()), + true, + true + ); + } + this.isDragging = true; + this.isDraggingMultiple = mouseEvent.ctrlKey; + this.startDraggingCell = startDraggingCell; + + this.dispatchChangedEvent(true, false); + } + + onDragging(mouseEvent: MouseEvent | null): void { + if (!this.isDragging || !mouseEvent) { + return; + } + + const endDraggingCell = this.mouseEventService.getCellPositionForEvent(mouseEvent); + + if (_.missing(endDraggingCell)) { + return; + } + + this.endDraggingCell = endDraggingCell; + + this.updateDraggingSelection(true); + } + + onDragStop(): void { + if (!this.isDragging) { + return; + } + + this.updateDraggingSelection(false); + this.isDragging = false; + this.isDraggingMultiple = false; + this.startDraggingCell = null; + this.endDraggingCell = null; + } + + private updateDraggingSelection(isExtends: boolean) { + if (!this.selection || !this.startDraggingCell || !this.endDraggingCell) { + return; + } + + let columns = this.getColumnsBetween(this.startDraggingCell.column, this.endDraggingCell.column); + const isRowsSelection = columns.includes(parseInt(INDEX_COLUMN_DEF.field!)); + const startRow = this.startDraggingCell.rowIndex; + const endRow = this.endDraggingCell.rowIndex; + + if (isRowsSelection) { + columns = this.getColumnsWithoutIndex(); + } + const firstRow = Math.min(startRow, endRow); + const lastRow = Math.max(startRow, endRow); + + this.temporaryRange = { + firstRow, + lastRow, + columns, + isMultiple: this.isDraggingMultiple, + }; + + if (!isExtends) { + this.selection.selectRange( + startRow, + endRow, + columns, + this.isDraggingMultiple + ); + this.clearTemporary(); + } + + this.dispatchChangedEvent(false, isExtends); + } + + private clearTemporary() { + this.temporaryRange = null; + } + + private isCellInTemporaryRange(rowId: number, column: number) { + if (!this.temporaryRange + || this.temporaryRange.firstRow > rowId || this.temporaryRange.lastRow < rowId + || !this.temporaryRange.columns.includes(column) + ) { + return false; + } + return true; + } + + onColumnVisibleChange(): void { + console.log('onColumnVisibleChange'); + } + + refreshLastRangeStart(): void { + console.log('refreshLastRangeStart'); + } + + isLastCellOfRange(cellRange: CellRange, cell: CellPosition) { + console.log('isLastCellOfRange'); + return false; + } + + isContiguousRange(cellRange: CellRange) { + console.log('isContiguousRange'); + return false; + } + + getRangeEdgeColumns(cellRange: CellRange) { + console.log('getRangeEdgeColumns'); + } + + extendLatestRangeInDirection(key: number) { + console.log('extendLatestRangeInDirection'); + return undefined; + } + + setCellRange(params: CellRangeParams): void { + console.log('extendLatestRangeInDirection'); + } + + setCellRanges(cellRanges: CellRange[]): void { + console.log('setCellRanges', cellRanges); + } + + createCellRangeFromCellRangeParams(params: CellRangeParams) { + console.log('createCellRangeFromCellRangeParams'); + return undefined; + } + + addCellRange(params: CellRangeParams): void { + console.log('addCellRange', params); + } + + getDraggingRange() { + console.log('getDraggingRange'); + } +} diff --git a/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeSelectionModule.ts b/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeSelectionModule.ts new file mode 100644 index 0000000000..ee059a81b7 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/modules/RangeSelection/rangeSelectionModule.ts @@ -0,0 +1,16 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Module, ModuleNames } from '@ag-grid-community/core'; + +import { RangeController } from './rangeController'; + +export const RangeSelectionModule: Module = { + moduleName: ModuleNames.RangeSelectionModule, + beans: [RangeController], +}; diff --git a/webapp/packages/ag-grid-plugin/src/styles/base.raw.scss b/webapp/packages/ag-grid-plugin/src/styles/base.raw.scss new file mode 100644 index 0000000000..e00c1ddd43 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/styles/base.raw.scss @@ -0,0 +1,40 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +/* changes the default icon color*/ +/* $icon-color: #03a9f4; */ + +/* changes the selected checkbox check mark color*/ +/* $accent-color: #03a9f4; */ + +$hover-color: cornsilk; +$background-color: transparent; + +/* $ag-range-selected-color-1: rgb(250, 235, 215); +$ag-range-selected-color-2: darken($ag-range-selected-color-1, 10%); +$ag-range-selected-color-3: darken($ag-range-selected-color-1, 20%); +$ag-range-selected-color-4: darken($ag-range-selected-color-1, 30%); */ + +@import "ag-grid-community/src/styles/ag-grid.scss"; +@import "ag-grid-community/src/styles/ag-theme-balham/sass/ag-theme-balham.scss"; + +.ag-theme-balham { + .ag-react-container { + position: relative; + display: flex; + width: 100%; + height: 100%; + padding-left: 12px; + padding-right: 12px; + } + + .ag-cell.ag-cell-inline-editing { + overflow: visible; + height: 27px; + } +} diff --git a/webapp/packages/ag-grid-plugin/src/styles/themes/_styles.scss b/webapp/packages/ag-grid-plugin/src/styles/themes/_styles.scss new file mode 100644 index 0000000000..9cccefe272 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/styles/themes/_styles.scss @@ -0,0 +1,88 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@import "@dbeaver/core/assets/styles/branding"; + +ag-grid-theme { + composes: ag-theme-balham from global; //reshadow compose + height: 100%; + + &:global(.ag-theme-balham) { + background-color: transparent !important; + + :global(.ag-root) { + border: none !important; + background-color: transparent !important; + } + + :global(.ag-header) { + border-bottom: 1px solid; + @include mdc-theme-prop(color, on-surface, false); + @include mdc-theme-prop(border-color, background, false); + } + + :global(.ag-header), + :global(.ag-row-even), + :global(.ag-row-odd) { + background: transparent; + } + + :global(.ag-row) { + border-width: 0px 0 1px !important; + @include mdc-theme-prop(color, on-surface, false); + @include mdc-theme-prop(border-color, background, false); + } + + :global(.ag-row-selected) { + border-color: #03a9f4; + background-color: transparent; + @include stripes-background($mdc-theme-secondary); + } + + :global(.ag-cell-range-selected-1) { + background-color: rgba(0, 145, 234, 0.3); + } + + :global(.ag-row-hover) { + background-color: transparent; + @include stripes-background($mdc-theme-secondary); + border-color: #01cca3; + } + + :global(.ag-cell.ag-cell-inline-editing) { + border: 0 !important; + border-radius: 0; + @include mdc-theme-prop(background-color, surface, false); + } + + :global(.ag-header-cell), + :global(.ag-header-group-cell) { + padding-left: 0; + padding-right: 0; + } + + :global(.ag-header-cell)::after, + :global(.ag-header-group-cell)::after { + height: 100%; + margin-top: 0; + border-right: 1px solid; + @include mdc-theme-prop(border-right-color, background, false); + } + + :global(.ag-ltr) + :global(.ag-cell):not(:global(.ag-cell-focus)):global(.ag-cell-last-left-pinned):not(:global(.ag-cell-range-right)), + :global(.ag-ltr) + :global(.ag-cell):global(.ag-cell-range-selected):not(:global(.ag-cell-range-single-cell)):global(.ag-cell-last-left-pinned):not(:global(.ag-cell-range-right)), + :global(.ag-ltr) + :global(.ag-root):not(:global(.ag-has-focus)) + :global(.ag-cell):global(.ag-cell-last-left-pinned):not(:global(.ag-cell-range-right)) { + border-right: 1px solid; + @include mdc-theme-prop(border-right-color, background, false); + } + } +} diff --git a/webapp/packages/ag-grid-plugin/src/styles/themes/dark.module.scss b/webapp/packages/ag-grid-plugin/src/styles/themes/dark.module.scss new file mode 100644 index 0000000000..5619680726 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/styles/themes/dark.module.scss @@ -0,0 +1,10 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@import '@dbeaver/core/assets/styles/theme-dark'; +@import 'styles'; diff --git a/webapp/packages/ag-grid-plugin/src/styles/themes/light.module.scss b/webapp/packages/ag-grid-plugin/src/styles/themes/light.module.scss new file mode 100644 index 0000000000..9515b97995 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/src/styles/themes/light.module.scss @@ -0,0 +1,10 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@import '@dbeaver/core/assets/styles/theme-light'; +@import "styles"; diff --git a/webapp/packages/ag-grid-plugin/tsconfig.build.json b/webapp/packages/ag-grid-plugin/tsconfig.build.json new file mode 100644 index 0000000000..fbd011158d --- /dev/null +++ b/webapp/packages/ag-grid-plugin/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declarationDir": "dist/types", + "declaration": true, + "noEmit": false, + "outDir": "dist/js" + }, + "exclude": [ + "node_modules", + "@dbeaver/core/**" + ], + "include": ["src/**/*"] +} diff --git a/webapp/packages/ag-grid-plugin/tsconfig.json b/webapp/packages/ag-grid-plugin/tsconfig.json new file mode 100644 index 0000000000..6b4c45f333 --- /dev/null +++ b/webapp/packages/ag-grid-plugin/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "@dbeaver/**" + ] +} diff --git a/webapp/packages/basic-connection-plugin/.gitignore b/webapp/packages/basic-connection-plugin/.gitignore new file mode 100644 index 0000000000..d443c6b30b --- /dev/null +++ b/webapp/packages/basic-connection-plugin/.gitignore @@ -0,0 +1,18 @@ + +# dependencies +/node_modules + +# testing +/coverage + +# production +/dist + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/basic-connection-plugin/package.json b/webapp/packages/basic-connection-plugin/package.json new file mode 100644 index 0000000000..31faf35680 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/package.json @@ -0,0 +1,34 @@ +{ + "name": "@dbeaver/basic-connection-plugin", + "version": "0.1.0", + "description": "", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/types", + "scripts": { + "build-plugin": "npm run clean-dist && webpack -p --config=configs/webpack/build.js", + "clean-dist": "rimraf dist/*", + "rollup": "npm run clean-dist && rollup --config ../../configs/rollup.config.js", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "license": "Apache-2.0", + "peerDependencies": { + "@dbeaver/core": "^0.1.0", + "@material/button": "^4.0.0", + "@material/elevation": "^4.0.0", + "@material/ripple": "^4.0.0", + "@material/theme": "^4.0.0", + "@material/typography": "^4.0.0", + "go-di": "^1.5.2", + "graphql": "^14.5.8", + "graphql-request": "^1.8.2", + "graphql-tag": "^2.10.1", + "mobx": "^5.15.0", + "mobx-react": "^6.1.4", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "reakit": "^1.0.0-beta.11", + "reshadow": "0.0.1-alpha.73", + "rxjs": "^6.5.4" + } +} diff --git a/webapp/packages/basic-connection-plugin/src/BasicConnectionPluginBootstrap.ts b/webapp/packages/basic-connection-plugin/src/BasicConnectionPluginBootstrap.ts new file mode 100644 index 0000000000..00b684a74c --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/BasicConnectionPluginBootstrap.ts @@ -0,0 +1,56 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { MainMenuService, ConnectionDialogsService } from '@dbeaver/core/app'; +import { injectable } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; +import { NotificationService } from '@dbeaver/core/eventsLog'; + +import { BasicConnectionService } from './BasicConnectionService'; +import { ConnectionDialog } from './ConnectionDialog/ConnectionDialog'; + +@injectable() +export class BasicConnectionPluginBootstrap { + + private hasPreconfiguredConnection = false; + + constructor(private connectionDialogsService: ConnectionDialogsService, + private mainMenuService: MainMenuService, + private basicConnectionService: BasicConnectionService, + private notificationService: NotificationService, + private commonDialogService: CommonDialogService) { + } + + bootstrap() { + this.loadDbSources(); + this.mainMenuService.registerMenuItem( + this.connectionDialogsService.newConnectionMenuToken, + { + id: 'mainMenuConnect', + order: 2, + title: 'Preconfigured', + onClick: () => this.openConnectionsDialog(), + isDisabled: () => !this.hasPreconfiguredConnection, + } + ); + } + + private async openConnectionsDialog() { + await this.commonDialogService.open(ConnectionDialog, null); + } + + private async loadDbSources() { + try { + const sources = await this.basicConnectionService.loadDBSourcesAsync(); + this.hasPreconfiguredConnection = !!sources.length; + } catch (error) { + this.notificationService.logException(error, 'DBSources loading failed'); + } + } + +} diff --git a/webapp/packages/basic-connection-plugin/src/BasicConnectionService.ts b/webapp/packages/basic-connection-plugin/src/BasicConnectionService.ts new file mode 100644 index 0000000000..2836903fd3 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/BasicConnectionService.ts @@ -0,0 +1,40 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { Connection, DBSource, ConnectionsManagerService } from '@dbeaver/core/app'; +import { injectable } from '@dbeaver/core/di'; +import { ConnectionConfig, GraphQLService } from '@dbeaver/core/sdk'; + +@injectable() +export class BasicConnectionService { + private dbSourcesCache: DBSource[] = []; + + constructor(private graphQLService: GraphQLService, + private connectionsManagerService: ConnectionsManagerService) { + } + + public getDBSources(): DBSource[] { + return this.dbSourcesCache; + } + + async loadDBSourcesAsync(): Promise { + if (this.dbSourcesCache.length > 0) { + return this.dbSourcesCache; + } + + const { dataSourceList } = await this.graphQLService.gql.dataSourceList(); + this.dbSourcesCache = dataSourceList; + return this.dbSourcesCache; + } + + async openConnectionAsync(config: ConnectionConfig): Promise { + const response = await this.graphQLService.gql.openConnection({ config }); + this.connectionsManagerService.addOpenedConnection(response.openConnection); + return response.openConnection; + } +} diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/Connection.tsx b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/Connection.tsx new file mode 100644 index 0000000000..e4ab9c0ceb --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/Connection.tsx @@ -0,0 +1,69 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { InputField } from '@dbeaver/core/blocks'; +import { useTranslate } from '@dbeaver/core/localization'; +import { useStyles } from '@dbeaver/core/theming'; + + +const styles = css` + custom-connection { + flex: 1; + box-sizing: border-box; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + InputField { + min-width: 380px; + } +`; + +type ConnectionProps = { + userName?: string; + userPassword?: string; + isConnecting: boolean; + onChange(property: 'userName' | 'userPassword', value: string): void; +} + +export const Connection = observer(function Connection({ + userName, userPassword, isConnecting, onChange, +}: ConnectionProps) { + const translate = useTranslate(); + + return styled(useStyles(styles))( + +
+ onChange('userName', value)} + disabled={isConnecting} + mod='surface' + > + {translate('customConnection_userName')} + + onChange('userPassword', value)} + disabled={isConnecting} + mod='surface' + > + {translate('customConnection_Password')} + +
+
+ ); +}); diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionController.ts b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionController.ts new file mode 100644 index 0000000000..728735a716 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionController.ts @@ -0,0 +1,164 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observable, action } from 'mobx'; + +import { + ConnectionsManagerService, DBSource, ErrorDetailsDialog, +} from '@dbeaver/core/app'; +import { injectable, IInitializableController, IDestructibleController } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; +import { NotificationService } from '@dbeaver/core/eventsLog'; +import { ConnectionConfig, GQLError } from '@dbeaver/core/sdk'; + +import { BasicConnectionService } from '../BasicConnectionService'; + +export enum ConnectionStep { + DBSource, + Connection +} + +export interface IConnectionController { + dbSource: DBSource | null; + config: ConnectionConfig; + isConnecting: boolean; + onChange(property: T, value: ConnectionConfig[T]): void; + onConnect(): void; +} + +@injectable() +export class ConnectionController +implements IInitializableController, IDestructibleController, IConnectionController { + @observable step = ConnectionStep.DBSource + @observable isLoading = true; + @observable isConnecting = false; + @observable dbSource: DBSource | null = null + @observable config: ConnectionConfig = { + userName: '', + userPassword: '', + } + @observable hasDetails = false + @observable responseMessage: string | null = null + + private exception: GQLError | null = null; + private onClose!: () => void + private isDistructed = false; + + get dbSources() { + return this.basicConnectionService.getDBSources(); + } + + get dbDrivers() { + return this.connectionsManagerService.getDBDrivers(); + } + + get dbDriver() { + if (!this.dbSource) { + return undefined; + } + return this.dbDrivers.get(this.dbSource.driverId); + } + + constructor(private connectionsManagerService: ConnectionsManagerService, + private basicConnectionService: BasicConnectionService, + private notificationService: NotificationService, + private commonDialogService: CommonDialogService) { } + + init(onClose: () => void) { + this.onClose = onClose; + this.loadDBSources(); + } + + destruct(): void { + this.isDistructed = true; + } + + onStep = (step: ConnectionStep) => { + this.step = step; + this.clearError(); + } + + onChange = (property: keyof ConnectionConfig, value: any) => { + this.config[property] = value; + } + + onConnect = async () => { + this.isConnecting = true; + this.clearError(); + try { + const connection = await this.basicConnectionService.openConnectionAsync(this.getConnectionConfig()); + + this.notificationService.logInfo({ title: `Connection ${connection.name} established` }); + this.onClose(); + } catch (exception) { + this.showError(exception, 'Failed to establish connection'); + } finally { + this.isConnecting = false; + } + } + + onDBSourceSelect = (sourceId: string) => { + this.dbSource = this.dbSources.find(dbSource => dbSource.id === sourceId)!; + this.clearError(); + this.setDBSourceDefaults(); + + this.step = ConnectionStep.Connection; + if (this.dbDriver && this.dbDriver.anonymousAccess) { + this.onConnect(); + } + } + + onShowDetails = () => { + if (this.exception) { + this.commonDialogService.open(ErrorDetailsDialog, this.exception); + } + } + + private getConnectionConfig(): ConnectionConfig { + const config: ConnectionConfig = {}; + config.dataSourceId = this.config.dataSourceId; + config.userName = this.config.userName; + config.userPassword = this.config.userPassword; + + return config; + } + + @action + private setDBSourceDefaults() { + this.config.dataSourceId = this.dbSource?.id; + this.config.userName = ''; + this.config.userPassword = ''; + } + + private clearError() { + this.responseMessage = null; + this.hasDetails = false; + this.exception = null; + } + + private showError(exception: Error, message: string) { + if (exception instanceof GQLError && !this.isDistructed) { + this.responseMessage = exception.errorText; + this.hasDetails = exception.hasDetails(); + this.exception = exception; + } else { + this.notificationService.logException(exception, message); + } + } + + private async loadDBSources() { + try { + await this.basicConnectionService.loadDBSourcesAsync(); + await this.connectionsManagerService.loadDriversAsync(); + } catch (exception) { + this.notificationService.logException(exception, 'Can\'t load database sources'); + } finally { + this.isLoading = false; + } + } +} diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialog.tsx b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialog.tsx new file mode 100644 index 0000000000..40c4db7311 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialog.tsx @@ -0,0 +1,97 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { ErrorMessage, SubmittingForm, Loader } from '@dbeaver/core/blocks'; +import { useController } from '@dbeaver/core/di'; +import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@dbeaver/core/dialogs'; +import { useLocale, useTranslate } from '@dbeaver/core/localization'; +import { useStyles } from '@dbeaver/core/theming'; + +import { Connection } from './Connection'; +import { ConnectionController, ConnectionStep } from './ConnectionController'; +import { ConnectionDialogFooter } from './ConnectionDialogFooter'; +import { DBSourceSelector } from './DBSourceSelector/DBSourceSelector'; + +const styles = css` + CommonDialogWrapper { + display: flex; + flex-direction: column; + max-height: 330px; + min-height: 330px; + } + SubmittingForm, center { + display: flex; + flex: 1; + } + center { + box-sizing: border-box; + flex-direction: column; + align-items: center; + justify-content: center; + } +`; + +export const ConnectionDialog: DialogComponent = observer( + function ConnectionDialog(props: DialogComponentProps) { + const controller = useController(ConnectionController, props.rejectDialog); + const translate = useTranslate(); + let title = useLocale('basicConnection_connectionDialog_newConnection'); + + if (controller.step === ConnectionStep.Connection && controller.dbSource?.name) { + title = controller.dbSource.name; + } + + return styled(useStyles(styles))( + controller.onStep(ConnectionStep.DBSource)} + onConnect={controller.onConnect} + /> + )} + onReject={props.rejectDialog} + > + {controller.isLoading && } + {!controller.isLoading && controller.step === ConnectionStep.DBSource && ( + + )} + {controller.step === ConnectionStep.Connection && (controller.dbDriver?.anonymousAccess ? ( +
+ {controller.isConnecting && translate('basicConnection_connectionDialog_connecting_message')} +
+ ) : ( + + + + ))} + {controller.responseMessage && ( + + )} +
+ ); + } +); diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialogFooter.tsx b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialogFooter.tsx new file mode 100644 index 0000000000..06f941c495 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/ConnectionDialogFooter.tsx @@ -0,0 +1,68 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { Button } from '@dbeaver/core/blocks'; +import { useTranslate } from '@dbeaver/core/localization'; + +const styles = css` +controls { + display: flex; + height: 100%; + align-items: center; +} + +fill { + flex: 1; +} + +Button:not(:first-child) { + margin-left: 24px; +} +`; + +type ConnectionDialogFooterProps = { + isConnecting: boolean; + onConnect(): void; + onBack(): void; +} + +export const ConnectionDialogFooter = observer( + function ConnectionDialogFooter({ + isConnecting, + onConnect, + onBack, + }: ConnectionDialogFooterProps) { + const translate = useTranslate(); + return styled(styles)( + + + + + + ); + } +); diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceItem.tsx b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceItem.tsx new file mode 100644 index 0000000000..3ff3bc57fc --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceItem.tsx @@ -0,0 +1,81 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { DBDriver, DBSource } from '@dbeaver/core/app'; +import { StaticImage } from '@dbeaver/core/blocks'; +import { useStyles, composes } from '@dbeaver/core/theming'; + +const styles = composes( + css` + db-source-name { + composes: theme-border-color-secondary from global; + } + `, + css` + StaticImage { + box-sizing: border-box; + width: 24px; + } + db-source { + position: relative; + cursor: pointer; + display: flex; + box-sizing: border-box; + align-items: center; + padding: 8px 12px; + } + db-source-icon { + display: flex; + align-items: center; + box-sizing: border-box; + padding: 0 12px; + } + db-source-name { + composes: theme-typography--body1 from global; + box-sizing: border-box; + font-weight: 500; + min-width: 250px; + padding: 0 24px 0 12px; + border-right: 1px solid; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + db-source-description { + composes: theme-typography--body2 from global; + box-sizing: border-box; + max-width: 460px; + padding: 0 12px 0 24px; + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +` +); + +type DBSourceItemProps = { + dbSource: DBSource; + dbDriver?: DBDriver; + onSelect(driverId: string): void; +} + +export const DBSourceItem = observer(function DBSourceItem({ dbSource, dbDriver, onSelect }: DBSourceItemProps) { + + return styled(useStyles(styles))( + onSelect(dbSource.id)}> + + {dbSource.name} + {dbSource.description} + + ); +}); diff --git a/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceSelector.tsx b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceSelector.tsx new file mode 100644 index 0000000000..4578a3cc32 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/ConnectionDialog/DBSourceSelector/DBSourceSelector.tsx @@ -0,0 +1,101 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { useState, useMemo } from 'react'; +import styled, { css, use } from 'reshadow'; + +import { DBDriver, DBSource } from '@dbeaver/core/app'; +import { StaticImage } from '@dbeaver/core/blocks'; +import { useTranslate } from '@dbeaver/core/localization'; +import { useStyles, composes } from '@dbeaver/core/theming'; + +import { DBSourceItem } from './DBSourceItem'; + +const styles = composes( + css` + list-item { + composes: theme-ripple theme-background-surface theme-border-color-secondary from global; + } + list-search { + composes: theme-background-secondary from global; + } + `, + css` + list { + box-sizing: border-box; + border-collapse: collapse; + } + list-item { + border-bottom: 1px solid; + } + list-search { + position: sticky; + top: 0; + padding: 8px 24px; + z-index: 1; + + & input { + padding: 4px 24px; + padding-left: 32px; + } + + & StaticImage { + position: absolute; + top: 15px; + left: 32px; + } + } + ` +); + +type DBSourceSelectorProps = { + dbSources: DBSource[]; + dbDrivers: Map; + className?: string; + onSelect(dbSourceId: string): void; +} + +export const DBSourceSelector = observer(function DBSourceSelector({ + dbSources, + dbDrivers, + className, + onSelect, +}: DBSourceSelectorProps) { + const [search, setSearch] = useState(''); + const translate = useTranslate(); + const filteredDBSources = useMemo(() => { + if (!search) { + return dbSources; + } + return dbSources.filter(source => source.name.toUpperCase().includes(search.toUpperCase())); + }, [search, dbSources]); + + return styled(useStyles(styles))( + + + + setSearch(event.target.value)} + {...use({ mod: 'surface' })} + /> + + {filteredDBSources.map(dbSource => ( + + + + ))} + + ); +}); diff --git a/webapp/packages/basic-connection-plugin/src/index.ts b/webapp/packages/basic-connection-plugin/src/index.ts new file mode 100644 index 0000000000..9bcec1110c --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/index.ts @@ -0,0 +1,3 @@ +import { basicConnectionPluginManifest } from './manifest'; + +export default basicConnectionPluginManifest; diff --git a/webapp/packages/basic-connection-plugin/src/manifest.ts b/webapp/packages/basic-connection-plugin/src/manifest.ts new file mode 100644 index 0000000000..8a41c4f742 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/src/manifest.ts @@ -0,0 +1,28 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { PluginManifest } from '@dbeaver/core/di'; + +import { BasicConnectionPluginBootstrap } from './BasicConnectionPluginBootstrap'; +import { BasicConnectionService } from './BasicConnectionService'; + +export const basicConnectionPluginManifest: PluginManifest = { + info: { + name: 'Basic connection plugin', + }, + + providers: [ + BasicConnectionService, + ], + + initialize(services): void { + services + .resolveServiceByClass(BasicConnectionPluginBootstrap) + .bootstrap(); + }, +}; diff --git a/webapp/packages/basic-connection-plugin/tsconfig.build.json b/webapp/packages/basic-connection-plugin/tsconfig.build.json new file mode 100644 index 0000000000..dd700023ea --- /dev/null +++ b/webapp/packages/basic-connection-plugin/tsconfig.build.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declarationDir": "dist/types", + "declaration": true, + "noEmit": false, + "outDir": "dist/js" + }, + "exclude": [ + "node_modules", + "@dbeaver/**" + ], + "include": ["src/**/*"] +} diff --git a/webapp/packages/basic-connection-plugin/tsconfig.json b/webapp/packages/basic-connection-plugin/tsconfig.json new file mode 100644 index 0000000000..cf0afdc7c0 --- /dev/null +++ b/webapp/packages/basic-connection-plugin/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "@dbeaver/**" + ] +} diff --git a/webapp/packages/core/.gitignore b/webapp/packages/core/.gitignore new file mode 100644 index 0000000000..12fec39440 --- /dev/null +++ b/webapp/packages/core/.gitignore @@ -0,0 +1,22 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +/dist + +# misc +.DS_Store +.env* + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/webapp/packages/core/app/package.json b/webapp/packages/core/app/package.json new file mode 100644 index 0000000000..f3a04286c0 --- /dev/null +++ b/webapp/packages/core/app/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/app", + "private": true, + "sideEffects": false, + "main": "../dist/app", + "module": "../dist/app", + "types": "../dist/types/app" +} diff --git a/webapp/packages/core/assets/locales/en.d.ts b/webapp/packages/core/assets/locales/en.d.ts new file mode 100644 index 0000000000..d7ab607d8f --- /dev/null +++ b/webapp/packages/core/assets/locales/en.d.ts @@ -0,0 +1,3 @@ + +declare const en: { [key: string]: string }; +export default en; diff --git a/webapp/packages/core/assets/locales/en.js b/webapp/packages/core/assets/locales/en.js new file mode 100644 index 0000000000..cd91d75938 --- /dev/null +++ b/webapp/packages/core/assets/locales/en.js @@ -0,0 +1,68 @@ +/** DEFAULT */ + +module.exports = { + name: 'en', + basicConnection_connectionDialog_newConnection: 'New connection', + basicConnection_connectionDialog_title: 'Connect to Database', + basicConnection_connectionDialog_listTitle: 'Database:', + basicConnection_connectionDialog_username: 'Database Username:', + basicConnection_connectionDialog_usernamePlaceholder: 'user', + basicConnection_connectionDialog_password: 'Database User Password:', + basicConnection_connectionDialog_passwordPlaceholder: 'password', + basicConnection_connectionDialog_connecting: 'Connecting...', + basicConnection_connectionDialog_connect: 'Connect', + basicConnection_connectionDialog_connecting_message: 'Connecting to database...', + + customConnection_connectionType_custom: 'Parameters', + customConnection_connectionType_url: 'URL', + customConnection_options: 'Options', + customConnection_properties: 'Driver Properties', + customConnection_properties_name: 'Name', + customConnection_properties_value: 'Value', + customConnection_properties_add: 'Add', + customConnection_custom_host: 'Host', + customConnection_custom_obligatory: '(obligatory)', + customConnection_custom_port: 'Port', + customConnection_custom_database: 'Database', + customConnection_custom_server: 'Server', + customConnection_url_JDBC: 'JDBC URL', + customConnection_userName: 'User name', + customConnection_Password: 'Password', + customConnection_test: 'Test Connection', + customConnection_create: 'Create', + + ui_stepper_back: 'Back', + ui_processing_loading: 'Loading...', + ui_processing_cancel: 'Cancel', + ui_error_message: 'Error:', + ui_errors_details: 'Details', + ui_search: 'Search...', + + app_root_session_expired_title: 'Session expired', + app_root_session_expired_message: 'Session has been expired. Do you want to reload?', + app_root_session_expired_reload: 'Reload', + + app_shared_connectionMenu_newConnection: 'New connection', + app_shared_connectionMenu_connection: 'Connection', + app_shared_connectionMenu_custom: 'Custom', + app_shared_connectionMenu_disconnect: 'Disconnect', + + app_shared_toolsMenu_tools: 'Tools', + app_shared_toolsMenu_logViewer: 'Log viewer', + + app_shared_settingsMenu_config: 'Configuration', + app_shared_settingsMenu_theme: 'Theme', + app_shared_settingsMenu_lang: 'Language', + + core_eventsLog_dbeaverErrorDetails: 'DBeaver error details', + + app_shared_inlineEditor_dialog_title: 'Edit mode', + app_shared_inlineEditor_dialog_apply: 'Apply', + app_shared_inlineEditor_dialog_cancel: 'Cancel', + + app_log_view_clear_log: 'Clear logs', + app_log_view_entry_type: 'Type', + app_log_view_entry_timestamp: 'Timestamp', + app_log_view_entry_message: 'Message', + app_log_view_entry_details: 'Details', +}; diff --git a/webapp/packages/core/assets/locales/ru.js b/webapp/packages/core/assets/locales/ru.js new file mode 100644 index 0000000000..e60fb9ebd1 --- /dev/null +++ b/webapp/packages/core/assets/locales/ru.js @@ -0,0 +1,67 @@ + +module.exports = { + name: 'ru', + basicConnection_connectionDialog_newConnection: 'Новое подключение', + basicConnection_connectionDialog_title: 'Подключение к базе данных', + basicConnection_connectionDialog_listTitle: 'Базы данных:', + basicConnection_connectionDialog_username: 'Имя пользователя:', + basicConnection_connectionDialog_usernamePlaceholder: 'user', + basicConnection_connectionDialog_password: 'Пароль пользователя:', + basicConnection_connectionDialog_passwordPlaceholder: 'password', + basicConnection_connectionDialog_connecting: 'Подключение...', + basicConnection_connectionDialog_connect: 'Подключиться', + basicConnection_connectionDialog_connecting_message: 'Подключение к базе...', + + customConnection_connectionType_custom: 'Параметры', + customConnection_connectionType_url: 'URL', + customConnection_options: 'Подключение', + customConnection_properties: 'Параметры драйвера', + customConnection_properties_name: 'Название', + customConnection_properties_value: 'Значение', + customConnection_properties_add: 'Добавить', + customConnection_custom_host: 'Хост', + customConnection_custom_obligatory: '(обязательное)', + customConnection_custom_port: 'Порт', + customConnection_custom_database: 'База', + customConnection_custom_server: 'Сервер', + customConnection_url_JDBC: 'JDBC URL', + customConnection_userName: 'Пользователь', + customConnection_Password: 'Пароль', + customConnection_test: 'Проверить', + customConnection_create: 'Создать', + + ui_stepper_back: 'Назад', + ui_processing_loading: 'Загрузка...', + ui_processing_cancel: 'Отменить...', + ui_error_message: 'Ошибка:', + ui_errors_details: 'Информация', + ui_search: 'Поиск...', + + app_root_session_expired_title: 'Сесстия истекла', + app_root_session_expired_message: 'Сессия истекла. Хотите перезагрузить страницу?', + app_root_session_expired_reload: 'Перезагрузить', + + app_shared_connectionMenu_newConnection: 'Новое подключение', + app_shared_connectionMenu_connection: 'Подключение', + app_shared_connectionMenu_custom: 'Custom', + app_shared_connectionMenu_disconnect: 'Отключить', + + app_shared_toolsMenu_tools: 'Инструменты', + app_shared_toolsMenu_logViewer: 'Просмотр лога', + + app_shared_settingsMenu_config: 'Настройки', + app_shared_settingsMenu_theme: 'Тема', + app_shared_settingsMenu_lang: 'Язык', + + core_eventsLog_dbeaverErrorDetails: 'Детали ошибки', + + app_shared_inlineEditor_dialog_title: 'Редактирование', + app_shared_inlineEditor_dialog_apply: 'Применить', + app_shared_inlineEditor_dialog_cancel: 'Отменить', + + app_log_view_clear_log: 'Очистить лог', + app_log_view_entry_type: 'Тип', + app_log_view_entry_timestamp: 'Время', + app_log_view_entry_message: 'Сообщение', + app_log_view_entry_details: 'Детали', +}; diff --git a/webapp/packages/core/assets/package.json b/webapp/packages/core/assets/package.json new file mode 100644 index 0000000000..a11594677e --- /dev/null +++ b/webapp/packages/core/assets/package.json @@ -0,0 +1,7 @@ +{ + "name": "@dbeaver/core/assets", + "private": true, + "sideEffects": false, + "main": ".", + "module": "." +} diff --git a/webapp/packages/core/assets/styles/_branding.scss b/webapp/packages/core/assets/styles/_branding.scss new file mode 100644 index 0000000000..1b07f371db --- /dev/null +++ b/webapp/packages/core/assets/styles/_branding.scss @@ -0,0 +1,29 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin stripes-background($color) { + background-image: linear-gradient( + 45deg, + transparent 37.5%, + $color 37.5%, + $color 50%, + transparent 50%, + transparent 87.5%, + $color 87.5%, + $color 100% + ); + background-size: 4px 4px; +} + +@mixin delimiter($color) { + hr { + border-color: $color; + border-style: solid; + border-bottom: none; + } +} diff --git a/webapp/packages/core/assets/styles/_button.scss b/webapp/packages/core/assets/styles/_button.scss new file mode 100644 index 0000000000..ed4eb7dc7b --- /dev/null +++ b/webapp/packages/core/assets/styles/_button.scss @@ -0,0 +1,62 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin button-core-styles() { + $mdc-button-disabled-container-fill-color: #cccccc; + $mdc-button-density-scale: -2; + $query: mdc-feature-all(); + @include mdc-button-core-styles; + @include mdc-button-theme-baseline; + + .theme-button { + composes: mdc-button; + height: 28px; + } + .theme-button_ripple { + composes: mdc-button__ripple; + } + .theme-button_raised { + composes: mdc-button--raised; + } + .theme-button_unelevated { + composes: mdc-button--unelevated; + } + .theme-button_outlined { + composes: mdc-button--outlined; + } + .theme-button_secondary { + @include mdc-button-ink-color(secondary, $query); + @include mdc-states($color: secondary, $query: $query, $ripple-target: $mdc-button-ripple-target); + + &.theme-button_outlined { + @include mdc-button-outline-color(secondary, $query); + } + + &.theme-button_raised, + &.theme-button_unelevated { + @include mdc-button-container-fill-color(secondary, $query); + @include mdc-button-ink-color(on-secondary, $query); + @include mdc-states($color: on-secondary, $query: $query, $ripple-target: $mdc-button-ripple-target); + } + } + .theme-button_background { + @include mdc-button-ink-color(background, $query); + @include mdc-states($color: background, $query: $query, $ripple-target: $mdc-button-ripple-target); + + &.theme-button_outlined { + @include mdc-button-outline-color(background, $query); + } + + &.theme-button_raised, + &.theme-button_unelevated { + @include mdc-button-container-fill-color(background, $query); + @include mdc-button-ink-color(on-secondary, $query); + @include mdc-states($color: on-secondary, $query: $query, $ripple-target: $mdc-button-ripple-target); + } + } +} diff --git a/webapp/packages/core/assets/styles/_color.scss b/webapp/packages/core/assets/styles/_color.scss new file mode 100644 index 0000000000..c0fd2680ef --- /dev/null +++ b/webapp/packages/core/assets/styles/_color.scss @@ -0,0 +1,64 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin colors($query: mdc-feature-all()) { + $feat-color: mdc-feature-create-target($query, color); + + @each $style in map-keys($mdc-theme-property-values) { + @if $style != "background" and $style != "surface" and $style != "on-surface" { + .theme-text-#{$style} { + @include theme-ripple-color($query, $style); + + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(color, $style, true); + } + + :global(svg), + & > :global(svg) { + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(fill, $style, true); + } + } + } + } @else { + .theme-text-#{$style} { + @include theme-ripple-color($query, $style); + + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(color, $style); + } + + :global(svg), + & > :global(svg) { + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(fill, $style); + } + } + } + .theme-background-#{$style} { + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(background-color, $style); + } + } + } + .theme-border-color-#{$style} { + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(border-color, $style, true); + } + } + } + + // CSS rules for using primary and secondary (plus light/dark variants) as background colors. + @each $style in ("primary", "secondary") { + .theme-background-#{$style} { + @include mdc-feature-targets($feat-color) { + @include mdc-theme-prop(background-color, $style, true); + } + } + } +} diff --git a/webapp/packages/core/assets/styles/_form-controls.scss b/webapp/packages/core/assets/styles/_form-controls.scss new file mode 100644 index 0000000000..d85dd88206 --- /dev/null +++ b/webapp/packages/core/assets/styles/_form-controls.scss @@ -0,0 +1,60 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin form-controls() { + input { + @include mdc-theme-prop(color, on-secondary, false); + @include mdc-theme-prop(background-color, secondary, false); + box-sizing: border-box; + padding: 0 4px; + overflow: hidden; + width: 100%; + border: 1px solid; + border-radius: 3px; + @include mdc-theme-prop(border-color, background, false); + + &:global([type="checkbox"]) { + height: 26px; + width: 14px; + } + + &:global([disabled]), + &:global([readonly]) { + opacity: 0.8; + } + + &[use|mod=surface] { + @include mdc-theme-prop(color, on-surface, false); + @include mdc-theme-prop(background-color, surface, false); + } + } + + label { + display: block; + box-sizing: border-box; + padding: 0; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + width: 100%; + + &:global([title]) { + text-decoration: none; + } + } + + textarea { + box-sizing: border-box; + font-size: 14px; + padding: 12px; + border-radius: 3px; + border: solid 1px $input-border-color; + @include mdc-theme-prop(color, on-surface, false); + @include mdc-theme-prop(background-color, surface, false); + } +} diff --git a/webapp/packages/core/assets/styles/_mixins.scss b/webapp/packages/core/assets/styles/_mixins.scss new file mode 100644 index 0000000000..56b75e78eb --- /dev/null +++ b/webapp/packages/core/assets/styles/_mixins.scss @@ -0,0 +1,23 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@import '@material/theme/mixins'; +@import '@material/ripple/mixins'; +@import '@material/button/mixins'; +@import "@material/elevation/mixins"; +@import "@material/typography/mixins"; +@import 'branding'; +@import 'theme-ripple'; +@import 'ripple'; +@import 'color'; +@import 'button'; +@import 'radio'; +@import 'tabs'; +@import 'table'; +@import 'placeholder'; +@import 'form-controls'; diff --git a/webapp/packages/core/assets/styles/_placeholder.scss b/webapp/packages/core/assets/styles/_placeholder.scss new file mode 100644 index 0000000000..c7a6c1a3e3 --- /dev/null +++ b/webapp/packages/core/assets/styles/_placeholder.scss @@ -0,0 +1,33 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin placeholder { + placeholder { + box-sizing: border-box; + overflow: hidden; + position: relative; + background: #f0f0f0; + border-radius: 3px; + margin: 2px; + animation: wave 1.5s linear infinite; + } + + @keyframes wave { + 0% { + opacity: 0; + } + + 50% { + opacity: 1; + } + + 100% { + opacity: 0; + } + } +} diff --git a/webapp/packages/core/assets/styles/_radio.scss b/webapp/packages/core/assets/styles/_radio.scss new file mode 100644 index 0000000000..99b5ebcf94 --- /dev/null +++ b/webapp/packages/core/assets/styles/_radio.scss @@ -0,0 +1,46 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@import '@material/radio/mixins'; + +@mixin radio-core-styles($query: mdc-feature-all()) { + @include mdc-radio-core-styles; + + .theme-radio { + composes: mdc-radio; + } + .theme-radio_native-control { + composes: mdc-radio__native-control; + } + .theme-radio_background { + composes: mdc-radio__background; + } + .theme-radio_outer-circle { + composes: mdc-radio__outer-circle; + } + .theme-radio_inner-circle { + composes: mdc-radio__inner-circle; + } + .theme-radio_ripple { + composes: mdc-radio__ripple; + } + + .theme-radio_primary { + $mdc-radio-baseline-theme-color: primary; + $mdc-radio-unchecked-color: rgba(mdc-theme-prop-value(on-surface), .54); + $mdc-radio-disabled-circle-color: rgba(mdc-theme-prop-value(on-surface), .26); + + @include mdc-radio-unchecked-stroke-color($mdc-radio-unchecked-color, $query: $query); + @include mdc-radio-checked-stroke-color($mdc-radio-baseline-theme-color, $query: $query); + @include mdc-radio-ink-color($mdc-radio-baseline-theme-color, $query: $query); + @include mdc-radio-disabled-unchecked-stroke-color($mdc-radio-disabled-circle-color, $query: $query); + @include mdc-radio-disabled-checked-stroke-color($mdc-radio-disabled-circle-color, $query: $query); + @include mdc-radio-disabled-ink-color($mdc-radio-disabled-circle-color, $query: $query); + @include mdc-radio-focus-indicator-color($mdc-radio-baseline-theme-color, $query: $query); + } +} \ No newline at end of file diff --git a/webapp/packages/core/assets/styles/_ripple.scss b/webapp/packages/core/assets/styles/_ripple.scss new file mode 100644 index 0000000000..0064d0e43f --- /dev/null +++ b/webapp/packages/core/assets/styles/_ripple.scss @@ -0,0 +1,31 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin ripple-core-styles($query: mdc-feature-all()) { + $feat-structure: mdc-feature-create-target($query, structure); + + .theme-ripple-surface { + @include mdc-ripple-surface($query: $query); + @include mdc-states($query: $query); + @include mdc-ripple-radius-bounded($query: $query); + + @include mdc-feature-targets($feat-structure) { + position: relative; + outline: none; + overflow: hidden; + } + + &--primary { + @include mdc-states(primary, $query: $query); + } + + &--accent { + @include mdc-states(secondary, $query: $query); + } + } +} \ No newline at end of file diff --git a/webapp/packages/core/assets/styles/_table.scss b/webapp/packages/core/assets/styles/_table.scss new file mode 100644 index 0000000000..241be4020a --- /dev/null +++ b/webapp/packages/core/assets/styles/_table.scss @@ -0,0 +1,56 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin table($query: mdc-feature-all()) { + table { + min-width: 100%; + text-align: left; + border-collapse: collapse; + } + + tr { + outline: none; + border-bottom: 1px solid; + @include mdc-theme-prop(border-color, background, false); + } + + tbody tr:hover { + @include stripes-background($mdc-theme-secondary); + } + + tbody tr:focus { + @include mdc-theme-prop(border-color, background, false); + } + + tbody tr[use|isSelected] { + @include stripes-background($mdc-theme-secondary); + @include mdc-theme-prop(border-color, $color-positive, false); + } + + th { + white-space: nowrap; + padding: 16px; + height: 36px; + padding-top: unset; + padding-bottom: unset; + border-right: 1px solid; + @include mdc-theme-prop(border-color, background, false); + text-transform: uppercase; + text-align: left; + text-decoration: none !important; + } + + th:last-child { + border-right: none; + } + + td { + height: 24px; + padding: 0 16px; + } +} diff --git a/webapp/packages/core/assets/styles/_tabs.scss b/webapp/packages/core/assets/styles/_tabs.scss new file mode 100644 index 0000000000..0a14b979c4 --- /dev/null +++ b/webapp/packages/core/assets/styles/_tabs.scss @@ -0,0 +1,116 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin theme-tabs($query: mdc-feature-all()) { + $feat-structure: mdc-feature-create-target($query, structure); + + tabs-box { + display: flex; + flex-direction: column; + flex: 1; + overflow: hidden; + } + + tabs { + display: flex; + flex-direction: row; + flex: 0 0 auto; + overflow: auto; + } + + tab-panels { + flex: 1; + display: flex; + overflow: hidden; + } + + tab-fill { + flex: 1; + } + + Tab { + composes: theme-typography--body2 from global; + position: relative; + display: flex; + flex-shrink: 0; + text-align: left; + align-items: center; + outline: none; + font-weight: normal; + cursor: pointer; + + height: 48px; + padding: 0 24px 0 12px; + border-top: solid 2px transparent; + + &:global([aria-selected]) { + font-weight: 500; + cursor: auto; + border-top-color: $color-negative; + + &:before { + display: none; + } + } + + &:not(:global([aria-selected='true'])) { + background-color: transparent !important; + } + } + + tab-icon { + height: 22px; + padding-right: 8px; + + & StaticImage, + & img { + width: 22px; + } + + & placeholder { + width: 22px; + height: 22px; + } + } + + tab-title placeholder { + width: 80px; + height: 16px; + } + + tab-action:before { + content: ""; + display: block; + left: -2px; + top: -2px; + height: 12px; + width: 12px; + position: absolute; + cursor: pointer; + } + + tab-action { + height: 8px; + width: 8px; + align-self: flex-start; + position: absolute; + top: 4px; + right: 4px; + + & Icon { + display: block; + } + } + + TabPanel { + flex: 1; + display: flex; + overflow: hidden; + outline: none; + } +} diff --git a/webapp/packages/core/assets/styles/_theme-dark.scss b/webapp/packages/core/assets/styles/_theme-dark.scss new file mode 100644 index 0000000000..fc64b8adf0 --- /dev/null +++ b/webapp/packages/core/assets/styles/_theme-dark.scss @@ -0,0 +1,39 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +$mdc-theme-primary: #2a7cb4; +$mdc-theme-on-primary: #ffffff; +$mdc-theme-secondary: #1b1b1d; +$mdc-theme-on-secondary: #cbcbcb; +$mdc-theme-surface: #2b2a32; +$mdc-theme-on-surface: #cbcbcb; +$mdc-theme-background: #474551; +$mdc-theme-error: #e94948; + +$color-positive: #01cca3; +$color-negative: #fd1d48; +$input-border-color: #585958; + +$mdc-theme-text-colors: ( + dark: ( + primary: white, + secondary: rgba(white, 0.7), + hint: rgba(white, 0.5), + disabled: rgba(white, 0.5), + icon: rgba(white, 0.5) + ), + light: ( + primary: rgba(black, 0.87), + secondary: rgba(black, 0.54), + hint: rgba(black, 0.38), + disabled: rgba(black, 0.38), + icon: rgba(black, 0.38) + ) +); + +@import 'mixins'; diff --git a/webapp/packages/core/assets/styles/_theme-light.scss b/webapp/packages/core/assets/styles/_theme-light.scss new file mode 100644 index 0000000000..9e402d6ba6 --- /dev/null +++ b/webapp/packages/core/assets/styles/_theme-light.scss @@ -0,0 +1,21 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +$mdc-theme-primary: #2a7cb4; // main menu background +$mdc-theme-on-primary: #ffffff; // main text color +$mdc-theme-secondary: #f1f1f1; +$mdc-theme-on-secondary: #6d6d6d; +$mdc-theme-surface: #ffffff; +$mdc-theme-on-surface: #6d6d6d; +$mdc-theme-background: #dedede; + +$color-positive: #01cca3; +$color-negative: #fd1d48; +$input-border-color: #dedede; + +@import 'mixins'; diff --git a/webapp/packages/core/assets/styles/_theme-ripple.scss b/webapp/packages/core/assets/styles/_theme-ripple.scss new file mode 100644 index 0000000000..b56901115b --- /dev/null +++ b/webapp/packages/core/assets/styles/_theme-ripple.scss @@ -0,0 +1,118 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +@mixin theme-ripple-color($query, $color) { + .theme-ripple { + @include mdc-states($color, $query: $query); + + &.theme-ripple-selectable { + &:global([aria-selected]) { + &::before { + opacity: mdc-states-opacity($color, focus) + mdc-states-opacity($color, hover); + } + } + } + } + + &.theme-ripple { + @include mdc-states($color, $query: $query); + + &.theme-ripple-selectable { + &:global([aria-selected]) { + &::before { + opacity: mdc-states-opacity($color, focus) + mdc-states-opacity($color, hover); + } + } + } + } +} + +@mixin theme-ripple($query: mdc-feature-all()) { + $feat-structure: mdc-feature-create-target($query, structure); + @include mdc-ripple-common($query); + + .theme-ripple { + position: relative; + + &::before { + @include mdc-ripple-surface($query); + @include mdc-ripple-radius-bounded($query: $query); + + @include mdc-feature-targets($feat-structure) { + position: absolute; + content: ''; + opacity: 0; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; + } + } + + &.theme-ripple-background { + @include mdc-theme-prop(background-color, background, false); + @include mdc-theme-prop(color, text-hint-on-light, false); + + &.theme-ripple-selectable { + &:hover { + @include mdc-theme-prop(color, text-primary-on-light, false); + } + + &:global([aria-selected]) { + @include mdc-theme-prop(background-color, surface, false); + @include mdc-theme-prop(color, text-primary-on-light, false); + + &::before { + opacity: 0 !important; + } + } + } + } + + &.theme-ripple-secondary { + @include mdc-theme-prop(background-color, secondary, false); + @include mdc-theme-prop(color, text-hint-on-light, false); + + &.theme-ripple-selectable { + &:hover { + @include mdc-theme-prop(color, text-primary-on-light, false); + } + + &:global([aria-selected]) { + @include mdc-theme-prop(background-color, surface, false); + @include mdc-theme-prop(color, text-primary-on-light, false); + + &::before { + opacity: 0 !important; + } + } + } + } + + &.theme-ripple-surface { + @include mdc-theme-prop(background-color, surface, false); + @include mdc-theme-prop(color, text-hint-on-light, false); + + &.theme-ripple-selectable { + &:hover { + @include mdc-theme-prop(color, text-primary-on-light, false); + } + + &:global([aria-selected]) { + @include mdc-theme-prop(background-color, surface, false); + @include mdc-theme-prop(color, text-primary-on-light, false); + + &::before { + opacity: 0 !important; + } + } + } + } + } +} \ No newline at end of file diff --git a/webapp/packages/core/blocks/package.json b/webapp/packages/core/blocks/package.json new file mode 100644 index 0000000000..e7db6716f4 --- /dev/null +++ b/webapp/packages/core/blocks/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/blocks", + "private": true, + "sideEffects": false, + "main": "../dist/blocks", + "module": "../dist/blocks", + "types": "../dist/types/blocks" +} diff --git a/webapp/packages/core/codegen.yml b/webapp/packages/core/codegen.yml new file mode 100644 index 0000000000..848e8a262f --- /dev/null +++ b/webapp/packages/core/codegen.yml @@ -0,0 +1,20 @@ +schema: + - ../../../server/bundles/io.cloudbeaver.server/schema/schema.graphqls + - ../../../server/bundles/io.cloudbeaver.service.metadata/schema/service.metadata.graphqls +documents: ./src/**/*.gql +generates: + src/sdk/sdk.ts: + plugins: + - add: '/* eslint-disable */' + - typescript + - typescript-operations + - typescript-graphql-request + config: + skipTypename: true + maybeValue: T + scalars: + Object: any + DateTime: any +hooks: + afterAllFileWrite: + - prettier --write --config ../../.prettierrc.js diff --git a/webapp/packages/core/configs/rollup.config.js b/webapp/packages/core/configs/rollup.config.js new file mode 100644 index 0000000000..2be8deef42 --- /dev/null +++ b/webapp/packages/core/configs/rollup.config.js @@ -0,0 +1,50 @@ +const path = require('path'); + +import {configBuilder, defaultBuilderOptions} from '../../../configs/rollup.config'; + +const options = defaultBuilderOptions; +options.babelConfig = path.join(__dirname, '../../../configs/babel-rollup.config.js'); + +const baseConfig = configBuilder(options); + +const modules = [ + // asserts are not here. it doesn't contain code + + // The order of modules is important. + // The module must be built before it is used in another module + '/di', + '/utils', + '/eventsLog', + + '/settings', + '/theming', + '/localization', + + '/root', + '/sdk', + + '/blocks', + '/dialogs', + '/app', + '', // root module +] + +const config = modules.map(module => { + const moduleConfig = {...baseConfig}; + moduleConfig.input = `src${module}/index.ts`; + moduleConfig.output = [ + { + dir: `dist${module}`, + format: 'esm', + }, + ]; + moduleConfig.external = [ + ...baseConfig.external, + ...modules + .filter(name => name !== module && name !== '') + .map(name => `@dbeaver/core${name}`), + ]; + return moduleConfig; +}) + +export default config; diff --git a/webapp/packages/core/configs/webpack/common-exp.js b/webapp/packages/core/configs/webpack/common-exp.js new file mode 100644 index 0000000000..c8ef080434 --- /dev/null +++ b/webapp/packages/core/configs/webpack/common-exp.js @@ -0,0 +1,24 @@ +const merge = require('webpack-merge'); +const commonConfig = require('../../../../configs/webpack/common'); +const path = require('path'); +const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); + +module.exports = (env, argv) => merge(commonConfig(env, argv), { + resolve: { + alias: { + + // rewrite imports for unnecessary work in `web/src/libs/sdk.ts` + 'graphql-tag': path.join(__dirname, '../../fix-gql.js'), + graphql: path.join(__dirname, '../../fix-gql.js'), + }, + }, + context: path.resolve(__dirname, '../../src'), + plugins: [ + new ForkTsCheckerWebpackPlugin({ + tsconfig: path.resolve(__dirname, '../../tsconfig.json'), + async: false, // slow but run before dev-server starts todo try set true later + }), + + ], + +}); diff --git a/webapp/packages/core/configs/webpack/exp.js b/webapp/packages/core/configs/webpack/exp.js new file mode 100644 index 0000000000..aec151dce2 --- /dev/null +++ b/webapp/packages/core/configs/webpack/exp.js @@ -0,0 +1,44 @@ +// production config +const merge = require('webpack-merge'); +const {resolve} = require('path'); +var PeerDepsExternalsPlugin = require('peer-deps-externals-webpack-plugin'); + +const commonConfig = require('./common-exp'); + +module.exports = (env, argv) => merge(commonConfig(env, argv), { + mode: argv.mode || 'development', + entry: { + app: "./app/index.ts", + blocks: "./blocks/index.ts", + di: "./di/index.ts", + dialogs: "./dialogs/index.ts", + eventsLog: "./eventsLog/index.ts", + localization: "./localization/index.ts", + // queries dir - ignore + root: "./root/index.ts", + sdk: "./sdk/index.ts", + settings: "./settings/index.ts", + theming: "./theming/index.ts", + utils: "./utils/index.ts", + main: "./index.ts", + }, + output: { + filename: (chunkData) => { + console.log('chunkData.chunk.name', chunkData.chunk.name); + return chunkData.chunk.name === 'main' ? 'index.js': '[name]/index.js'; + }, + // filename: 'index.js', + path: resolve(__dirname, '../../dist'), + library: ["@dbeaver/core", "[name]"], + libraryTarget: 'umd', + libraryExport: 'default', + umdNamedDefine: true, + }, + // devtool: 'source-map', + externals: [ + /^@dbeaver\/.+$/ + ], + plugins: [ + new PeerDepsExternalsPlugin(), // allow to treat all peerDependencies as externals + ], +}); diff --git a/webapp/packages/core/di/package.json b/webapp/packages/core/di/package.json new file mode 100644 index 0000000000..2a18d283c7 --- /dev/null +++ b/webapp/packages/core/di/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/di", + "private": true, + "sideEffects": false, + "main": "../dist/di", + "module": "../dist/di", + "types": "../dist/types/di" +} diff --git a/webapp/packages/core/dialogs/package.json b/webapp/packages/core/dialogs/package.json new file mode 100644 index 0000000000..0bf658bdde --- /dev/null +++ b/webapp/packages/core/dialogs/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/dialogs", + "private": true, + "sideEffects": false, + "main": "../dist/dialogs", + "module": "../dist/dialogs", + "types": "../dist/types/dialogs" +} diff --git a/webapp/packages/core/eventsLog/package.json b/webapp/packages/core/eventsLog/package.json new file mode 100644 index 0000000000..88834ce30c --- /dev/null +++ b/webapp/packages/core/eventsLog/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/eventsLog", + "private": true, + "sideEffects": false, + "main": "../dist/eventsLog", + "module": "../dist/eventsLog", + "types": "../dist/types/eventsLog" +} diff --git a/webapp/packages/core/fix-gql.js b/webapp/packages/core/fix-gql.js new file mode 100644 index 0000000000..5f2d0259de --- /dev/null +++ b/webapp/packages/core/fix-gql.js @@ -0,0 +1,5 @@ +/** + * rewrite imports for unnecessary work in `webapp/packages/core/src/libs/sdk.ts` + */ +module.exports = strings => strings[0] +module.exports.print = v => v diff --git a/webapp/packages/core/localization/package.json b/webapp/packages/core/localization/package.json new file mode 100644 index 0000000000..e728c544cf --- /dev/null +++ b/webapp/packages/core/localization/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/localization", + "private": true, + "sideEffects": false, + "main": "../dist/localization", + "module": "../dist/localization", + "types": "../dist/types/localization" +} diff --git a/webapp/packages/core/package.json b/webapp/packages/core/package.json new file mode 100644 index 0000000000..b12b28e001 --- /dev/null +++ b/webapp/packages/core/package.json @@ -0,0 +1,52 @@ +{ + "name": "@dbeaver/core", + "version": "0.1.0", + "buildVersion": "0.13.0", + "description": "", + "module": "dist/index.js", + "types": "dist/types", + "scripts": { + "build-plugin": "npm run clean-dist && webpack -p --config=configs/webpack/exp.js", + "rollup": "npm run clean-dist && rollup --config configs/rollup.config.js", + "clean-dist": "rimraf dist/*", + "build:analyze": "echo \"no analyze specified\" && exit 0", + "lint": "eslint ./src/ --ext .ts,.tsx", + "lint-fix": "eslint ./src/ --ext .ts,.tsx --fix", + "prettify": "prettier --write **/*.ts **/*.tsx", + "gql:gen": "graphql-codegen", + "gql:gen:dev": "graphql-codegen --watch" + }, + "license": "Apache-2.0", + "dependencies": { + "go-di": "^1.5.2", + "graphql-request": "^1.8.2", + "inversify": "^5.0.1", + "react-sanitized-html": "^2.0.0", + "reflect-metadata": "^0.1.13", + "sanitize-html": "^1.22.1", + "uuid": "^3.4.0" + }, + "peerDependencies": { + "go-split": "^1.3.2", + "graphql": "^14.5.8", + "graphql-tag": "^2.10.1", + "mobx": "^5.15.0", + "mobx-react": "^6.1.4", + "react": "^16.12.0", + "react-dom": "^16.12.0", + "reakit": "^1.0.0-beta.11", + "reshadow": "0.0.1-alpha.73", + "rxjs": "^6.5.4" + }, + "devDependencies": { + "@material/button": "^4.0.0", + "@material/elevation": "^4.0.0", + "@material/ripple": "^4.0.0", + "@material/radio": "^4.0.0", + "@material/theme": "^4.0.0", + "@material/typography": "^4.0.0", + "@types/uuid": "^3.4.7", + "graphql-request": "^1.8.2" + }, + "dbeaverPlugins": [] +} diff --git a/webapp/packages/core/root/package.json b/webapp/packages/core/root/package.json new file mode 100644 index 0000000000..293bca0321 --- /dev/null +++ b/webapp/packages/core/root/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/root", + "private": true, + "sideEffects": false, + "main": "../dist/root", + "module": "../dist/root", + "types": "../dist/types/root" +} diff --git a/webapp/packages/core/sdk/package.json b/webapp/packages/core/sdk/package.json new file mode 100644 index 0000000000..3d923a23c6 --- /dev/null +++ b/webapp/packages/core/sdk/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/sdk", + "private": true, + "sideEffects": false, + "main": "../dist/sdk", + "module": "../dist/sdk", + "types": "../dist/types/sdk" +} diff --git a/webapp/packages/core/settings/package.json b/webapp/packages/core/settings/package.json new file mode 100644 index 0000000000..1fa3906d6a --- /dev/null +++ b/webapp/packages/core/settings/package.json @@ -0,0 +1,8 @@ +{ + "name": "@dbeaver/core/settings", + "private": true, + "sideEffects": false, + "main": "../dist/settings", + "module": "../dist/settings", + "types": "../dist/types/settings" +} diff --git a/webapp/packages/core/src/AppBootstrap.ts b/webapp/packages/core/src/AppBootstrap.ts new file mode 100644 index 0000000000..015dd6a91d --- /dev/null +++ b/webapp/packages/core/src/AppBootstrap.ts @@ -0,0 +1,59 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { + ConnectionDialogsService, + ConnectionsManagerService, + NodesManagerService, + NavigationTabsService, NavigationTreeContextMenuService, LogViewerMenuService, +} from '@dbeaver/core/app'; +import { injectable } from '@dbeaver/core/di'; +import { ExceptionsCatcherService } from '@dbeaver/core/eventsLog'; +import { SessionService } from '@dbeaver/core/root'; +import { ThemeService } from '@dbeaver/core/theming'; + +/** + * AppBootstrap.init() will be executed between first and second phase of App initialization, + * when application has registered all dependencies but plugins did'nt initialized. + * It is required to run this code in front of other initialization steps. + * + */ +@injectable() +export class AppBootstrap { + + constructor(private exceptionsCatcherService: ExceptionsCatcherService, + private sessionService: SessionService, + private themeService: ThemeService, + private connectionDialogService: ConnectionDialogsService, + private connectionsManager: ConnectionsManagerService, + private logViewerMenuService: LogViewerMenuService, + private navigationTreeContextMenuService: NavigationTreeContextMenuService, + private nodesManagerService: NodesManagerService, + private navigationTabsService: NavigationTabsService) { + } + + async init() { + this.exceptionsCatcherService.subscribe(); + + await this.sessionService.init(); + await this.themeService.init(); + + this.navigationTreeContextMenuService.registerMenuItems(); + this.connectionDialogService.registerMenuItems(); + this.logViewerMenuService.registerMenuItems(); + + await this.connectionsManager.restoreConnections(); + + await this.nodesManagerService.updateRootChildren(); + } + + async doAfterPluginsInit() { + // todo this should be moved to the NavigationTabs component creation phase but now it leads to bugs + await this.navigationTabsService.restoreTabs(); + } +} diff --git a/webapp/packages/core/src/app/Body.tsx b/webapp/packages/core/src/app/Body.tsx new file mode 100644 index 0000000000..c31bcd7ffc --- /dev/null +++ b/webapp/packages/core/src/app/Body.tsx @@ -0,0 +1,38 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { DialogsPortal } from '@dbeaver/core/dialogs'; +import { useStyles, useTheme } from '@dbeaver/core/theming'; + +import { Main } from './Main'; +import { Notifications } from './Notifications/Notifications'; +import { TopNavBar } from './TopNavBar/TopNavBar'; + +const bodyStyles = css` + container { + height: 100vh; + display: flex; + flex-direction: column; + } +`; + +export const Body = observer(function Body() { + useTheme(); + + return styled(useStyles(bodyStyles))( + + +
+ + + + ); +}); diff --git a/webapp/packages/core/src/app/Main.tsx b/webapp/packages/core/src/app/Main.tsx new file mode 100644 index 0000000000..4247210a43 --- /dev/null +++ b/webapp/packages/core/src/app/Main.tsx @@ -0,0 +1,60 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { + splitStyles, Split, ResizerControls, Pane, +} from '@dbeaver/core/blocks'; +import { composes, useStyles } from '@dbeaver/core/theming'; + +import { NavigationTree } from './NavigationTree'; +import { RightArea } from './RightArea'; + +const mainStyles = composes( + css` + space { + composes: theme-background-primary from global; + } + Pane { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + space { + composes: theme-typography--body2 from global; + } + Pane { + display: flex; + } + Pane:first-child { + position: relative; + flex: 0 1 auto; + } + Pane:last-child { + overflow: hidden; + } + ` +); + +export const Main = observer(function Main() { + return styled(useStyles(mainStyles, splitStyles))( + + + + + + + + + + + + ); +}); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx new file mode 100644 index 0000000000..988739c26d --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTree.tsx @@ -0,0 +1,62 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { useChildren } from '../shared/useChildren'; +import { NavigationTreeNode } from './NavigationTreeNode/NavigationTreeNode'; + +const navigationTreeStyles = css` + tree { + padding-top: 16px; + min-width: 240px; + width: 100%; + } + + center { + display: flex; + height: 100%; + width: 100%; + + & tree { + margin: auto; + text-align: center; + } + } + message { + box-sizing: border-box; + max-width: 240px; + padding: 24px; + } +`; + +export const NavigationTree = observer(function NavigationTree() { + const nodeChildren = useChildren(); + + if (!nodeChildren || nodeChildren.children.length === 0) { + return styled(navigationTreeStyles)( +
+ + + No connections.
+ Use the top menu to setup connection to your database. +
+
+
+ ); + } + + return styled(navigationTreeStyles)( + + {nodeChildren.children.map(id => ( + + ))} + + ); +}); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts new file mode 100644 index 0000000000..dc9acbc04d --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeContextMenuService.ts @@ -0,0 +1,55 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { + ContextMenuService, IContextMenuItem, IMenuContext, IMenuPanel, +} from '@dbeaver/core/dialogs'; + +import { NodesManagerService } from '../shared/NodesManager/NodesManagerService'; +import { NodeWithParent } from '../shared/NodesManager/NodeWithParent'; + +@injectable() +export class NavigationTreeContextMenuService { + static nodeContextType = 'NodeWithParent'; + private static menuToken = 'navTreeMenu'; + + constructor(private contextMenuService: ContextMenuService, + private nodesManagerService: NodesManagerService) { + } + + getMenuToken() { + return NavigationTreeContextMenuService.menuToken; + } + + constructMenuWithContext(node: NodeWithParent): IMenuPanel { + const context: IMenuContext = { + menuId: this.getMenuToken(), + contextId: node.id, + contextType: NavigationTreeContextMenuService.nodeContextType, + data: node, + }; + return this.contextMenuService.createContextMenu(context); + } + + registerMenuItems() { + const openNodeTab: IContextMenuItem = { + id: 'openNodeTab', + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType; + }, + order: 1, + title: 'Open', + onClick: (context: IMenuContext) => { + const node = context.data; + this.nodesManagerService.navToNode(node.id); + }, + }; + this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), openNodeTab); + } +} diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx new file mode 100644 index 0000000000..3d08c23164 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx @@ -0,0 +1,130 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { + MouseEvent, KeyboardEvent, useCallback, PropsWithChildren, +} from 'react'; +import styled, { use } from 'reshadow'; + +import { Icon, StaticImage, Loader } from '@dbeaver/core/blocks'; +import { useStyles } from '@dbeaver/core/theming'; + +import { navigationNodeStyles } from './navigationNodeStyles'; +import { Placeholder } from './Placeholder/Placeholder'; + +type NodeProps = PropsWithChildren<{ + id: string; + title?: string; + icon?: string; + isExpanded: boolean; + isLoaded?: boolean; + isSelected: boolean; + portal?: JSX.Element; + isExpandable?: boolean; + placeholder?: JSX.Element; + onExpand?: (id: string) => void; + onDoubleClick?: (id: string) => void; + onClick?: (id: string, isMultiple?: boolean) => void; +}> + +const KEYCODE = { + ENTER: 13, + UP: 38, + DOWN: 40, +}; + +export const NavigationNode = observer(function NavigationNode( + props: NodeProps +) { + const { + id, + title, + icon, + children, + isLoaded, + isExpanded, + isSelected, + portal, + isExpandable, + placeholder, + onExpand, + onDoubleClick, + onClick, + } = props; + + const styles = useStyles(navigationNodeStyles); + + const handleExpand = useCallback( + (e: MouseEvent) => { + e.stopPropagation(); + if (onExpand) { + onExpand(id); + } + }, + [id, onExpand] + ); + + const handleDoubleClick = useCallback( + (e: MouseEvent) => { + if (onDoubleClick) { + onDoubleClick(id); + } + }, + [id, onDoubleClick] + ); + + const handleClick = useCallback( + (e: MouseEvent) => { + if (onClick) { + onClick(id, e.ctrlKey); + } + }, + [id, onClick] + ); + + const handleEnter = useCallback( + (event: KeyboardEvent) => { + switch (event.keyCode) { + case KEYCODE.ENTER: + if (onClick) { + onClick(id, event.ctrlKey); + } + break; + } + return true; + }, + [id, onClick] + ); + + return styled(styles)( + <> + + + + + + + {title} + {portal} + + + {children || placeholder || } + + ); +}); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/Placeholder.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/Placeholder.tsx new file mode 100644 index 0000000000..347d289091 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/Placeholder.tsx @@ -0,0 +1,19 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import styled from 'reshadow'; + +import { placeholderStyles } from './placeholderStyles'; + +export function Placeholder({ className }: { className?: string }) { + return styled(placeholderStyles)( + +
+
+ ); +} diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/placeholderStyles.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/placeholderStyles.ts new file mode 100644 index 0000000000..1d99f72603 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/Placeholder/placeholderStyles.ts @@ -0,0 +1,41 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +export const placeholderStyles = css` + box { + position: relative; + overflow: hidden; + padding: 0 6px; + max-width: 200px; + } + + div { + box-sizing: border-box; + overflow: hidden; + position: relative; + height: 18px; + background: #dedede; + border-radius: 3px; + margin: 2px 0px; + animation: wave 1.5s linear infinite; + } + + @keyframes wave { + 0% { + opacity: 0; + } + 50% { + opacity: 1; + } + 100% { + opacity: 0; + } + } +`; diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts new file mode 100644 index 0000000000..40e3c793fd --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts @@ -0,0 +1,99 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +import { composes } from '@dbeaver/core/theming'; + +export const navigationNodeStyles = composes( + css` + control { + composes: theme-ripple theme-ripple-selectable from global; + } + `, + css` + node { + box-sizing: border-box; + width: fit-content; + min-width: 100%; + height: 20px; + + &[use|isExpanded] { + & + nested { + display: block; + } + + & arrow Icon { + height: 16px; + width: 16px; + transform: rotate(90deg); + } + } + + & control { + display: flex; + align-items: center; + padding: 0px 5px; + user-select: none; + white-space: nowrap; + height: inherit; + position: initial; + outline: none; + + &:hover > portal, + &[aria-selected] > portal { + visibility: visible; + } + + &::before { + left: 0; + top: auto; + height: inherit; + width: 100%; + } + } + } + + arrow { + cursor: pointer; + opacity: 0.5; + width: 16px; + height: 16px; + + &[hidden] + icon { + margin-left: 20px; + } + } + + icon { + margin-left: 4px; + width: 16px; + height: 16px; + + & StaticImage { + height: 100%; + } + } + + name { + margin-left: 4px; + } + + portal { + margin-left: auto; + margin-right: 16px; + visibility: hidden; + } + + nested { + padding: 2px 0; + padding-left: 16px; + display: none; + } + ` +); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren/NavigationNodeChildren.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren/NavigationNodeChildren.tsx new file mode 100644 index 0000000000..66f2e3ad8c --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationNodeChildren/NavigationNodeChildren.tsx @@ -0,0 +1,40 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { IReactComponent, observer } from 'mobx-react'; +import React, { memo, PropsWithChildren } from 'react'; + +import { useChildren } from '../../../shared/useChildren'; + +type NavigationNodeChildrenProps = PropsWithChildren<{ + parentId: string; + component: IReactComponent<{id: string}>; +}> + +export const NavigationNodeChildren = memo( + observer(function NavigationNodeChildren({ + parentId, + component, + }: NavigationNodeChildrenProps) { + const nodeChildren = useChildren(parentId); + + if (!nodeChildren?.isLoaded) { + return null; + } + + const NavigationNodeChild = component; + return ( + <> + {nodeChildren.children.map(id => ( + + ))} + + ); + }), + (prev, next) => prev.parentId === next.parentId +); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx new file mode 100644 index 0000000000..c1a625957a --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx @@ -0,0 +1,49 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { PropsWithChildren } from 'react'; + +import { NavigationNode } from './NavigationNode/NavigationNode'; +import { NavigationNodeChildren } from './NavigationNodeChildren/NavigationNodeChildren'; +import { TreeNodeMenu } from './TreeNodeMenu/TreeNodeMenu'; +import { useNavigationTree } from './useNavigationTree'; + +type NavigationTreeNodeProps = PropsWithChildren<{ + id: string; +}> + +export const NavigationTreeNode = observer(function NavigationTreeNodeFn({ + id, +}: NavigationTreeNodeProps) { + const node = useNavigationTree(id); + + if (!node) { + return null; + } + + return ( + } + > + + + ); +}); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx new file mode 100644 index 0000000000..d8cd3869f0 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx @@ -0,0 +1,55 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed } from 'mobx'; +import { observer } from 'mobx-react'; +import { useMemo } from 'react'; +import styled, { use } from 'reshadow'; + +import { Icon } from '@dbeaver/core/blocks'; +import { useService } from '@dbeaver/core/di'; +import { MenuTrigger } from '@dbeaver/core/dialogs'; + +import { useNode } from '../../../shared/NodesManager/useNode'; +import { NavigationTreeContextMenuService } from '../../NavigationTreeContextMenuService'; +import { treeNodeMenuStyles } from './treeNodeMenuStyles'; + + +type TreeNodeMenuProps = { + nodeId: string; + isSelected: boolean; +} + +export const TreeNodeMenu = observer(function TreeNodeMenu({ + nodeId, + isSelected, +}: TreeNodeMenuProps) { + + const navigationTreeContextMenuService = useService(NavigationTreeContextMenuService); + const node = useNode(nodeId)!; + + const menuPanel = useMemo( + () => navigationTreeContextMenuService.constructMenuWithContext(node), + [nodeId] + ); + const isHidden = useMemo( + () => computed(() => !menuPanel.menuItems.length + || menuPanel.menuItems.every(item => item.isHidden)), + [nodeId] + ); + + if (isHidden.get()) { + return null; + } + + return styled(treeNodeMenuStyles)( + + + + ); +}); diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts new file mode 100644 index 0000000000..4b2718cd83 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts @@ -0,0 +1,31 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +export const treeNodeMenuStyles = css` + MenuTrigger { + margin-left: 16px; + padding: 0; + height: 16px; + display: flex; + + & > Icon { + cursor: pointer; + width: 16px; + height: 10px; + fill: #dedede; + } + + &[use|isSelected] > Icon, + &:hover > Icon, + &:focus > Icon { + fill: #338fcc; + } + } +`; diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts new file mode 100644 index 0000000000..12cae46442 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeNode/useNavigationTree.ts @@ -0,0 +1,90 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { useState, useCallback, useEffect } from 'react'; + +import { useService } from '@dbeaver/core/di'; + +import { EObjectFeature } from '../../shared/NodesManager/EObjectFeature'; +import { NodesManagerService } from '../../shared/NodesManager/NodesManagerService'; +import { NodeWithParent } from '../../shared/NodesManager/NodeWithParent'; +import { useNode } from '../../shared/NodesManager/useNode'; +import { useChildren } from '../../shared/useChildren'; +import { NavigationTreeService } from '../NavigationTreeService'; + + +export function useNavigationTree(nodeId: string) { + const navigationTreeService = useService(NavigationTreeService); + const nodesManagerService = useService(NodesManagerService); + const [isExpanded, switchExpand] = useState(false); + const [isSelected, switchSelect] = useState(false); + const node = useNode(nodeId); + const children = useChildren(nodeId); + + if (!node) { + return undefined; + } + + const isLoaded = children?.isLoaded; + const isExpandable = isExpandableFilter(node) && (!children?.isLoaded || children.children.length > 0); + + const handleDoubleClick = useCallback( + (nodeId: string) => nodesManagerService.navToNode(nodeId), + [nodesManagerService] + ); + + const handleExpand = useCallback( + (id: string) => { + if (!isExpanded) { + navigationTreeService.loadNestedNodes(id).then(state => !state && switchExpand(false)); + } + switchExpand(!isExpanded); + }, + [isExpanded] + ); + + const handleSelect = useCallback( + (id: string, isMultiple?: boolean) => { + switchSelect(navigationTreeService.selectNode(id, isMultiple)); + }, + [isSelected] + ); + + const { + name, nodeType, icon, hasChildren, + } = node; + + useEffect(() => { + if (!isExpandable || !hasChildren) { + switchExpand(false); + } + }, [isExpandable && hasChildren]); + + // Here we subscribe to selected nodes if current node selected (mobx) + if (isSelected && !navigationTreeService.selectedNodes.includes(node.id)) { + switchSelect(false); + } + + return { + name, + nodeType, + icon, + isExpanded, + isLoaded, + isExpandable, + isSelected, + hasChildren, + handleDoubleClick, + handleSelect, + handleExpand, + }; +} + +export function isExpandableFilter(node: NodeWithParent) { + return !node.object?.features?.find(feature => feature === EObjectFeature.entity); +} diff --git a/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts b/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts new file mode 100644 index 0000000000..b5563effe6 --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/NavigationTreeService.ts @@ -0,0 +1,48 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observable } from 'mobx'; + +import { injectable } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; + +import { NodesManagerService } from '../shared/NodesManager/NodesManagerService'; + +@injectable() +export class NavigationTreeService { + + selectedNodes = observable.array([]); + + constructor(private nodesManagerService: NodesManagerService, + private notificationService: NotificationService) { + } + + async loadNestedNodes(id = '/') { + try { + const children = await this.nodesManagerService.loadChildren(id); + if (children) { + return true; + } + } catch (exception) { + this.notificationService.logException(exception, `Can't load tree node: ${id}`); + } + return false; + } + + selectNode(id: string, isMultiple?: boolean) { + if (!isMultiple) { + this.selectedNodes.clear(); + } + if (!this.selectedNodes.includes(id)) { + this.selectedNodes.push(id); + return true; + } + this.selectedNodes.remove(id); + return false; + } +} diff --git a/webapp/packages/core/src/app/NavigationTree/index.ts b/webapp/packages/core/src/app/NavigationTree/index.ts new file mode 100644 index 0000000000..792799005f --- /dev/null +++ b/webapp/packages/core/src/app/NavigationTree/index.ts @@ -0,0 +1 @@ +export * from './NavigationTree'; diff --git a/webapp/packages/core/src/app/Notifications/Notifications.tsx b/webapp/packages/core/src/app/Notifications/Notifications.tsx new file mode 100644 index 0000000000..d3cf1ce822 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/Notifications.tsx @@ -0,0 +1,41 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { Portal } from 'reakit'; +import styled, { css } from 'reshadow'; + +import { useService } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; +import { useStyles } from '@dbeaver/core/theming'; + +import { NotificationsItem } from './NotificationsItem/NotificationsItem'; + +const styles = css` + notifications { + composes: theme-typography from global; + position: absolute; + bottom:0; + left: 0; + z-index: 1000; /* modal dialogs is 999, but more correct way is place notifications after dialogs in the dom */ + } + `; + +export const Notifications = observer(function Notifications() { + const notificationService = useService(NotificationService); + + return styled(useStyles(styles))( + + + {notificationService.notifications.map(notification => ( + + ))} + + + ); +}); diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx new file mode 100644 index 0000000000..e3bcbf8d4c --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog.tsx @@ -0,0 +1,82 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { useCallback, useMemo } from 'react'; +import styled from 'reshadow'; + +import { useClipboard, Button, SanitizedHTML } from '@dbeaver/core/blocks'; +import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@dbeaver/core/dialogs'; +import { useLocale } from '@dbeaver/core/localization'; +import { useStyles } from '@dbeaver/core/theming'; + +import { ErrorModel, IErrorInfo } from './ErrorModel'; +import { styles } from './styles'; + + +function DisplayErrorInfo({ error }: {error: IErrorInfo}) { + return styled(useStyles(styles))( + <> + + + {error.message} + + + {error.stackTrace && ( + + + + )} + + ); +} + +export const ErrorDetailsDialog: DialogComponent = observer( + function ErrorDetailsDialog(props: DialogComponentProps) { + const error = useMemo( + () => (props.payload instanceof Error + ? new ErrorModel({ error: props.payload }) + : new ErrorModel({ reason: props.payload })), + [props.payload] + ); + + const title = useLocale('core_eventsLog_dbeaverErrorDetails'); + + const [copy] = useClipboard(); + const copyHandler = useCallback( + () => copy(error.textToCopy), + [] + ); + + return styled(useStyles(styles))( + + + {error.textToCopy && ( + + )} + + )} + onReject={props.rejectDialog} + > + {error.reason && {error.reason}} + {error.htmlBody && ()} + {error.errors.map( + (error, id) => ( + <> + {id > 0 &&
} + + + ) + )} +
+ ); + } +); diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorModel.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorModel.ts new file mode 100644 index 0000000000..9fdf54075d --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/ErrorModel.ts @@ -0,0 +1,76 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { GQLError, ServerInternalError } from '@dbeaver/core/sdk'; + +export interface IErrorInfo { + message: string; + stackTrace: string; +} + +export interface IErrorModelOptions { + reason?: string; + error?: Error; +} + +export class ErrorModel { + + reason: string; + errors: IErrorInfo[] = []; + textToCopy = ''; + htmlBody = ''; + + constructor({ reason, error }: IErrorModelOptions) { + this.reason = reason || ''; + // text error + if (!error) { + this.textToCopy = this.reason; + } + // GQL Error + else if (error instanceof GQLError) { + this.errors = (error.response?.errors || []) + .map((error) => { + const errorInfo: IErrorInfo = { + message: error.message, + stackTrace: error.extensions?.stackTrace || '', + }; + return errorInfo; + }); + + this.textToCopy = error.isTextBody + ? error.errorText + : this.textToCopy = this.errors + .map(error => `${error.message}\n${error.stackTrace}`) + .join('------------------\n'); + + if (error.isTextBody) { + this.htmlBody = error.errorText; + } + + } + else if (error instanceof ServerInternalError) { + this.errors = [ + { + message: error.message, + stackTrace: error.stackTrace || '', + }, + ]; + this.textToCopy = `${error.message}\n${error.stackTrace}`; + } + // Common Error + else if (error instanceof Error) { + this.errors = [ + { + message: error.message, + stackTrace: error.stack || '', + }, + ]; + this.textToCopy = `${error.message}\n${error.stack}`; + } + } +} diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/styles.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/styles.ts new file mode 100644 index 0000000000..5ecac4a409 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/ErrorDetailsDialog/styles.ts @@ -0,0 +1,39 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +export const styles = css` + code, message { + display: block; + white-space: pre-wrap; + } + controls { + display: flex; + height: 100%; + align-items: center; + flex-direction: row-reverse; + } + Button { + margin-left: 24px; + } + textarea { + width: 100% !important; + min-height: 270px; + } + message { + overflow: auto; + max-height: 96px; + } + property { + padding: 8px 16px; + } + error-details { + border: solid 1px; + } +`; diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts new file mode 100644 index 0000000000..919b488d79 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationItemController.ts @@ -0,0 +1,57 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observable } from 'mobx'; + +import { IInitializableController, injectable } from '@dbeaver/core/di'; +import { CommonDialogService } from '@dbeaver/core/dialogs'; +import { ENotificationType, INotification } from '@dbeaver/core/eventsLog'; + +import { ErrorDetailsDialog } from './ErrorDetailsDialog/ErrorDetailsDialog'; + +const FadeTimeout = 5000; + +@injectable() +export class NotificationItemController implements IInitializableController { + closeAfter = 0; + @observable isDetailsDialogOpen = false; + private notification!: INotification; + + constructor(private commonDialogService: CommonDialogService) {} + + init(notification: INotification) { + this.notification = notification; + + if (this.notification.details) { + this.handleShowDetails = this.showDetails.bind(this); + } + + if (notification.type !== ENotificationType.Error) { + this.closeAfter = FadeTimeout; + } + } + + handleShowDetails?: () => void; + + handleClose = () => { + if (!this.notification) { + return; + } + this.notification.close(); + } + + private async showDetails() { + this.isDetailsDialogOpen = true; + try { + this.notification.showDetails(); + await this.commonDialogService.open(ErrorDetailsDialog, this.notification.details!); + } finally { + this.isDetailsDialogOpen = false; + } + } +} diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx new file mode 100644 index 0000000000..7c7750f108 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/NotificationsItem.tsx @@ -0,0 +1,37 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; + +import { useController } from '@dbeaver/core/di'; +import { INotification } from '@dbeaver/core/eventsLog'; + +import { NotificationItemController } from './NotificationItemController'; +import { Snackbar } from './Snackbar/Snackbar'; + +type NotificationProps = { + notification: INotification; +} + +export const NotificationsItem = observer(function Notification({ notification }: NotificationProps) { + if (!notification) { + return null; + } + const controller = useController(NotificationItemController, notification); + + return ( + + ); +}); diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/NotificationMark.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/NotificationMark.tsx new file mode 100644 index 0000000000..3fb9e8fd2a --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/NotificationMark.tsx @@ -0,0 +1,25 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { HTMLProps } from 'react'; + +import { CheckCircle, Frown } from '@dbeaver/core/blocks'; +import { ENotificationType } from '@dbeaver/core/eventsLog'; + +type IconProps = HTMLProps & { + type: ENotificationType; + } + +export function NotificationMark({ type, ...props }: IconProps) { + return ( +
+ {type === ENotificationType.Info && } + {type === ENotificationType.Error && } +
+ ); +} diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx new file mode 100644 index 0000000000..af4c374659 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/Snackbar.tsx @@ -0,0 +1,93 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { useEffect, useState } from 'react'; +import { Button as ReakitButton } from 'reakit/Button'; +import styled, { use } from 'reshadow'; + +import { Icon, Button } from '@dbeaver/core/blocks'; +import { ENotificationType } from '@dbeaver/core/eventsLog'; +import { useStyles } from '@dbeaver/core/theming'; + +import { NotificationMark } from './NotificationMark'; +import { snackbarStyles } from './styles'; + +type SnackbarProps = { + text: string; + closeAfter?: number; + type?: ENotificationType; + disableShowDetails?: boolean; + onClose?: () => void; + onShowDetails?: () => void; +} + +export const Snackbar = observer(function Snackbar(props: SnackbarProps) { + const styles = useStyles(snackbarStyles); + const [mounted, setMounted] = useState(false); + const [closing, setClosing] = useState(false); + const { + closeAfter, onClose, onShowDetails, type, text, + } = props; + + useEffect(() => { + setMounted(true); + let timeOutId: any; + let timeOutClosingId: any; + + if (closeAfter) { + timeOutId = setTimeout(() => { + setClosing(true); + + if (onClose) { + timeOutClosingId = setTimeout(onClose, 1000); + } + }, closeAfter); + } + return () => { + if (timeOutId) { clearTimeout(timeOutId); } + if (timeOutClosingId) { clearTimeout(timeOutClosingId); } + }; + }, []); + + return styled(styles)( + + + {type && } + + {text} + + {!closeAfter && onClose && ( + + + + )} + + + {onShowDetails && ( + + + + )} + + + ); +}); diff --git a/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts new file mode 100644 index 0000000000..1ddf27de36 --- /dev/null +++ b/webapp/packages/core/src/app/Notifications/NotificationsItem/Snackbar/styles.ts @@ -0,0 +1,109 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +import { composes } from '@dbeaver/core/theming'; + +export const snackbarStyles = composes( + css` + notification { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + notification { + composes: theme-elevation-z5 from global; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; + overflow: hidden; + width: 400px; + margin-bottom: 16px; + margin-left: 16px; + padding: 18px 24px; + line-height: 1.5; + opacity: 0; + border-radius: 4px; + transition: opacity 0.3s ease-in-out, transform 0.5s ease-in-out; + transform: translateX(-100%); + + &[use|mounted] { + transform: translateX(0); + opacity: 1; + } + &[use|closing] { + opacity: 0; + } + } + + notification-header { + display: flex; + } + + ReakitButton { + color: rgba(0, 0, 0, 0.45); + outline: none; + border: none; + background: none; + cursor: pointer; + padding: 0; + margin: 4px; + height: 16px; + + & Icon { + width: 16px; + height: 16px; + } + } + + message { + flex: 1; + overflow: auto; + max-height: 200px; + padding-right: 24px; + word-break: break-word; + } + + NotificationMark { + display: block; + box-sizing: border-box; + overflow: hidden; + width: 24px; + height: 24px; + font-size: 24px; + font-style: normal; + line-height: 0; + text-align: center; + text-transform: none; + vertical-align: -0.125em; + text-rendering: optimizeLegibility; + + &[type='Info'] :global(svg) { + fill: #52c41a !important; + } + & :global(svg) { + fill: #ed3b26 !important; + } + } + + NotificationMark + message { + padding-left: 24px; + } + + actions { + margin-top: 24px; + + & Button { + margin-left: auto; + display: block; + } + } + ` +); diff --git a/webapp/packages/core/src/app/RightArea.tsx b/webapp/packages/core/src/app/RightArea.tsx new file mode 100644 index 0000000000..cc4ffc5556 --- /dev/null +++ b/webapp/packages/core/src/app/RightArea.tsx @@ -0,0 +1,55 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { + Pane, ResizerControls, Split, splitHorizontalStyles, splitStyles, +} from '@dbeaver/core/blocks'; +import { composes, useStyles } from '@dbeaver/core/theming'; + +import { useController } from '../di'; +import { NavigationTabsBar } from './shared/NavigationTabs/NavigationTabsBar'; +import { LogViewTab } from './shared/ToolsPanel/LogViewTab/LogViewTab'; +import { LogViewTabController } from './shared/ToolsPanel/LogViewTab/LogViewTabController'; + +const styles = composes( + css` + Pane { + composes: theme-background-surface theme-text-on-surface from global; + } + `, + css` + Pane { + flex: 1; + display: flex; + overflow: auto; + } + Pane:last-child { + flex: 0 0 30%; + } + ` +); + + +export const RightArea = observer(function RightArea() { + const controller = useController(LogViewTabController); + + return styled(useStyles(styles, splitStyles, splitHorizontalStyles))( + + + + + {controller.isActive && } + + + + + ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts new file mode 100644 index 0000000000..8299f8f372 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService.ts @@ -0,0 +1,236 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed } from 'mobx'; + +import { injectable } from '@dbeaver/core/di'; +import { NotificationService } from '@dbeaver/core/eventsLog'; + +import { + Connection, + ConnectionsManagerService, + ISchema, +} from '../../shared/ConnectionsManager/ConnectionsManagerService'; +import { NavigationTabsService } from '../../shared/NavigationTabs/NavigationTabsService'; +import { NodeManagerUtils } from '../../shared/NodesManager/NodeManagerUtils'; +import { NodesManagerService } from '../../shared/NodesManager/NodesManagerService'; +import { ConnectionSchemaStore, IConnectionWithIcon } from './ConnectionSchemaStore'; +import { IConnectionCatalogSchema, ITabHasConnectionChangeBehavior } from './IConnectionCatalogSchema'; + +@injectable() +export class ConnectionSchemaManagerService { + + get currentConnection() { + return this.currentTabState + ? this.store.getConnectionById(this.currentTabState.connectionId) : null; + } + + get schemaList(): ISchema[] { + return this.store.schemaList; + } + get connectionsList() { + return this.store.connectionsList; + } + get catalogsList() { + return this.store.catalogsList; + } + + @computed get connectionSelectionDisabled(): boolean { + return !this.isTabStateChangeable + || this.connectionsList.length === 0; + } + + @computed get isTabStateChangeable(): boolean { + return Boolean(this.currentTabState && this.currentTabState.changeSchema); + } + + @computed get isSelectorVisible(): boolean { + return Boolean(this.currentTabState); + } + + @computed get currentTabState(): ITabHasConnectionChangeBehavior | null { + const tab = this.navigationTabsService.getTab(this.navigationTabsService.currentTabId); + if (!tab) { + return null; + } + // todo everything here should be refactored and moved to a specific tab strategy + const sqlHandlerState = tab.getHandlerState('sql_editor'); + if (sqlHandlerState) { + return sqlHandlerState; + } + + const isObjectViewer = tab.hasHandler('object_viewer_properties') || tab.hasHandler('data_viewer_data'); + if (isObjectViewer) { + const connectionAndSchema = this.nodesManagerService.getConnectionCatalogSchema(tab.nodeId); + if (connectionAndSchema.connectionId) { + // connection node id differs from connection id + const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(connectionAndSchema.connectionId); + return { + connectionId, + schemaId: connectionAndSchema.schemaId || null, + catalogId: connectionAndSchema.catalogId || null, + }; + } + } + return null; + } + + private store = new ConnectionSchemaStore(); + + constructor(private navigationTabsService: NavigationTabsService, + private nodesManagerService: NodesManagerService, + private connectionsManagerService: ConnectionsManagerService, + private notificationService: NotificationService) { + + this.connectionsManagerService.connections.forEach((connection) => { + this.addConnectionToList(connection); + }); + + this.connectionsManagerService.onOpenConnection + .subscribe((connection) => { + this.addConnectionToList(connection); + }); + + this.connectionsManagerService.onCloseConnection + .subscribe((connectionId) => { + this.removeConnectionFromList(connectionId); + }); + + this.navigationTabsService.onTabActivate + .subscribe(tab => this.onTabActivate()); + this.onTabActivate(); + } + + /** + * Trigger when user select connection in dropdown + */ + onSelectConnection(connection: IConnectionWithIcon) { + if (!this.currentTabState) { + return; + } + if (this.currentTabState.connectionId === connection.id) { + return; + } + this.changeTabConnection({ + connectionId: connection.id, + catalogId: null, + schemaId: null, + }); + } + + /** + * Trigger when user select schema in dropdown + */ + onSelectSchema(schemaId: string) { + if (!this.currentConnection) { + throw new Error('The try to change schema without connection'); + } + if (this.currentTabState?.schemaId === schemaId) { + return; + } + + const params = { + connectionId: this.currentConnection.id, + catalogId: this.currentTabState?.catalogId || null, + schemaId, + }; + this.changeTabConnection(params); + } + + /** + * Trigger when user select catalog in dropdown + */ + onSelectCatalog(catalogId: string) { + if (!this.currentConnection) { + throw new Error('The try to change schema without connection'); + } + if (this.currentTabState?.catalogId === catalogId) { + return; + } + + const params = { + connectionId: this.currentConnection.id, + catalogId, + schemaId: null, + }; + this.changeTabConnection(params); + } + + private onTabActivate() { + if (this.currentTabState && this.isTabStateChangeable) { + this.refreshSchemasList(this.currentTabState.connectionId, this.currentTabState.catalogId); + } + } + + private async changeTabConnection(connectionCatalogSchema: IConnectionCatalogSchema): Promise { + + if (!this.currentTabState || !this.currentTabState.changeSchema) { + return; + } + + const currentConnectionId = this.currentTabState.connectionId; + const currentCatalogId = this.currentTabState.catalogId; + + const res = await this.currentTabState.changeSchema(connectionCatalogSchema); + + if (currentConnectionId !== res.connectionId || currentCatalogId !== res.catalogId) { + this.refreshSchemasList(res.connectionId, res.catalogId); + } + + } + + private addConnectionToList(connection: Connection) { + this.store.addConnection({ + ...connection, + icon: '', + }); + this.loadIcon(connection); + } + + private async loadIcon(connection: Connection): Promise { + try { + const res = await this.connectionsManagerService.loadConnectionDriver(connection.driverId); + this.store.updateIcon(connection.id, res?.icon); + } catch (exception) { + this.notificationService.logException(exception, `Can't load icon for driver: ${connection.driverId}`); + } + } + + private removeConnectionFromList(connectionId: string) { + this.store.removeConnection(connectionId); + } + + private async refreshSchemasList(connectionId: string, catalogId: string | null) { + if (this.store.listsParams.connectionId === connectionId + && this.store.listsParams.catalogId === catalogId) { + return; + } + // hide non-actual data while loading + this.store.setSchemasAndCatalogs( + { + connectionId: null, + catalogId: null, + }, + { + catalogs: [], + schemas: [], + } + ); + + const catalogAndSchemas = await this.connectionsManagerService + .getCatalogsAndSchemas(connectionId, catalogId || undefined); + + this.store.setSchemasAndCatalogs( + { + connectionId, + catalogId: catalogId || null, + }, + catalogAndSchemas + ); + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts new file mode 100644 index 0000000000..1d0ca1cb70 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSchemaStore.ts @@ -0,0 +1,77 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { action } from 'mobx'; + +import { OrderedMap } from '@dbeaver/core/utils'; + +import { Connection, ICatalogsAndSchemas, ISchema } from '../../shared/ConnectionsManager/ConnectionsManagerService'; + +export interface IConnectionWithIcon extends Connection { + icon?: string; +} + +interface IListsParams { + connectionId: string | null; + catalogId: string | null; +} + +export class ConnectionSchemaStore { + + private connectionsMap = new OrderedMap(c => c.id); + private schemaMap = new OrderedMap(s => s.id); + private catalogsMap = new OrderedMap(s => s.id); + + listsParams: IListsParams = { + connectionId: null, + catalogId: null, + }; + + get schemaList(): ISchema[] { + return this.schemaMap.values; + } + + get connectionsList() { + return this.connectionsMap.values; + } + + get catalogsList() { + return this.catalogsMap.values; + } + + getConnectionById(connectionId: string | null): IConnectionWithIcon | null { + if (connectionId == null) { + return null; + } + return this.connectionsMap.get(connectionId) || null; + } + + @action updateIcon(connectionId: string, icon?: string) { + const connection = this.getConnectionById(connectionId); + if (connection) { + connection.icon = icon; + } + } + + @action + setSchemasAndCatalogs(params: IListsParams, catalogsAndSchemas: ICatalogsAndSchemas): void { + this.listsParams = params; + this.schemaMap.bulkRewrite(catalogsAndSchemas.schemas); + this.catalogsMap.bulkRewrite(catalogsAndSchemas.catalogs); + } + + @action + addConnection(connection: IConnectionWithIcon) { + this.connectionsMap.addValue(connection); + } + + @action + removeConnection(connectionId: string) { + this.connectionsMap.remove(connectionId); + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx new file mode 100644 index 0000000000..6fab54d2d6 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelector.tsx @@ -0,0 +1,62 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css, use } from 'reshadow'; + +import { useController } from '@dbeaver/core/di'; +import { useStyles } from '@dbeaver/core/theming'; + +import { TopMenuItem } from '../../shared/TopMenuItem'; +import { ConnectionSelectorController } from './ConnectionSelectorController'; + +const menuStyles = css` + Menu { + max-height: 400px; + overflow: auto; + } +`; + +const connectionMenu = css` + menu-trigger-icon { + background-color: #fff; + border-radius: 4px; + padding: 1px; + + & IconOrImage { + width: 22px; + } + } +`; + +const style = css` + connection-selector { + display: flex; + height: 100%; + visibility: hidden; + background: #338ecc; + + &[use|isVisible] { + visibility: visible; + } + } +`; +export const ConnectionSelector = observer(function ConnectionSelector() { + const controller = useController(ConnectionSelectorController); + const ConnectionMenu = TopMenuItem; + const SchemaOrCatalogMenu = TopMenuItem; + + return styled(useStyles(style))( + + + {controller.isSchemaSelectorVisible && ( + + )} + + ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts new file mode 100644 index 0000000000..c7fadf3a3a --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/ConnectionSelector/ConnectionSelectorController.ts @@ -0,0 +1,116 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed } from 'mobx'; + +import { ISchema } from '@dbeaver/core/app'; +import { injectable } from '@dbeaver/core/di'; +import { ComputedMenuItemModel, ComputedMenuPanelModel, IMenuItem } from '@dbeaver/core/dialogs'; + +import { ConnectionSchemaManagerService } from '../ConnectionSchemaManagerService'; + +@injectable() +export class ConnectionSelectorController { + + connectionMenu: IMenuItem; + schemaOrCatalogMenu: IMenuItem; + + get isSelectorVisible() { + return this.connectionSelectorService.isSelectorVisible; + } + get isSchemaSelectorVisible() { + return this.connectionSelectorService.isSelectorVisible && Boolean(this.currentSchemaTitle); + } + + @computed get schemaSelectionDisabled(): boolean { + return !this.connectionSelectorService.isTabStateChangeable + || this.getSchemaOrCatalogItems().length === 0; + } + + @computed private get currentSchemaTitle(): string { + const currentTabState = this.connectionSelectorService.currentTabState; + if (!currentTabState) { + return ''; + } + if (currentTabState.schemaId && currentTabState.catalogId) { + return `${currentTabState.schemaId}@${currentTabState.catalogId}`; + } + return currentTabState.schemaId || currentTabState.catalogId || ''; + } + + @computed private get currentSchemaIcon(): string { + const currentTabState = this.connectionSelectorService.currentTabState; + if (currentTabState && currentTabState.schemaId) { + // TODO move such kind of icon paths to a set of constants + return 'schema_system'; + } + if (currentTabState && currentTabState.catalogId) { + return 'database'; + } + return ''; + } + + constructor(private connectionSelectorService: ConnectionSchemaManagerService) { + + this.connectionMenu = new ComputedMenuItemModel({ + id: 'connectionsDropdown', + isDisabled: () => this.connectionSelectorService.connectionSelectionDisabled, + titleGetter: () => this.connectionSelectorService?.currentConnection?.name, + iconGetter: () => this.connectionSelectorService?.currentConnection?.icon, + panel: new ComputedMenuPanelModel({ + id: 'connectionsDropdownPanel', + menuItemsGetter: () => this.getConnectionItems(), + }), + }); + + this.schemaOrCatalogMenu = new ComputedMenuItemModel({ + id: 'connectionsDropdown', + isDisabled: () => this.schemaSelectionDisabled, + titleGetter: () => this.currentSchemaTitle, + iconGetter: () => this.currentSchemaIcon, + panel: new ComputedMenuPanelModel({ + id: 'connectionsDropdownPanel', + menuItemsGetter: () => this.getSchemaOrCatalogItems(), + }), + }); + } + + private getConnectionItems(): IMenuItem[] { + return this.connectionSelectorService.connectionsList.map((item) => { + const menuItem: IMenuItem = { + id: item.id, + title: item.name || item.id, + onClick: () => this.connectionSelectorService.onSelectConnection(item), + }; + return menuItem; + }); + } + + private getSchemaOrCatalogItems(): IMenuItem[] { + return this.connectionSelectorService.schemaList.length + ? this.listToMenuItems(this.connectionSelectorService.schemaList, true) + : this.listToMenuItems(this.connectionSelectorService.catalogsList, false); + } + + private listToMenuItems(list: ISchema[], isSchema: boolean): IMenuItem[] { + return list.map((item) => { + const menuItem: IMenuItem = { + id: item.id, + title: item.id, + onClick: () => { + if (isSchema) { + this.connectionSelectorService.onSelectSchema(item.id); + } else { + this.connectionSelectorService.onSelectCatalog(item.id); + } + }, + }; + return menuItem; + }); + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts new file mode 100644 index 0000000000..0aaf3ffb7d --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema.ts @@ -0,0 +1,19 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + + +export interface IConnectionCatalogSchema { + connectionId: string; + schemaId: string | null; // some dbs has no schema + catalogId: string | null; // some dbs has no catalog +} + +export interface ITabHasConnectionChangeBehavior extends IConnectionCatalogSchema { + // todo remove optional sign whem we can destinguish ITabHasConnectionBehavior and ITabHasConnectionBehavior + changeSchema?: (connectionCatalogSchema: IConnectionCatalogSchema) => Promise; +} diff --git a/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql new file mode 100644 index 0000000000..2efac2e811 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/ConnectionSchemaManager/queries/navGetStructContainers.gql @@ -0,0 +1,14 @@ +query navGetStructContainers($connectionId: ID!, $catalogId: ID) { + navGetStructContainers(connectionId: $connectionId, catalog: $catalogId) { + catalogList { + name + description + type + } + schemaList { + name + description + type + } + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenu.tsx b/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenu.tsx new file mode 100644 index 0000000000..ed19176da0 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenu.tsx @@ -0,0 +1,39 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled, { css } from 'reshadow'; + +import { useService } from '@dbeaver/core/di'; + +import { TopMenuItem } from '../shared/TopMenuItem'; +import { MainMenuService } from './MainMenuService'; + +const styles = css` + menu-wrapper { + display: flex; + height: 100%; + background: #338ecc; + } + TopMenuItem { + text-transform: uppercase; + font-weight: 700; + } +`; + +export const MainMenu = observer(function MainMenu() { + const mainMenuService = useService(MainMenuService); + + return styled(styles)( + + {mainMenuService.getMainMenu().map((topItem, i) => ( + + ))} + + ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenuService.ts b/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenuService.ts new file mode 100644 index 0000000000..de02cd027d --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/MainMenu/MainMenuService.ts @@ -0,0 +1,58 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { IComputedMenuItemOptions, StaticMenu } from '@dbeaver/core/dialogs'; + +export enum EMainMenu { + 'mainMenuConnectionsPanel' = 'mainMenuConnectionsPanel', + 'mainMenuToolsPanel' = 'mainMenuToolsPanel', +} + +@injectable() +export class MainMenuService { + + private menuOptions = new StaticMenu(); + private mainMenuToken = 'mainMenu'; + + constructor() { + this.menuOptions.addRootPanel(this.mainMenuToken); + + const connectionMenu: IComputedMenuItemOptions = { + id: EMainMenu.mainMenuConnectionsPanel, + order: 1, + title: 'app_shared_connectionMenu_connection', + isPanel: true, + }; + this.registerRootItem(connectionMenu); + + const toolsMenu: IComputedMenuItemOptions = { + id: EMainMenu.mainMenuToolsPanel, + order: 3, + title: 'app_shared_toolsMenu_tools', + isPanel: true, + }; + this.registerRootItem(toolsMenu); + } + + getMainMenu() { + return this.menuOptions.getMenu(this.mainMenuToken).menuItems; + } + + isEmptyMenuPanel(token: string): boolean { + return this.menuOptions.getMenu(token).menuItems.length === 0; + } + + registerMenuItem(panelId: string, options: IComputedMenuItemOptions): void { + this.menuOptions.addMenuItem(panelId, options); + } + + registerRootItem(menuItem: IComputedMenuItemOptions): void { + this.registerMenuItem(this.mainMenuToken, menuItem); + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenu.tsx b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenu.tsx new file mode 100644 index 0000000000..f3cfaf4c38 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenu.tsx @@ -0,0 +1,29 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled from 'reshadow'; + +import { Icon } from '@dbeaver/core/blocks'; +import { useService } from '@dbeaver/core/di'; +import { MenuTrigger } from '@dbeaver/core/dialogs'; +import { useStyles } from '@dbeaver/core/theming'; + +import { topMenuStyles } from '../shared/topMenuStyles'; +import { SettingsMenuService } from './SettingsMenuService'; +import { settingsMenuStyles } from './settingsMenuStyles'; + +export const SettingsMenu = observer(function SettingsMenu() { + const settingsMenuService = useService(SettingsMenuService); + + return styled(useStyles(settingsMenuStyles))( + + + + ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenuService.ts b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenuService.ts new file mode 100644 index 0000000000..26749f8109 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/SettingsMenuService.ts @@ -0,0 +1,80 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { IComputedMenuItemOptions, StaticMenu } from '@dbeaver/core/dialogs'; +import { LocalizationService } from '@dbeaver/core/localization'; +import { ThemeService } from '@dbeaver/core/theming'; + + +@injectable() +export class SettingsMenuService { + settingsMenuToken = 'settingsMenu'; + + private menu = new StaticMenu(); + private langMenuToken = 'langMenu'; + private themeMenuToken = 'themeMenu'; + + constructor(private localizationService: LocalizationService, + private themeService: ThemeService) { + + this.menu.addRootPanel(this.settingsMenuToken); + this.addThemes(); + this.addLocales(); + } + + getMenu() { + return this.menu.getMenu(this.settingsMenuToken); + } + + addMenuItem(panelId: string, options: IComputedMenuItemOptions) { + this.menu.addMenuItem(panelId, options); + } + + private addThemes() { + this.addMenuItem(this.settingsMenuToken, { + id: this.themeMenuToken, + order: 1, + title: 'app_shared_settingsMenu_theme', + isPanel: true, + }); + + this.themeService.themes.forEach((theme) => { + this.addMenuItem( + this.themeMenuToken, + { + id: theme.id, + title: theme.name, + isDisabled: () => theme.id === this.themeService.currentThemeId, + onClick: () => this.themeService.changeThemeAsync(theme.id), + } + ); + }); + } + + private addLocales() { + this.addMenuItem(this.settingsMenuToken, { + id: this.langMenuToken, + order: 2, + title: 'app_shared_settingsMenu_lang', + isPanel: true, + }); + + this.localizationService.getSupportedLanguages().forEach((lang) => { + this.addMenuItem( + this.langMenuToken, + { + id: lang.isoCode, + title: lang.nativeName, + isDisabled: () => lang.isoCode === this.localizationService.getCurrentLanguage(), + onClick: () => this.localizationService.changeLocaleAsync(lang.isoCode), + } + ); + }); + } +} diff --git a/webapp/packages/core/src/app/TopNavBar/SettingsMenu/settingsMenuStyles.ts b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/settingsMenuStyles.ts new file mode 100644 index 0000000000..1bc41b2d7a --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/SettingsMenu/settingsMenuStyles.ts @@ -0,0 +1,36 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +export const settingsMenuStyles = css` + MenuTrigger { + padding: 0 16px !important; + margin-left: auto; + height: 48px; + cursor: pointer; + background: none; + border: none; + text-transform: uppercase; + font-weight: 700; + outline: none !important; + } + Icon { + height: 24px; + } + menu-panel-item { + flex-direction: row-reverse; + + & menu-item-text { + text-align: right; + } + & Icon { + transform: rotate(-180deg); + } + } +`; diff --git a/webapp/packages/core/src/app/TopNavBar/TopNavBar.tsx b/webapp/packages/core/src/app/TopNavBar/TopNavBar.tsx new file mode 100644 index 0000000000..117f5c6edc --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/TopNavBar.tsx @@ -0,0 +1,39 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import styled from 'reshadow'; + +import { Icon } from '@dbeaver/core/blocks'; +import { useService } from '@dbeaver/core/di'; +import { SessionService } from '@dbeaver/core/root'; +import { useStyles } from '@dbeaver/core/theming'; + +import { ConnectionSelector } from './ConnectionSchemaManager/ConnectionSelector/ConnectionSelector'; +import { MainMenu } from './MainMenu/MainMenu'; +import { SettingsMenu } from './SettingsMenu/SettingsMenu'; +import { topNavBarStyles } from './topNavBarStyles'; + + +declare const version: string; // declared in webpack DefinePlugin // todo move to enviroment? + +export const TopNavBar = observer(function TopNavBar() { + const sessionService = useService(SessionService); + const title = `Frontend: ${version}\nBackend: ${sessionService.version}`; + + return styled(useStyles(topNavBarStyles))( +
+ + + + + + +
+ ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/shared/TopMenuItem.tsx b/webapp/packages/core/src/app/TopNavBar/shared/TopMenuItem.tsx new file mode 100644 index 0000000000..fcaf0f9678 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/shared/TopMenuItem.tsx @@ -0,0 +1,85 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { ButtonHTMLAttributes } from 'react'; +import { Button } from 'reakit'; +import styled, { css } from 'reshadow'; + +import { Icon, IconOrImage } from '@dbeaver/core/blocks'; +import { IMenuItem, MenuTrigger } from '@dbeaver/core/dialogs'; +import { useTranslate } from '@dbeaver/core/localization'; +import { composes, useStyles, Style } from '@dbeaver/core/theming'; + +import { topMenuStyles } from './topMenuStyles'; + +type TopMenuItemProps = Omit, 'style'> & { + menuItem: IMenuItem; + style?: Style[]; +}; + +const buttonStyle = composes( + css` + Button { + composes: theme-ripple from global; + } + `, + css` + Button { + height: 100%; + padding: 0 16px !important; + text-transform: uppercase; + font-weight: 700; + + background: none; + border: none; + outline: none !important; + color: inherit; + cursor: pointer; + display: flex; + align-items: center; + + & > span > Icon { + height: 16px; + transform: rotate(90deg); + } + } + ` +); + +export const TopMenuItem = observer(function TopMenuItem({ menuItem, style = [], ...props }: TopMenuItemProps) { + const translate = useTranslate(); + + if (!menuItem.panel) { + return styled(useStyles(buttonStyle))( + + ); + } + + return styled(useStyles(...style, topMenuStyles))( + + {menuItem.icon && ( + + + + )} +
{translate(menuItem.title)}
+ +
+ ); +}); diff --git a/webapp/packages/core/src/app/TopNavBar/shared/topMenuStyles.ts b/webapp/packages/core/src/app/TopNavBar/shared/topMenuStyles.ts new file mode 100644 index 0000000000..2e25d7d8f7 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/shared/topMenuStyles.ts @@ -0,0 +1,64 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +import { composes } from '@dbeaver/core/theming'; + +export const topMenuStyles = composes( + css` + Menu { + composes: theme-text-on-primary from global; + } + `, + css` + Menu { + background-color: #338ecc!important; + } + MenuTrigger { + height: 100%; + padding: 0 16px !important; + display: flex; + align-items: center; + padding-right: 0!important; + + &:before { + display: none; + } + + & > div { + display: block; + } + & > Icon { + background: #47a0dd; + margin-left: 16px; + width: 16px; + height: 100%; + padding: 0 16px; + } + & IconOrImage { + display: block; + width: 24px; + } + & menu-trigger-icon { + margin-right: 8px; + } + &:hover > Icon { + background: #236ea0; + } + } + MenuItem { + &:hover { + background: #236ea0; + } + &:before { + display: none; + } + } + ` +); diff --git a/webapp/packages/core/src/app/TopNavBar/topNavBarStyles.ts b/webapp/packages/core/src/app/TopNavBar/topNavBarStyles.ts new file mode 100644 index 0000000000..55efffa1c2 --- /dev/null +++ b/webapp/packages/core/src/app/TopNavBar/topNavBarStyles.ts @@ -0,0 +1,41 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { css } from 'reshadow'; + +import { composes } from '@dbeaver/core/theming'; + +export const topNavBarStyles = composes( + css` + header { + composes: theme-background-primary theme-text-on-primary from global; + } + `, + css` + header { + composes: theme-typography--body2 from global; + display: flex; + align-items: center; + height: 48px; + padding: 0 16px; + z-index: 1; + } + logo { + height: 100%; + display: flex; + align-items: center; + margin-right: 16px; + width: 250px; + + & Icon { + height: 32px; + width: auto; + } + } + ` +); diff --git a/webapp/packages/core/src/app/index.ts b/webapp/packages/core/src/app/index.ts new file mode 100644 index 0000000000..ff551cbe27 --- /dev/null +++ b/webapp/packages/core/src/app/index.ts @@ -0,0 +1,46 @@ +// Services +export * from './TopNavBar/MainMenu/MainMenuService'; +export * from './TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService'; +export * from './TopNavBar/SettingsMenu/SettingsMenuService'; +export * from './shared/ConnectionsManager/ConnectionsManagerService'; +export * from './shared/ConnectionsManager/ConnectionDialogsService'; +export * from './shared/Navigation/NavigationService'; +export * from './shared/NavigationTabs/NavigationTabsService'; +export * from './shared/NodesManager/NodesManagerService'; +export * from './NavigationTree/NavigationTreeService'; +export * from './NavigationTree/NavigationTreeContextMenuService'; +export * from './shared/ToolsPanel/LogViewTab/LogViewerMenuService'; +export * from './shared/ToolsPanel/LogViewTab/LogViewerService'; + +// Models +export * from './shared/NavigationTabs/Tab'; +export * from './shared/NavigationTabs/TabHandler'; +export * from './shared/Navigation/NavigationContext'; +export * from './shared/TabEntity/TabEntity'; +// Enums +// export * from ''; + +// hooks +export * from './shared/useChildren'; +export * from './shared/NavigationTabs/NavigationTabsBar/Tabs/useTab'; +export * from './shared/NavigationTabs/useTabHandlerState'; +export * from './shared/NodesManager/useDatabaseObjectInfo'; +export * from './shared/NodesManager/useNode'; +export * from './shared/InlineEditor/InlineEditor'; + +// components +export * from './Body'; +export * from './Notifications/NotificationsItem/ErrorDetailsDialog/ErrorDetailsDialog'; + +// Interfaces +export * from './TopNavBar/ConnectionSchemaManager/IConnectionCatalogSchema'; +export * from './shared/Navigation'; +export * from './shared/NodesManager/ENodeFeature'; +export * from './shared/NodesManager/EObjectFeature'; +export * from './shared/NodesManager/NodeManagerUtils'; +export * from './shared/NodesManager/NodeWithParent'; + +// tab entity +export * from './shared/TabEntity/TabContainerEntity'; +export * from './shared/TabEntity/TabEntity'; +export * from './shared/TabEntity/TabToken'; diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts new file mode 100644 index 0000000000..738b97f439 --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionDialogsService.ts @@ -0,0 +1,71 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { injectable } from '@dbeaver/core/di'; +import { + ContextMenuService, + IContextMenuItem, + IMenuContext, +} from '@dbeaver/core/dialogs'; + +import { NavigationTreeContextMenuService } from '../../NavigationTree/NavigationTreeContextMenuService'; +import { EMainMenu, MainMenuService } from '../../TopNavBar/MainMenu/MainMenuService'; +import { EObjectFeature } from '../NodesManager/EObjectFeature'; +import { NodeManagerUtils } from '../NodesManager/NodeManagerUtils'; +import { NodeWithParent } from '../NodesManager/NodeWithParent'; +import { ConnectionsManagerService } from './ConnectionsManagerService'; + + +@injectable() +export class ConnectionDialogsService { + newConnectionMenuToken = 'connectionMenu'; + + constructor(private mainMenuService: MainMenuService, + private contextMenuService: ContextMenuService, + private connectionsManagerService: ConnectionsManagerService) { + } + + registerMenuItems() { + this.mainMenuService.registerMenuItem( + EMainMenu.mainMenuConnectionsPanel, + { + id: this.newConnectionMenuToken, + order: 1, + title: 'app_shared_connectionMenu_newConnection', + isPanel: true, + isHidden: () => this.mainMenuService.isEmptyMenuPanel(this.newConnectionMenuToken), + } + ); + + this.mainMenuService.registerMenuItem( + EMainMenu.mainMenuConnectionsPanel, + { + id: 'mainMenuDisconnect', + order: 2, + title: 'app_shared_connectionMenu_disconnect', + onClick: () => this.connectionsManagerService.closeAllConnections(), + } + ); + + const closeConnection: IContextMenuItem = { + id: 'closeConnection', + isPresent(context) { + return context.contextType === NavigationTreeContextMenuService.nodeContextType + && Boolean((context.data as NodeWithParent)?.object?.features?.includes(EObjectFeature.dataSource)); + }, + title: 'Disconnect', + onClick: (context: IMenuContext) => { + const node = context.data; + const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id); + this.connectionsManagerService.closeConnectionAsync(connectionId); + }, + }; + + this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), closeConnection); + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts new file mode 100644 index 0000000000..38581fc15c --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/ConnectionsManagerService.ts @@ -0,0 +1,170 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { computed, observable } from 'mobx'; +import { Subject } from 'rxjs'; + +import { injectable } from '@dbeaver/core/di'; +import { ConnectionShortInfo, SessionService } from '@dbeaver/core/root'; +import { + ConnectionInfo, + DataSourceInfo, + DriverInfo, + GraphQLService, + NavGetStructContainersQuery, + CachedResource, +} from '@dbeaver/core/sdk'; + +import { NavigationTabsService } from '../NavigationTabs/NavigationTabsService'; +import { NodesManagerService } from '../NodesManager/NodesManagerService'; + +export type DBDriver = Pick< + DriverInfo, + | 'id' + | 'name' + | 'icon' + | 'description' + | 'defaultPort' + | 'sampleURL' + | 'embedded' + | 'anonymousAccess' + | 'promotedScore' +> +export type DBSource = Pick +export type Connection = Pick + +export interface ISchema { + id: string; +} + +export interface ICatalogsAndSchemas { + catalogs: ISchema[]; + schemas: ISchema[]; +} + +@injectable() +export class ConnectionsManagerService { + + @observable private connectionsMap: Map = new Map(); + private dbDrivers = new CachedResource(new Map(), this.refreshDriversAsync.bind(this)); + + @computed get connections(): Connection[] { + return Array.from(this.connectionsMap.values()); + } + + onOpenConnection = new Subject(); + onCloseConnection = new Subject(); + + constructor(private graphQLService: GraphQLService, + private navigationTabsService: NavigationTabsService, + private nodesManagerService: NodesManagerService, + private sessionService: SessionService) { + } + + getDBDrivers(): Map { + return this.dbDrivers.data; + } + + async loadDriversAsync(): Promise> { + return this.dbDrivers.load(); + } + + addOpenedConnection(connection: Connection) { + this.connectionsMap.set(connection.id, connection); + this.onOpenConnection.next(connection); + this.nodesManagerService.updateRootChildren(); // Update connections list, probably here we must also request node info and add it to nodes manager + } + + getConnectionById(connectionId: string): Connection | undefined { + return this.connectionsMap.get(connectionId); + } + + hasAnyConnection(): boolean { + return Boolean(this.connections.length); + } + + async closeAllConnections(): Promise { + for (const connection of this.connections) { + await this.closeConnectionAsync(connection.id, true); + } + await this.nodesManagerService.updateRootChildren(); + } + + async closeConnectionAsync(id: string, skipNodesRefresh?: boolean): Promise { + await this.graphQLService.gql.closeConnection({ id }); + this.onCloseConnection.next(id); + this.closeConnectionTabs(id); + this.connectionsMap.delete(id); + + if (!skipNodesRefresh) { + await this.nodesManagerService.updateRootChildren(); // Update connections list, probably here we must just remove nodes from nodes manager + } + } + + async getCatalogsAndSchemas(connectionId: string, catalogId?: string): Promise { + const schemas = await this.loadSchemasAndCatalogs(connectionId, catalogId); + const catalogAndSchemas: ICatalogsAndSchemas = { + schemas: schemas.navGetStructContainers + .schemaList.map(s => ({ + id: s.name || '', + })), + catalogs: schemas.navGetStructContainers + .catalogList.map(s => ({ + id: s.name || '', + })), + }; + return catalogAndSchemas; + } + + async loadConnectionDriver(driverId: string): Promise { + const drivers = await this.graphQLService.gql.getDriverById({ driverId }); + return drivers.driverList[0] || null; + } + + async restoreConnections() { + for (const connection of this.sessionService.getConnections()) { + this.restoreConnection(connection); + } + } + + private async refreshDriversAsync(data: Map): Promise> { + const { driverList } = await this.graphQLService.gql.driverList(); + + data.clear(); + + for (const driver of driverList) { + data.set(driver.id, driver); + } + + return data; + } + + private closeConnectionTabs(id: string) { + // todo must be called from navigation service + const activeTabs = this.navigationTabsService.tabIdList.filter(tabId => tabId.includes(id)); + for (const tabId of activeTabs) { + this.navigationTabsService.closeTab(tabId); // here must be async, like when we want to show prompt 'save before close' + } + } + + /** + * Note that this request returns either schemaList or catalogList. You never got both lists together + */ + private async loadSchemasAndCatalogs(connectionId: string, catalogId?: string): Promise { + return this.graphQLService.gql.navGetStructContainers({ connectionId, catalogId }); + } + + private restoreConnection(connectionInfo: ConnectionShortInfo) { + const connection: Connection = { + ...connectionInfo, + connected: true, + }; + this.connectionsMap.set(connection.id, connection); + this.onOpenConnection.next(connection); + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/closeConnection.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/closeConnection.gql new file mode 100644 index 0000000000..f6ab51657e --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/closeConnection.gql @@ -0,0 +1,3 @@ +mutation closeConnection($id: ID!) { + closeConnection(id: $id) +} \ No newline at end of file diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/createConnection.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/createConnection.gql new file mode 100644 index 0000000000..56ff0a5bce --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/createConnection.gql @@ -0,0 +1,8 @@ +mutation createConnection($config: ConnectionConfig!) { + createConnection(config: $config) { + id + name + driverId + connected + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/dataSourceList.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/dataSourceList.gql new file mode 100644 index 0000000000..6ee8944c4f --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/dataSourceList.gql @@ -0,0 +1,8 @@ +query dataSourceList { + dataSourceList { + id + name + driverId + description + } +} \ No newline at end of file diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverList.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverList.gql new file mode 100644 index 0000000000..539bfd65f7 --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverList.gql @@ -0,0 +1,13 @@ +query driverList { + driverList { + id + name + icon + description + defaultPort + sampleURL + embedded + anonymousAccess + promotedScore + } +} \ No newline at end of file diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverProperties.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverProperties.gql new file mode 100644 index 0000000000..a64958190f --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/driverProperties.gql @@ -0,0 +1,14 @@ +query driverProperties($driverId: ID!) { + driver: driverList(id: $driverId) { + driverProperties { + id + displayName + description + category + dataType + defaultValue + validValues + } + driverParameters + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/getDriverById.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/getDriverById.gql new file mode 100644 index 0000000000..c4464ef0da --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/getDriverById.gql @@ -0,0 +1,7 @@ +query getDriverById($driverId: ID!) { + driverList(id: $driverId) { + id + name + icon + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/openConnection.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/openConnection.gql new file mode 100644 index 0000000000..3e837f0fcf --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/openConnection.gql @@ -0,0 +1,8 @@ +mutation openConnection($config: ConnectionConfig!) { + openConnection(config: $config) { + id + name + driverId + connected + } +} diff --git a/webapp/packages/core/src/app/shared/ConnectionsManager/queries/testConnection.gql b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/testConnection.gql new file mode 100644 index 0000000000..244edcb72d --- /dev/null +++ b/webapp/packages/core/src/app/shared/ConnectionsManager/queries/testConnection.gql @@ -0,0 +1,5 @@ +mutation testConnection($config: ConnectionConfig!) { + testConnection(config: $config) { + id + } +} diff --git a/webapp/packages/core/src/app/shared/InlineEditor/EditorDialog.tsx b/webapp/packages/core/src/app/shared/InlineEditor/EditorDialog.tsx new file mode 100644 index 0000000000..d32ba269a7 --- /dev/null +++ b/webapp/packages/core/src/app/shared/InlineEditor/EditorDialog.tsx @@ -0,0 +1,68 @@ +/* + * cloudbeaver - Cloud Database Manager + * Copyright (C) 2020 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ + +import { observer } from 'mobx-react'; +import { + useState, useCallback, ChangeEvent, useRef, useEffect, +} from 'react'; +import styled, { css } from 'reshadow'; + +import { Button } from '@dbeaver/core/blocks'; +import { CommonDialogWrapper, DialogComponent, DialogComponentProps } from '@dbeaver/core/dialogs'; +import { useLocale, useTranslate } from '@dbeaver/core/localization'; +import { useStyles } from '@dbeaver/core/theming'; + +const styles = css` + controls { + display: flex; + height: 100%; + align-items: center; + flex-direction: row-reverse; + } + Button { + margin-left: 24px; + } + textarea { + width: 100% !important; + min-height: 250px; + box-sizing: border-box; + } +`; + +export const EditorDialog: DialogComponent = observer( + function EditorDialog(props: DialogComponentProps) { + const textareaRef = useRef(null); + const [value, setValue] = useState(props.payload); + const handleChange = useCallback((event: ChangeEvent) => setValue(event.target.value), []); + const handleApply = useCallback(() => props.resolveDialog(value), [value, props.resolveDialog]); + const translate = useTranslate(); + + useEffect(() => { + setTimeout(() => textareaRef.current?.focus(), 100); + }, []); + + return styled(useStyles(styles))( + + + + + )} + > +