Compare commits
264 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b0d77569c | |||
| 40d2696b43 | |||
| 0f276b0422 | |||
| 59d728a80b | |||
| 715c9e634b | |||
| e1ad90da49 | |||
| ffbcbc6d06 | |||
| 946ec7eb32 | |||
| 011ca0a5c4 | |||
| 72619a9902 | |||
| 97a4cd1653 | |||
| 22d37bda5d | |||
| cc049368e3 | |||
| 14e139a163 | |||
| 8f98b352da | |||
| a9c38d5782 | |||
| dd6cdeb946 | |||
| c9227d1e37 | |||
| c06965c79f | |||
| ccd9a0fa70 | |||
| d1aef572bd | |||
| c5e75dfe3e | |||
| 943f9c1c24 | |||
| cd23687428 | |||
| f7d559ad20 | |||
| 8b340e9b7e | |||
| 4328107a1d | |||
| ad4fa77e46 | |||
| a21b61cc7e | |||
| 26b1c69f12 | |||
| cac85d776a | |||
| 93af08626a | |||
| d7fac5bc6a | |||
| d1033f0e82 | |||
| 9600c213ef | |||
| f2dbe1f103 | |||
| f60ee04883 | |||
| 2cd2546f3f | |||
| 9fe0c2ac86 | |||
| 2f2310a883 | |||
| c2ccf27adf | |||
| 9a275d62c0 | |||
| 35398dd401 | |||
| 99746f48df | |||
| 83eec6a37a | |||
| cca6b77ba5 | |||
| 69b9ce8afc | |||
| 63af42b69d | |||
| 0821d63c9f | |||
| 904a9b54c1 | |||
| 2000a4cf2b | |||
| fcc1459dee | |||
| b44f49a492 | |||
| 87fde4185a | |||
| 817efb1c72 | |||
| 49849820e8 | |||
| 0541cbf1f6 | |||
| 7485582612 | |||
| 9d50ac0093 | |||
| 7c8c7467e1 | |||
| fe1180d1e4 | |||
| c5c4b0b6de | |||
| b25cc146cb | |||
| 46fbcf13ed | |||
| 5ab2ed9646 | |||
| b71b58c93f | |||
| bb35a496f8 | |||
| 03c7bbb7af | |||
| bdb8f44895 | |||
| a80c680914 | |||
| 3f858cae68 | |||
| b96d876f0e | |||
| 087a438a74 | |||
| d2471b63a9 | |||
| 89ea95ef45 | |||
| dda52e6067 | |||
| 7fb6893cce | |||
| 49950068a3 | |||
| 9bc39d5851 | |||
| 5a9f40111c | |||
| cdfc40b73f | |||
| a539a7e4f8 | |||
| ae05d8e561 | |||
| 7508f57549 | |||
| f0f5eaacd8 | |||
| 10f1331376 | |||
| 0f4700fb0c | |||
| 52aed213b7 | |||
| d681ab4e79 | |||
| de06d42781 | |||
| 132e1603de | |||
| 7fe6438292 | |||
| 51c9f09286 | |||
| 84cc2c6b8e | |||
| 077136814d | |||
| 287d29bbdb | |||
| 6ac2122c76 | |||
| cc317c2433 | |||
| 4c4ad638d5 | |||
| a78570a2f5 | |||
| f57805123f | |||
| 18370cbd97 | |||
| 71832cbb5e | |||
| fc4b97adcb | |||
| 162874f383 | |||
| 463b179a6b | |||
| 878c47e58c | |||
| b033658444 | |||
| fc782304fd | |||
| bb39ec6543 | |||
| 131db1af29 | |||
| 1bded8473d | |||
| 1736934a1b | |||
| 1b44050c37 | |||
| b3e25150e9 | |||
| 6b80d72c03 | |||
| db7eb96360 | |||
| fc79f5f07c | |||
| fbd254bafc | |||
| 4cc1da3319 | |||
| 7a5187e283 | |||
| d71f27f8b3 | |||
| 99ab1b38cd | |||
| 351ac5e6a7 | |||
| 5a42e8c9ae | |||
| 0fdf333c0b | |||
| 2f6c749941 | |||
| b87f51c5b5 | |||
| 1de9b9f1fb | |||
| 4962d81661 | |||
| 290acdb68a | |||
| ed11b9e5a1 | |||
| 9d10068799 | |||
| cd03bb09fe | |||
| 0055f03c49 | |||
| 3f318fdef5 | |||
| 10a213d8a3 | |||
| 5558869ff4 | |||
| 60fd1c38ae | |||
| c10c6afbc5 | |||
| 79759b1665 | |||
| 0747614e00 | |||
| ab0a551d67 | |||
| 8fccaffedb | |||
| f211450b06 | |||
| 891a0a99e4 | |||
| 370412ea1d | |||
| 586a17f64e | |||
| 9aa58c8de7 | |||
| 1090d4effe | |||
| 96a52750e2 | |||
| e8ffaaab6f | |||
| a18f98bf28 | |||
| 95e0905f01 | |||
| 54bdaac64f | |||
| ca985330f6 | |||
| 7d5d2d131f | |||
| 8f5d866905 | |||
| ed15db4c20 | |||
| a7926a1a71 | |||
| 3a89d1a07b | |||
| 808267d97a | |||
| 4ab59e09d7 | |||
| c108c7d787 | |||
| fa5e128d45 | |||
| 1a181ff714 | |||
| 0f16f842bf | |||
| e502f5e72c | |||
| 2978063ac8 | |||
| ae0606cc84 | |||
| 0c0c0356a6 | |||
| 1e447a8937 | |||
| 3dcc761c14 | |||
| b1a2093e6b | |||
| ed98a9e2da | |||
| 737298c6f3 | |||
| 0081deb844 | |||
| 0857757ce2 | |||
| f7bab744e6 | |||
| 0316cb16eb | |||
| adcab4ae69 | |||
| 728353d60a | |||
| ac4aa94976 | |||
| d502dc0dfd | |||
| 55d0c77536 | |||
| 79ddbd439f | |||
| 7a0883ea03 | |||
| 5256deb567 | |||
| f993e82b0b | |||
| 698756b9d2 | |||
| 3921913742 | |||
| 576bdd64a0 | |||
| 27a78facf5 | |||
| ee50604112 | |||
| 1c30eb337b | |||
| 6a78272cc5 | |||
| e05f01dce8 | |||
| a29026321f | |||
| f8ee3b92cf | |||
| e0c91214fd | |||
| cc11e63cd7 | |||
| b47c9f81d2 | |||
| fcb93015cc | |||
| fc6355126f | |||
| fd2747d166 | |||
| 3bb22ddc36 | |||
| 0c4d5b5356 | |||
| 61217a944b | |||
| 5434302aa5 | |||
| 23d9c9279c | |||
| 4eae30c022 | |||
| ac7816fc4b | |||
| 10dc7343ae | |||
| a3837083da | |||
| 1644587072 | |||
| 6471141926 | |||
| 65197cf038 | |||
| 785c7e54ab | |||
| 9a2a945762 | |||
| 56eecb0836 | |||
| 8d9cb51baa | |||
| f55049f212 | |||
| dc6093e4fc | |||
| 5df650bc51 | |||
| d23a013579 | |||
| fc5c0eb239 | |||
| 9a42d1d6bd | |||
| 942115acef | |||
| 22b2a62209 | |||
| 0a3a1c9468 | |||
| 655429693a | |||
| 2afd46dc91 | |||
| 9bf755ff25 | |||
| d693cb734b | |||
| 836f48c810 | |||
| 327f2140cf | |||
| e4b605162e | |||
| e952d5c6f8 | |||
| 203e490321 | |||
| 7264e52e6c | |||
| 40bca24dfb | |||
| 56c6ddd392 | |||
| 2cd370d519 | |||
| 42545a027f | |||
| d1ed62ded9 | |||
| 2c9d3abe8c | |||
| a03a3416c2 | |||
| 45138b2bc1 | |||
| 3e57aab717 | |||
| 9aced8f01f | |||
| 3e1e6f7164 | |||
| 5aff88630a | |||
| 7e3555d84a | |||
| 6d7e7f97c7 | |||
| 0785c375a5 | |||
| 0d68eeac63 | |||
| 634e63a3dc | |||
| f60fd9cc63 | |||
| 49628ad3cf | |||
| 13a18eb556 | |||
| 76ec548b4f | |||
| 9c9c82a547 | |||
| b1ccd16870 | |||
| fedf2db847 |
@@ -3,14 +3,13 @@ name: Electron app
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
|
||||
# branches:
|
||||
# - production
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
strategy:
|
||||
@@ -33,31 +32,94 @@ jobs:
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
- name: fillNativeModulesElectron
|
||||
run: |
|
||||
yarn fillNativeModulesElectron
|
||||
- name: Install Snapcraft
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
uses: samuelmeuli/action-snapcraft@v1
|
||||
- name: Publish
|
||||
run: |
|
||||
yarn run build:app
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }} # token for electron publish
|
||||
WIN_CSC_LINK: ${{ secrets.WINCERT_CERTIFICATE }}
|
||||
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINCERT_PASSWORD }}
|
||||
|
||||
- name: Copy artifacts Linux, MacOs
|
||||
if: matrix.os != 'windows-2016'
|
||||
- name: generatePadFile
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mv app/dist/*.deb artifacts/dbgate-linux.deb || true
|
||||
mv app/dist/*.AppImage artifacts/dbgate-linux.AppImage || true
|
||||
mv app/dist/*.dmg artifacts/dbgate-mac.dmg || true
|
||||
yarn generatePadFile
|
||||
|
||||
mv app/dist/*.deb artifacts || true
|
||||
mv app/dist/*.AppImage artifacts || true
|
||||
mv app/dist/*.dmg artifacts || true
|
||||
- name: Save snap login
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: 'echo "$SNAPCRAFT_LOGIN" > snapcraft.login'
|
||||
shell: bash
|
||||
env:
|
||||
SNAPCRAFT_LOGIN: ${{secrets.SNAPCRAFT_LOGIN}}
|
||||
|
||||
mv app/dist/latest.yml artifacts/latest.yml || true
|
||||
- name: Copy artifacts Win
|
||||
- name: publishSnap
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: |
|
||||
snapcraft login --with snapcraft.login
|
||||
snapcraft upload --release=beta app/dist/*.snap
|
||||
|
||||
- name: Copy artifacts
|
||||
run: |
|
||||
mkdir artifacts
|
||||
|
||||
cp app/dist/*.deb artifacts/dbgate-latest.deb || true
|
||||
cp app/dist/*.AppImage artifacts/dbgate-latest.AppImage || true
|
||||
cp app/dist/*.exe artifacts/dbgate-latest.exe || true
|
||||
cp app/dist/*.dmg artifacts/dbgate-latest.dmg || true
|
||||
|
||||
mv app/dist/*.exe artifacts/ || true
|
||||
mv app/dist/*.AppImage artifacts/ || true
|
||||
mv app/dist/*.deb artifacts/ || true
|
||||
mv app/dist/*.dmg artifacts/ || true
|
||||
mv app/dist/*.snap artifacts/dbgate-latest.snap || true
|
||||
|
||||
# - name: Copy artifacts Linux, MacOs
|
||||
# if: matrix.os != 'windows-2016'
|
||||
# run: |
|
||||
# mkdir artifacts
|
||||
|
||||
# cp app/dist/*.AppImage artifacts/ || true
|
||||
# cp app/dist/*.dmg artifacts/ || true
|
||||
# cp app/dist/*.deb artifacts/ || true
|
||||
|
||||
# mv app/dist/*.deb artifacts/dbgate-linux.deb || true
|
||||
# mv app/dist/*.AppImage artifacts/dbgate-linux.AppImage || true
|
||||
# mv app/dist/*.dmg artifacts/dbgate-mac.dmg || true
|
||||
|
||||
# - name: Copy artifacts Win
|
||||
# if: matrix.os == 'windows-2016'
|
||||
# run: |
|
||||
# mkdir artifacts
|
||||
|
||||
# cp app/dist/*.exe artifacts/ || true
|
||||
|
||||
# mv app/dist/*.exe artifacts/dbgate-windows.exe
|
||||
|
||||
# mv app/dist/latest.yml artifacts/latest.yml || true
|
||||
|
||||
- name: Copy latest.yml (windows)
|
||||
if: matrix.os == 'windows-2016'
|
||||
run: |
|
||||
mkdir artifacts
|
||||
mv app/dist/*.exe artifacts/dbgate-windows.exe
|
||||
mv app/dist/*.zip artifacts/dbgate-windows.zip
|
||||
mv app/dist/latest.yml artifacts/latest.yml || true
|
||||
mv app/dist/dbgate-pad.xml artifacts/ || true
|
||||
|
||||
mv app/dist/*.exe artifacts || true
|
||||
- name: Copy latest-linux.yml
|
||||
if: matrix.os == 'ubuntu-18.04'
|
||||
run: |
|
||||
mv app/dist/latest-linux.yml artifacts/latest-linux.yml || true
|
||||
|
||||
- name: Copy latest-mac.yml
|
||||
if: matrix.os == 'macOS-10.14'
|
||||
run: |
|
||||
mv app/dist/latest-mac.yml artifacts/latest-mac.yml || true
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v1
|
||||
@@ -69,7 +131,7 @@ jobs:
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
files: "artifacts/**"
|
||||
files: 'artifacts/**'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
@@ -82,4 +144,4 @@ jobs:
|
||||
# tag_name: ${{ github.ref }}
|
||||
# release_name: Release ${{ github.ref }}
|
||||
# draft: false
|
||||
# prerelease: false
|
||||
# prerelease: false
|
||||
|
||||
@@ -37,6 +37,9 @@ jobs:
|
||||
- name: yarn install
|
||||
run: |
|
||||
yarn install
|
||||
- name: setCurrentVersion
|
||||
run: |
|
||||
yarn setCurrentVersion
|
||||
- name: Prepare docker image
|
||||
run: |
|
||||
yarn run prepare:docker
|
||||
|
||||
@@ -22,3 +22,5 @@ dist
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
app/src/nativeModulesContent.js
|
||||
packages/api/src/nativeModulesContent.js
|
||||
|
||||
@@ -1,674 +1,21 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. 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
|
||||
them 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 prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. 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.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey 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;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If 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 convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU 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 that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
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.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
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.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
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
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
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 3 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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program 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, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU 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. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jan Prochazka
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -21,6 +21,7 @@ DbGate is fast and efficient database administration tool. It is focused to work
|
||||
* Archives - backup your data in JSON files on local filesystem (or on DbGate server, when using web application)
|
||||
* Light and dark theme
|
||||
* For detailed info, how to run DbGate in docker container, visit [docker hub](https://hub.docker.com/r/dbgate/dbgate)
|
||||
* Extensible plugin architecture
|
||||
|
||||

|
||||
|
||||
@@ -34,14 +35,23 @@ DbGate is fast and efficient database administration tool. It is focused to work
|
||||
* There is plan to incorporate SQLite to support work with local datasets
|
||||
* Platform independed - will run as web application in single docker container on server, or as application using Electron platform on Linux, Windows and Mac
|
||||
|
||||
## Plugins
|
||||
Plugins are standard NPM packages published on [npmjs.com](https://www.npmjs.com).
|
||||
See all [existing DbGate plugins](https://www.npmjs.com/search?q=keywords:dbgateplugin).
|
||||
Visit [dbgate generator homepage](https://github.com/dbshell/generator-dbgate) to see, how to create your own plugin.
|
||||
|
||||
Currently following extensions can be implemented using plugins:
|
||||
- File format parsers/writers
|
||||
- Database engine connectors
|
||||
|
||||
## How Can I Contribute?
|
||||
You're welcome to contribute to this project! Below are some ideas, how to contribute:
|
||||
|
||||
* Create plugins for new import/export formats
|
||||
* Bug fixing
|
||||
* Test Mac edition
|
||||
* Improve linux package build, add to APT repository
|
||||
* Auto-upgrade of electron application
|
||||
* Support for new import/export formats
|
||||
|
||||
Any help is appreciated!
|
||||
|
||||
@@ -99,9 +109,10 @@ Some dbgate packages can be used also without DbGate. You can find them on [NPM
|
||||
* [api](https://github.com/dbshell/dbgate/tree/master/packages/api) - backend, Javascript, ExpressJS [](https://www.npmjs.com/package/dbgate-api)
|
||||
* [datalib](https://github.com/dbshell/dbgate/tree/master/packages/datalib) - TypeScript library for utility classes
|
||||
* [app](https://github.com/dbshell/dbgate/tree/master/app) - application (JavaScript)
|
||||
* [engines](https://github.com/dbshell/dbgate/tree/master/packages/engines) - drivers for database engine (mssql, mysql, postgres), analysing database structure, creating specific queries (JavaScript) [](https://www.npmjs.com/package/dbgate-engines)
|
||||
structure, creating specific queries (JavaScript) [](https://www.npmjs.com/package/dbgate-engines)
|
||||
* [filterparser](https://github.com/dbshell/dbgate/tree/master/packages/filterparser) - TypeScript library for parsing data filter expressions using parsimmon
|
||||
* [sqltree](https://github.com/dbshell/dbgate/tree/master/packages/sqltree) - JSON representation of SQL query, functions converting to SQL (TypeScript) [](https://www.npmjs.com/package/dbgate-sqltree)
|
||||
* [types](https://github.com/dbshell/dbgate/tree/master/packages/types) - common TypeScript definitions [](https://www.npmjs.com/package/dbgate-types)
|
||||
* [web](https://github.com/dbshell/dbgate/tree/master/packages/web) - frontend in React (JavaScript)
|
||||
* [tools](https://github.com/dbshell/dbgate/tree/master/packages/tools) - various tools [](https://www.npmjs.com/package/dbgate-tools)
|
||||
|
||||
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 Jan Prochazka
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,156 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<XML_DIZ_INFO>
|
||||
<MASTER_PAD_VERSION_INFO>
|
||||
<MASTER_PAD_VERSION>3.01</MASTER_PAD_VERSION>
|
||||
<MASTER_PAD_EDITOR>PADManager 2.0.46</MASTER_PAD_EDITOR>
|
||||
<MASTER_PAD_INFO>Portable Application Description, or PAD for short, is a data set that is used by shareware authors to disseminate information to anyone interested in their software products. To find out more go to http://www.asp-shareware.org/pad</MASTER_PAD_INFO>
|
||||
</MASTER_PAD_VERSION_INFO>
|
||||
<Company_Info>
|
||||
<Company_Name>JenaSoft</Company_Name>
|
||||
<Address_1>Vratislavska 396</Address_1>
|
||||
<Address_2>Praha 8</Address_2>
|
||||
<City_Town>Prague</City_Town>
|
||||
<State_Province/>
|
||||
<Zip_Postal_Code>18100</Zip_Postal_Code>
|
||||
<Country>Czech Republic</Country>
|
||||
<Company_WebSite_URL>https://dbgate.org/</Company_WebSite_URL>
|
||||
<Contact_Info>
|
||||
<Contact_First_Name>Jan</Contact_First_Name>
|
||||
<Contact_Last_Name>Prochazka</Contact_Last_Name>
|
||||
<Contact_Email>jenasoft.database@gmail.com</Contact_Email>
|
||||
<Author_First_Name>Jan</Author_First_Name>
|
||||
<Author_Last_Name>Prochazka</Author_Last_Name>
|
||||
<Author_Email>jenasoft.database@gmail.com</Author_Email>
|
||||
</Contact_Info>
|
||||
<Support_Info>
|
||||
<Sales_Email>jenasoft.database@gmail.com</Sales_Email>
|
||||
<Support_Email>jenasoft.database@gmail.com</Support_Email>
|
||||
<General_Email>jenasoft.database@gmail.com</General_Email>
|
||||
<Sales_Phone/>
|
||||
<Support_Phone/>
|
||||
<General_Phone/>
|
||||
<Fax_Phone/>
|
||||
</Support_Info>
|
||||
</Company_Info>
|
||||
<Site>
|
||||
<Site_FORM>Y</Site_FORM>
|
||||
<Site_VERSION>1.0</Site_VERSION>
|
||||
<Site_URL>https://dbgate.org/</Site_URL>
|
||||
<Site_DESCRIPTION>This PAD extension allows you to add your site info into your PAD file. This info can be used by site submission software or by web directories themselves.</Site_DESCRIPTION>
|
||||
<Site_Contact_Email>jenasoft.database@gmail.com</Site_Contact_Email>
|
||||
<Site_Contact_First_Name>Jan</Site_Contact_First_Name>
|
||||
<Site_Contact_Last_Name>Prochazka</Site_Contact_Last_Name>
|
||||
<Site_Description_100>Database tools - effecient data manipulation</Site_Description_100>
|
||||
<Site_Description_250>Database tools - effecient data manipulation, import, export, filtering, master/detail, query designer</Site_Description_250>
|
||||
<Site_Description_450>Database tools - effecient data manipulation, import, export, filtering, master/detail, query designer</Site_Description_450>
|
||||
<Site_Site_URL>https://dbgate.org/</Site_Site_URL>
|
||||
<Site_Site_Title>DbGate</Site_Site_Title>
|
||||
<Site_Keywords>database, manager, organizer, explorer, mssql, sqlserver, tsql, sql, mysql, postgre, postgresql, export, import, filter</Site_Keywords>
|
||||
</Site>
|
||||
<Program_Info>
|
||||
<Program_Name>DbGate</Program_Name>
|
||||
<Program_Version>#VERSION#</Program_Version>
|
||||
<Program_Release_Month>#MONTH#</Program_Release_Month>
|
||||
<Program_Release_Day>#DAY#</Program_Release_Day>
|
||||
<Program_Release_Year>#YEAR#</Program_Release_Year>
|
||||
<Program_Cost_Dollars>0.00</Program_Cost_Dollars>
|
||||
<Program_Type>Freeware</Program_Type>
|
||||
<Program_Release_Status>Major Update</Program_Release_Status>
|
||||
<Program_Install_Support>Install and Uninstall</Program_Install_Support>
|
||||
<Program_OS_Support>Linux,Linux Gnome,Linux GPL,Linux Open Source,Mac OS X,Win7 x64,WinOther</Program_OS_Support>
|
||||
<Program_Language>English</Program_Language>
|
||||
<Program_Change_Info>Query designer</Program_Change_Info>
|
||||
<Program_System_Requirements></Program_System_Requirements>
|
||||
<Program_Category_Class>Business::Databases & Tools</Program_Category_Class>
|
||||
<File_Info>
|
||||
<File_Size_Bytes>#SIZE_BYTES#</File_Size_Bytes>
|
||||
<File_Size_K>#SIZE_KB#</File_Size_K>
|
||||
<File_Size_MB>#SIZE_MB#</File_Size_MB>
|
||||
<Filename_Versioned/>
|
||||
<Filename_Previous/>
|
||||
<Filename_Generic/>
|
||||
<Filename_Long/>
|
||||
</File_Info>
|
||||
<Expire_Info>
|
||||
<Has_Expire_Info>N</Has_Expire_Info>
|
||||
<Expire_Based_On>Days</Expire_Based_On>
|
||||
<Expire_Count/>
|
||||
<Expire_Other_Info/>
|
||||
<Expire_Month/>
|
||||
<Expire_Day/>
|
||||
<Expire_Year/>
|
||||
</Expire_Info>
|
||||
<Program_Categories>Business & Finance :: Database Managers,Programming :: Databases & Networks,Programming :: SQL</Program_Categories>
|
||||
<Limitations>No limitations</Limitations>
|
||||
<Program_Cost_Other_Code/>
|
||||
<Program_Cost_Other/>
|
||||
<Awards/>
|
||||
<Program_Specific_Category/>
|
||||
<Includes_JAVA_VM/>
|
||||
<Includes_VB_Runtime/>
|
||||
<Includes_DirectX/>
|
||||
</Program_Info>
|
||||
<Web_Info>
|
||||
<Application_URLs>
|
||||
<Application_Info_URL>https://dbgate.org/</Application_Info_URL>
|
||||
<Application_Screenshot_URL>https://raw.githubusercontent.com/dbshell/dbgate/master/screenshot.png</Application_Screenshot_URL>
|
||||
<Application_Icon_URL>https://github.com/dbshell/dbgate/raw/master/app/icon.png</Application_Icon_URL>
|
||||
<Application_XML_File_URL>https://github.com/dbshell/dbgate/releases/latest/download/dbgate-pad.xml</Application_XML_File_URL>
|
||||
<Application_Order_URL></Application_Order_URL>
|
||||
</Application_URLs>
|
||||
<Download_URLs>
|
||||
<Primary_Download_URL>https://github.com/dbshell/dbgate/releases/latest/download/dbgate-latest.exe</Primary_Download_URL>
|
||||
<Secondary_Download_URL></Secondary_Download_URL>
|
||||
<Additional_Download_URL_1/>
|
||||
<Additional_Download_URL_2/>
|
||||
</Download_URLs>
|
||||
</Web_Info>
|
||||
<Permissions>
|
||||
<Distribution_Permissions>This product can be freely distributed through the Internet. To put it onto a CD, please contact jenasoft.database@gmail.com</Distribution_Permissions>
|
||||
<EULA>MIT License
|
||||
|
||||
Copyright (c) 2021 Jan Prochazka
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.</EULA>
|
||||
</Permissions>
|
||||
<Program_Descriptions>
|
||||
<English>
|
||||
<Keywords>sql, management, tool, microsoft, sql, mssql, postgres, postgresql, mysql, csv, data, export, import, javascript, database</Keywords>
|
||||
<Char_Desc_45>Modern data manipulation tool</Char_Desc_45>
|
||||
<Char_Desc_80>Modern cross-platform data manipulation tool</Char_Desc_80>
|
||||
<Char_Desc_250>Modern cross-platform data manipulation tool for Microsoft SQL Server, PostgreSQL and MySQL. Query designer. Master/detail views. Advanced data filtering. Export, import. Exports can be saved as jobs</Char_Desc_250>
|
||||
<Char_Desc_450>Modern cross-platform data manipulation tool for Microsoft SQL Server, PostgreSQL and MySQL. Query designer. Charts. Master/detail views. Advanced data filtering. Export, import. Exports can be saved as jobs</Char_Desc_450>
|
||||
<Char_Desc_2000>
|
||||
Modern cross-platform data manipulation tool for Microsoft SQL Server, PostgreSQL and MySQL
|
||||
Main features:
|
||||
Quick database browser
|
||||
Database editor, view and and views, stored procedures, functions, triggers, table relations
|
||||
Export, import - CSV, MS Excel
|
||||
Form view for tables with many columns
|
||||
Query designer
|
||||
Advanced data filtering
|
||||
Export, import
|
||||
JavaScript data scripting
|
||||
Run queries, code completion
|
||||
Display data from referenced tables
|
||||
Create charts, supports for active charts - are refreshed when underlying source is changed
|
||||
</Char_Desc_2000>
|
||||
</English>
|
||||
</Program_Descriptions>
|
||||
</XML_DIZ_INFO>
|
||||
+35
-9
@@ -1,34 +1,57 @@
|
||||
{
|
||||
"name": "dbgate",
|
||||
"version": "3.7.33",
|
||||
"version": "3.8.39",
|
||||
"private": true,
|
||||
"author": "Jan Prochazka <jenasoft.database@gmail.com>",
|
||||
"description": "Opensource database administration tool",
|
||||
"dependencies": {
|
||||
"electron-log": "^4.3.1",
|
||||
"electron-store": "^5.1.1",
|
||||
"electron-updater": "^4.3.1"
|
||||
"electron-updater": "^4.3.5"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
},
|
||||
"build": {
|
||||
"appId": "org.dbgate",
|
||||
"mac": {
|
||||
"category": "database",
|
||||
"icon": "icon512.png"
|
||||
"icon": "icon512.png",
|
||||
"artifactName": "dbgate-mac-${version}.${ext}",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"AppImage",
|
||||
"deb"
|
||||
"deb",
|
||||
"snap"
|
||||
],
|
||||
"icon": "icon512.png"
|
||||
"icon": "icon.png",
|
||||
"artifactName": "dbgate-linux-${version}.${ext}",
|
||||
"category": "Development",
|
||||
"synopsis": "Database administration tool for MS SQL, MySQL and PostgreSQL",
|
||||
"publish": [
|
||||
"github"
|
||||
]
|
||||
},
|
||||
"win": {
|
||||
"target": [
|
||||
"nsis"
|
||||
],
|
||||
"icon": "icon.ico"
|
||||
"artifactName": "dbgate-windows-${version}.${ext}",
|
||||
"icon": "icon.ico",
|
||||
"publish": [
|
||||
"github"
|
||||
],
|
||||
"rfc3161TimeStampServer": "http://sha256timestamp.ws.symantec.com/sha256/timestamp"
|
||||
},
|
||||
"files": [
|
||||
"packages",
|
||||
"src"
|
||||
"src",
|
||||
"icon.png"
|
||||
]
|
||||
},
|
||||
"homepage": "./",
|
||||
@@ -45,7 +68,10 @@
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.2.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"electron": "8.1.1",
|
||||
"electron-builder": "22.4.1"
|
||||
"electron": "11.1.1",
|
||||
"electron-builder": "22.9.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
+29
-8
@@ -2,8 +2,10 @@ const electron = require('electron');
|
||||
const os = require('os');
|
||||
const { Menu } = require('electron');
|
||||
const { fork } = require('child_process');
|
||||
var { autoUpdater } = require('electron-updater');
|
||||
const { autoUpdater } = require('electron-updater');
|
||||
const Store = require('electron-store');
|
||||
const log = require('electron-log');
|
||||
|
||||
// Module to control application life.
|
||||
const app = electron.app;
|
||||
// Module to create native browser window.
|
||||
@@ -19,6 +21,9 @@ const store = new Store();
|
||||
let mainWindow;
|
||||
let splashWindow;
|
||||
|
||||
log.transports.file.level = 'debug';
|
||||
autoUpdater.logger = log;
|
||||
|
||||
function hideSplash() {
|
||||
if (splashWindow) {
|
||||
splashWindow.destroy();
|
||||
@@ -99,6 +104,12 @@ function buildMenu() {
|
||||
require('electron').shell.openExternal('https://hub.docker.com/r/dbgate/dbgate');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'About',
|
||||
click() {
|
||||
mainWindow.webContents.executeJavaScript(`dbgate_showAbout()`);
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -108,15 +119,17 @@ function buildMenu() {
|
||||
|
||||
function createWindow() {
|
||||
const bounds = store.get('winBounds');
|
||||
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1200,
|
||||
height: 800,
|
||||
title: 'DbGate',
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : 'icon512.png',
|
||||
...bounds,
|
||||
icon: os.platform() == 'win32' ? 'icon.ico' : path.resolve(__dirname, '../icon.png'),
|
||||
show: false,
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -130,13 +143,16 @@ function createWindow() {
|
||||
protocol: 'file:',
|
||||
slashes: true,
|
||||
});
|
||||
mainWindow.webContents.on('did-finish-load', function () {
|
||||
mainWindow.webContents.on('did-finish-load', function() {
|
||||
hideSplash();
|
||||
});
|
||||
mainWindow.on('close', () => {
|
||||
store.set('winBounds', mainWindow.getBounds());
|
||||
});
|
||||
mainWindow.loadURL(startUrl);
|
||||
if (os.platform() == 'linux') {
|
||||
mainWindow.setIcon(path.resolve(__dirname, '../icon.png'));
|
||||
}
|
||||
}
|
||||
|
||||
splashWindow = new BrowserWindow({
|
||||
@@ -156,8 +172,13 @@ function createWindow() {
|
||||
if (process.env.ELECTRON_START_URL) {
|
||||
loadMainWindow();
|
||||
} else {
|
||||
const apiProcess = fork(path.join(__dirname, '../packages/api/dist/bundle.js'), ['--dynport']);
|
||||
apiProcess.on('message', (msg) => {
|
||||
const apiProcess = fork(path.join(__dirname, '../packages/api/dist/bundle.js'), [
|
||||
'--dynport',
|
||||
'--native-modules',
|
||||
path.join(__dirname, 'nativeModules'),
|
||||
// '../../../src/nativeModules'
|
||||
]);
|
||||
apiProcess.on('message', msg => {
|
||||
if (msg.msgtype == 'listening') {
|
||||
const { port } = msg;
|
||||
global['port'] = port;
|
||||
@@ -173,7 +194,7 @@ function createWindow() {
|
||||
// mainWindow.webContents.openDevTools();
|
||||
|
||||
// Emitted when the window is closed.
|
||||
mainWindow.on('closed', function () {
|
||||
mainWindow.on('closed', function() {
|
||||
// Dereference the window object, usually you would store windows
|
||||
// in an array if your app supports multi windows, this is the time
|
||||
// when you should delete the corresponding element.
|
||||
@@ -192,7 +213,7 @@ function onAppReady() {
|
||||
app.on('ready', onAppReady);
|
||||
|
||||
// Quit when all windows are closed.
|
||||
app.on('window-all-closed', function () {
|
||||
app.on('window-all-closed', function() {
|
||||
// On OS X it is common for applications and their menu bar
|
||||
// to stay active until the user quits explicitly with Cmd + Q
|
||||
if (process.platform !== 'darwin') {
|
||||
@@ -200,7 +221,7 @@ app.on('window-all-closed', function () {
|
||||
}
|
||||
});
|
||||
|
||||
app.on('activate', function () {
|
||||
app.on('activate', function() {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (mainWindow === null) {
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
const content = require('./nativeModulesContent');
|
||||
|
||||
module.exports = content;
|
||||
+620
-246
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
const fs = require('fs');
|
||||
|
||||
let fillContent = '';
|
||||
|
||||
// if (!process.argv.includes('--electron')) {
|
||||
if (process.platform == 'win32') {
|
||||
fillContent += `content.msnodesqlv8 = () => require('msnodesqlv8');`;
|
||||
}
|
||||
|
||||
const getContent = (empty) => `
|
||||
// this file is generated automatically by script fillNativeModules.js, do not edit it manually
|
||||
const content = {};
|
||||
|
||||
${empty ? '' : fillContent}
|
||||
|
||||
module.exports = content;
|
||||
`;
|
||||
|
||||
fs.writeFileSync(
|
||||
'packages/api/src/nativeModulesContent.js',
|
||||
getContent(process.argv.includes('--electron') ? true : false)
|
||||
);
|
||||
fs.writeFileSync('app/src/nativeModulesContent.js', getContent(false));
|
||||
@@ -0,0 +1,31 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const packageJson = fs.readFileSync('app/package.json', { encoding: 'utf-8' });
|
||||
const json = JSON.parse(packageJson);
|
||||
const { version } = json;
|
||||
|
||||
const template = fs.readFileSync('app/dbgate-pad-template.xml', { encoding: 'utf-8' });
|
||||
|
||||
const date = new Date();
|
||||
let size = 0;
|
||||
|
||||
const files = fs.readdirSync('app/dist');
|
||||
for (const file of files) {
|
||||
if (file.endsWith('.exe')) {
|
||||
const stats = fs.statSync(path.join('app', 'dist', file));
|
||||
size = stats.size;
|
||||
}
|
||||
}
|
||||
|
||||
const padContent = template
|
||||
.replace('#VERSION#', version)
|
||||
.replace('#YEAR#', date.getFullYear())
|
||||
.replace('#MONTH#', (date.getMonth() + 1).toString().padStart(2, '0'))
|
||||
.replace('#DAY#', date.getDate().toString().padStart(2, '0'))
|
||||
.replace('#SIZE_BYTES#', size)
|
||||
.replace('#SIZE_KB#', Math.round(size / 1024))
|
||||
.replace('#SIZE_MB#', Math.round(size / 1024 / 1024));
|
||||
|
||||
if (size > 0) {
|
||||
fs.writeFileSync('app/dist/dbgate-pad.xml', padContent, 'utf-8');
|
||||
}
|
||||
+6
-1
@@ -7,6 +7,7 @@
|
||||
"scripts": {
|
||||
"start:api": "yarn workspace dbgate-api start",
|
||||
"start:api:portal": "yarn workspace dbgate-api start:portal",
|
||||
"start:api:covid": "yarn workspace dbgate-api start:covid",
|
||||
"start:web": "yarn workspace dbgate-web start",
|
||||
"start:sqltree": "yarn workspace dbgate-sqltree start",
|
||||
"start:tools": "yarn workspace dbgate-tools start",
|
||||
@@ -22,6 +23,10 @@
|
||||
"build:web:docker": "yarn workspace dbgate-web build:docker",
|
||||
"build:app:local": "cd app && yarn build:local",
|
||||
"start:app:local": "cd app && yarn start:local",
|
||||
"setCurrentVersion": "node setCurrentVersion",
|
||||
"generatePadFile": "node generatePadFile",
|
||||
"fillNativeModules": "node fillNativeModules",
|
||||
"fillNativeModulesElectron": "node fillNativeModules --eletron",
|
||||
|
||||
"copy:docker:build": "copyfiles packages/api/dist/* docker -f && copyfiles packages/web/build/* docker -u 2 && copyfiles \"packages/web/build/**/*\" docker -u 2",
|
||||
"prepare:docker": "yarn build:web:docker && yarn build:api && yarn copy:docker:build",
|
||||
@@ -32,7 +37,7 @@
|
||||
"ts:api": "yarn workspace dbgate-api ts",
|
||||
"ts:web": "yarn workspace dbgate-web ts",
|
||||
"ts": "yarn ts:api && yarn ts:web",
|
||||
"postinstall": "patch-package"
|
||||
"postinstall": "patch-package && yarn fillNativeModules"
|
||||
},
|
||||
"dependencies": {
|
||||
"concurrently": "^5.1.0",
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
CONNECTIONS=mysql
|
||||
|
||||
LABEL_mysql=MySql
|
||||
SERVER_mysql=dbgate.org
|
||||
USER_mysql=reader
|
||||
PASSWORD_mysql=CovidReader2020
|
||||
PORT_mysql=3326
|
||||
ENGINE_mysql=mysql@dbgate-plugin-mysql
|
||||
SINGLE_CONNECTION=mysql
|
||||
SINGLE_DATABASE=covid
|
||||
|
||||
PERMISSIONS=files/charts/read
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-api-v
|
||||
version-git-message "packages-api v%s"
|
||||
+8
-40
@@ -11,15 +11,19 @@ Allows run DbGate data-manipulation scripts.
|
||||
This example exports table Customer info CSV file.
|
||||
|
||||
```javascript
|
||||
|
||||
const dbgateApi = require('dbgate-api');
|
||||
const dbgatePluginMssql = require("dbgate-plugin-mssql");
|
||||
const dbgatePluginCsv = require("dbgate-plugin-csv");
|
||||
|
||||
dbgateApi.registerPlugins(dbgatePluginMssql);
|
||||
|
||||
async function run() {
|
||||
const reader = await dbgateApi.tableReader({
|
||||
connection: { server: 'localhost', engine: 'mssql', user: 'sa', password: 'xxxx', database: 'Chinook' },
|
||||
schemaName: 'dbo',
|
||||
pureName: 'Customer',
|
||||
});
|
||||
const writer = await dbgateApi.csvWriter({ fileName: 'Customer.csv' });
|
||||
const writer = await dbgatePluginCsv.shellApi.writer({ fileName: 'Customer.csv' });
|
||||
await dbgateApi.copyStream(reader, writer);
|
||||
|
||||
console.log('Finished job script');
|
||||
@@ -88,36 +92,11 @@ Imports data into table. Options are optional, default values are false.
|
||||
});
|
||||
```
|
||||
|
||||
### dbgateApi.csvReader
|
||||
Reads CSV file
|
||||
```js
|
||||
const reader = await dbgateApi.csvReader({
|
||||
fileName: '/home/root/test.csv',
|
||||
encoding: 'utf-8',
|
||||
header: true,
|
||||
delimiter: ',',
|
||||
quoted: false,
|
||||
limitRows: null
|
||||
});
|
||||
```
|
||||
|
||||
### dbgateApi.csvWriter
|
||||
Writes CSV file
|
||||
```js
|
||||
const reader = await dbgateApi.csvWriter({
|
||||
fileName: '/home/root/test.csv',
|
||||
encoding: 'utf-8',
|
||||
header: true,
|
||||
delimiter: ',',
|
||||
quoted: false
|
||||
});
|
||||
```
|
||||
|
||||
### dbgateApi.jsonLinesReader
|
||||
Reads JSON lines data file. On first line could be structure. Every line contains one row as JSON serialized object.
|
||||
```js
|
||||
const reader = await dbgateApi.jsonLinesReader({
|
||||
fileName: '/home/root/test.jsonl',
|
||||
fileName: 'test.jsonl',
|
||||
encoding: 'utf-8',
|
||||
header: true,
|
||||
limitRows: null
|
||||
@@ -128,19 +107,8 @@ Reads JSON lines data file. On first line could be structure. Every line contain
|
||||
Writes JSON lines data file. On first line could be structure. Every line contains one row as JSON serialized object.
|
||||
```js
|
||||
const reader = await dbgateApi.jsonLinesWriter({
|
||||
fileName: '/home/root/test.jsonl',
|
||||
fileName: 'test.jsonl',
|
||||
encoding: 'utf-8',
|
||||
header: true
|
||||
});
|
||||
```
|
||||
|
||||
### dbgateApi.excelSheetReader
|
||||
Reads tabular data from one sheet in MS Excel file.
|
||||
```js
|
||||
const reader = await dbgateApi.excelSheetReader({
|
||||
fileName: '/home/root/test.xlsx',
|
||||
sheetName: 'Album',
|
||||
limitRows: null
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "dbgate-api",
|
||||
"main": "src/index.js",
|
||||
"version": "1.0.5",
|
||||
"version": "1.0.7",
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,6 +23,7 @@
|
||||
"body-parser": "^1.19.0",
|
||||
"bufferutil": "^4.0.1",
|
||||
"byline": "^5.0.0",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cors": "^2.8.5",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
@@ -38,11 +39,14 @@
|
||||
"lodash": "^4.17.15",
|
||||
"ncp": "^2.0.0",
|
||||
"nedb-promises": "^4.0.1",
|
||||
"tar": "^6.0.5"
|
||||
"node-cron": "^2.0.3",
|
||||
"tar": "^6.0.5",
|
||||
"uuid": "^3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "nodemon src/index.js",
|
||||
"start:portal": "env-cmd nodemon src/index.js",
|
||||
"start:covid": "env-cmd -f .covid-env nodemon src/index.js",
|
||||
"ts": "tsc",
|
||||
"build": "webpack"
|
||||
},
|
||||
@@ -50,9 +54,13 @@
|
||||
"@types/lodash": "^4.14.149",
|
||||
"dbgate-types": "^1.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"node-loader": "^1.0.2",
|
||||
"nodemon": "^2.0.2",
|
||||
"typescript": "^3.7.4",
|
||||
"webpack": "^4.42.0",
|
||||
"webpack-cli": "^3.3.11"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"msnodesqlv8": "^2.0.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,34 @@
|
||||
const currentVersion = require('../currentVersion');
|
||||
|
||||
module.exports = {
|
||||
get_meta: 'get',
|
||||
async get() {
|
||||
const toolbarButtons = process.env.TOOLBAR;
|
||||
const toolbar = toolbarButtons
|
||||
? toolbarButtons.split(',').map((name) => ({
|
||||
name,
|
||||
icon: process.env[`ICON_${name}`],
|
||||
title: process.env[`TITLE_${name}`],
|
||||
page: process.env[`PAGE_${name}`],
|
||||
}))
|
||||
: null;
|
||||
const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
// const toolbarButtons = process.env.TOOLBAR;
|
||||
// const toolbar = toolbarButtons
|
||||
// ? toolbarButtons.split(',').map((name) => ({
|
||||
// name,
|
||||
// icon: process.env[`ICON_${name}`],
|
||||
// title: process.env[`TITLE_${name}`],
|
||||
// page: process.env[`PAGE_${name}`],
|
||||
// }))
|
||||
// : null;
|
||||
// const startupPages = process.env.STARTUP_PAGES ? process.env.STARTUP_PAGES.split(',') : [];
|
||||
const permissions = process.env.PERMISSIONS ? process.env.PERMISSIONS.split(',') : null;
|
||||
const singleDatabase =
|
||||
process.env.SINGLE_CONNECTION && process.env.SINGLE_DATABASE
|
||||
? {
|
||||
conid: process.env.SINGLE_CONNECTION,
|
||||
database: process.env.SINGLE_DATABASE,
|
||||
}
|
||||
: null;
|
||||
|
||||
return {
|
||||
runAsPortal: !!process.env.CONNECTIONS,
|
||||
toolbar,
|
||||
startupPages,
|
||||
// toolbar,
|
||||
// startupPages,
|
||||
singleDatabase,
|
||||
permissions,
|
||||
...currentVersion,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ module.exports = {
|
||||
raw: true,
|
||||
},
|
||||
test(req, res) {
|
||||
const subprocess = fork(process.argv[1], ['connectProcess']);
|
||||
const subprocess = fork(process.argv[1], ['connectProcess', ...process.argv.slice(3)]);
|
||||
subprocess.on('message', (resp) => {
|
||||
// @ts-ignore
|
||||
const { msgtype } = resp;
|
||||
|
||||
@@ -7,7 +7,7 @@ const { DatabaseAnalyser } = require('dbgate-tools');
|
||||
module.exports = {
|
||||
/** @type {import('dbgate-types').OpenedDatabaseConnection[]} */
|
||||
opened: [],
|
||||
closed: [],
|
||||
closed: {},
|
||||
requests: {},
|
||||
|
||||
handle_structure(conid, database, { structure }) {
|
||||
@@ -28,6 +28,7 @@ module.exports = {
|
||||
handle_status(conid, database, { status }) {
|
||||
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
if (!existing) return;
|
||||
if (existing.status == status) return;
|
||||
existing.status = status;
|
||||
socket.emitChanged(`database-status-changed-${conid}-${database}`);
|
||||
},
|
||||
@@ -38,8 +39,8 @@ module.exports = {
|
||||
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['databaseConnectionProcess']);
|
||||
const lastClosed = this.closed.find((x) => x.conid == conid && x.database == database);
|
||||
const subprocess = fork(process.argv[1], ['databaseConnectionProcess', ...process.argv.slice(3)]);
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
@@ -89,6 +90,8 @@ module.exports = {
|
||||
async status({ conid, database }) {
|
||||
const existing = this.opened.find((x) => x.conid == conid && x.database == database);
|
||||
if (existing) return existing.status;
|
||||
const lastClosed = this.closed[`${conid}/${database}`];
|
||||
if (lastClosed) return lastClosed.status;
|
||||
return {
|
||||
name: 'error',
|
||||
message: 'Not connected',
|
||||
@@ -118,7 +121,7 @@ module.exports = {
|
||||
existing.disconnected = true;
|
||||
if (kill) existing.subprocess.kill();
|
||||
this.opened = this.opened.filter((x) => x.conid != conid || x.database != database);
|
||||
this.closed[conid] = {
|
||||
this.closed[`${conid}/${database}`] = {
|
||||
status: {
|
||||
...existing.status,
|
||||
name: 'error',
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { filesdir } = require('../utility/directories');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
const socket = require('../utility/socket');
|
||||
const scheduler = require('./scheduler');
|
||||
|
||||
function serialize(format, data) {
|
||||
if (format == 'text') return data;
|
||||
if (format == 'json') return JSON.stringify(data);
|
||||
throw new Error(`Invalid format: ${format}`);
|
||||
}
|
||||
|
||||
function deserialize(format, text) {
|
||||
if (format == 'text') return text;
|
||||
if (format == 'json') return JSON.parse(text);
|
||||
throw new Error(`Invalid format: ${format}`);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
list_meta: 'get',
|
||||
async list({ folder }) {
|
||||
if (!hasPermission(`files/${folder}/read`)) return [];
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = (await fs.readdir(dir)).map((file) => ({ folder, file }));
|
||||
return files;
|
||||
},
|
||||
|
||||
listAll_meta: 'get',
|
||||
async listAll() {
|
||||
const folders = await fs.readdir(filesdir());
|
||||
const res = [];
|
||||
for (const folder of folders) {
|
||||
if (!hasPermission(`files/${folder}/read`)) continue;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
const files = (await fs.readdir(dir)).map((file) => ({ folder, file }));
|
||||
res.push(...files);
|
||||
}
|
||||
return res;
|
||||
},
|
||||
|
||||
delete_meta: 'post',
|
||||
async delete({ folder, file }) {
|
||||
if (!hasPermission(`files/${folder}/write`)) return;
|
||||
await fs.unlink(path.join(filesdir(), folder, file));
|
||||
socket.emitChanged(`files-changed-${folder}`);
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
},
|
||||
|
||||
rename_meta: 'post',
|
||||
async rename({ folder, file, newFile }) {
|
||||
if (!hasPermission(`files/${folder}/write`)) return;
|
||||
await fs.rename(path.join(filesdir(), folder, file), path.join(filesdir(), folder, newFile));
|
||||
socket.emitChanged(`files-changed-${folder}`);
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
},
|
||||
|
||||
load_meta: 'post',
|
||||
async load({ folder, file, format }) {
|
||||
if (!hasPermission(`files/${folder}/read`)) return null;
|
||||
const text = await fs.readFile(path.join(filesdir(), folder, file), { encoding: 'utf-8' });
|
||||
return deserialize(format, text);
|
||||
},
|
||||
|
||||
save_meta: 'post',
|
||||
async save({ folder, file, data, format }) {
|
||||
if (!hasPermission(`files/${folder}/write`)) return;
|
||||
const dir = path.join(filesdir(), folder);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await fs.mkdir(dir);
|
||||
}
|
||||
await fs.writeFile(path.join(dir, file), serialize(format, data));
|
||||
socket.emitChanged(`files-changed-${folder}`);
|
||||
socket.emitChanged(`all-files-changed`);
|
||||
if (folder == 'shell') {
|
||||
scheduler.reload();
|
||||
}
|
||||
},
|
||||
|
||||
favorites_meta: 'get',
|
||||
async favorites() {
|
||||
if (!hasPermission(`files/favorites/read`)) return [];
|
||||
const dir = path.join(filesdir(), 'favorites');
|
||||
if (!(await fs.exists(dir))) return [];
|
||||
const files = await fs.readdir(dir);
|
||||
const res = [];
|
||||
for (const file of files) {
|
||||
const filePath = path.join(dir, file);
|
||||
const text = await fs.readFile(filePath, { encoding: 'utf-8' });
|
||||
res.push({
|
||||
file,
|
||||
folder: 'favorites',
|
||||
...JSON.parse(text),
|
||||
});
|
||||
}
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -1,10 +1,13 @@
|
||||
const fs = require('fs-extra');
|
||||
const axios = require('axios');
|
||||
const path = require('path');
|
||||
const { extractPackageName } = require('dbgate-tools');
|
||||
const { pluginsdir, datadir } = require('../utility/directories');
|
||||
const socket = require('../utility/socket');
|
||||
const compareVersions = require('compare-versions');
|
||||
const requirePlugin = require('../shell/requirePlugin');
|
||||
const downloadPackage = require('../utility/downloadPackage');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
|
||||
// async function loadPackageInfo(dir) {
|
||||
// const readmeFile = path.join(dir, 'README.md');
|
||||
@@ -24,13 +27,13 @@ const downloadPackage = require('../utility/downloadPackage');
|
||||
// };
|
||||
// }
|
||||
|
||||
const preinstallPlugins = [
|
||||
'dbgate-plugin-mssql',
|
||||
'dbgate-plugin-mysql',
|
||||
'dbgate-plugin-postgres',
|
||||
'dbgate-plugin-csv',
|
||||
'dbgate-plugin-excel',
|
||||
];
|
||||
const preinstallPluginMinimalVersions = {
|
||||
'dbgate-plugin-mssql': '1.0.8',
|
||||
'dbgate-plugin-mysql': '1.0.2',
|
||||
'dbgate-plugin-postgres': '1.0.2',
|
||||
'dbgate-plugin-csv': '1.0.8',
|
||||
'dbgate-plugin-excel': '1.0.6',
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
script_meta: 'get',
|
||||
@@ -104,22 +107,42 @@ module.exports = {
|
||||
// );
|
||||
},
|
||||
|
||||
async saveRemovePlugins() {
|
||||
await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
|
||||
},
|
||||
|
||||
install_meta: 'post',
|
||||
async install({ packageName }) {
|
||||
if (!hasPermission(`plugins/install`)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
if (!(await fs.exists(dir))) {
|
||||
await downloadPackage(packageName, dir);
|
||||
}
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
this.removedPlugins = this.removedPlugins.filter((x) => x != packageName);
|
||||
await this.saveRemovePlugins();
|
||||
},
|
||||
|
||||
uninstall_meta: 'post',
|
||||
async uninstall({ packageName }) {
|
||||
if (!hasPermission(`plugins/install`)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
this.removedPlugins.push(packageName);
|
||||
await fs.writeFile(path.join(datadir(), 'removed-plugins'), this.removedPlugins.join('\n'));
|
||||
await this.saveRemovePlugins();
|
||||
},
|
||||
|
||||
upgrade_meta: 'post',
|
||||
async upgrade({ packageName }) {
|
||||
if (!hasPermission(`plugins/install`)) return;
|
||||
const dir = path.join(pluginsdir(), packageName);
|
||||
if (await fs.exists(dir)) {
|
||||
await fs.rmdir(dir, { recursive: true });
|
||||
await downloadPackage(packageName, dir);
|
||||
}
|
||||
|
||||
socket.emitChanged(`installed-plugins-changed`);
|
||||
},
|
||||
|
||||
command_meta: 'post',
|
||||
@@ -128,6 +151,14 @@ module.exports = {
|
||||
return content.commands[command](args);
|
||||
},
|
||||
|
||||
authTypes_meta: 'post',
|
||||
async authTypes({ engine }) {
|
||||
const packageName = extractPackageName(engine);
|
||||
const content = requirePlugin(packageName);
|
||||
if (!content.driver || content.driver.engine != engine || !content.driver.getAuthTypes) return null;
|
||||
return content.driver.getAuthTypes() || null;
|
||||
},
|
||||
|
||||
async _init() {
|
||||
const installed = await this.installed();
|
||||
try {
|
||||
@@ -137,8 +168,21 @@ module.exports = {
|
||||
} catch (err) {
|
||||
this.removedPlugins = [];
|
||||
}
|
||||
for (const packageName of preinstallPlugins) {
|
||||
for (const packageName of Object.keys(preinstallPluginMinimalVersions)) {
|
||||
if (this.removedPlugins.includes(packageName)) continue;
|
||||
const installedVersion = installed.find((x) => x.name == packageName);
|
||||
if (installedVersion) {
|
||||
const requiredVersion = preinstallPluginMinimalVersions[packageName];
|
||||
if (compareVersions(installedVersion.version, requiredVersion) < 0) {
|
||||
console.log(
|
||||
`Upgrading preinstalled plugin, found ${installedVersion.version}, required version ${requiredVersion}`,
|
||||
packageName
|
||||
);
|
||||
await this.upgrade({ packageName });
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
console.log('Preinstalling plugin', packageName);
|
||||
await this.install({ packageName });
|
||||
|
||||
@@ -91,7 +91,8 @@ module.exports = {
|
||||
fs.mkdirSync(directory);
|
||||
const pluginNames = fs.readdirSync(pluginsdir());
|
||||
console.log(`RUNNING SCRIPT ${scriptFile}`);
|
||||
const subprocess = fork(scriptFile, ['--checkParent'], {
|
||||
// const subprocess = fork(scriptFile, ['--checkParent', '--max-old-space-size=8192'], {
|
||||
const subprocess = fork(scriptFile, ['--checkParent', ...process.argv.slice(3)], {
|
||||
cwd: directory,
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
||||
env: {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
const { filesdir } = require('../utility/directories');
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const cron = require('node-cron');
|
||||
const runners = require('./runners');
|
||||
const hasPermission = require('../utility/hasPermission');
|
||||
|
||||
const scheduleRegex = /\s*\/\/\s*@schedule\s+([^\n]+)\n/;
|
||||
|
||||
module.exports = {
|
||||
tasks: [],
|
||||
|
||||
async unload() {
|
||||
this.tasks.forEach((x) => x.destroy());
|
||||
this.tasks = [];
|
||||
},
|
||||
|
||||
async processFile(file) {
|
||||
const text = await fs.readFile(file, { encoding: 'utf-8' });
|
||||
const match = text.match(scheduleRegex);
|
||||
if (!match) return;
|
||||
const pattern = match[1];
|
||||
if (!cron.validate(pattern)) return;
|
||||
console.log(`Schedule script ${file} with pattern ${pattern}`);
|
||||
const task = cron.schedule(pattern, () => runners.start({ script: text }));
|
||||
this.tasks.push(task);
|
||||
},
|
||||
|
||||
async reload() {
|
||||
if (!hasPermission('files/shell/read')) return;
|
||||
const shellDir = path.join(filesdir(), 'shell');
|
||||
await this.unload();
|
||||
if (!(await fs.exists(shellDir))) return;
|
||||
const files = await fs.readdir(shellDir);
|
||||
for (const file of files) {
|
||||
await this.processFile(path.join(shellDir, file));
|
||||
}
|
||||
},
|
||||
|
||||
async _init() {
|
||||
this.reload();
|
||||
},
|
||||
};
|
||||
@@ -19,16 +19,13 @@ module.exports = {
|
||||
existing.status = status;
|
||||
socket.emitChanged(`server-status-changed`);
|
||||
},
|
||||
handle_error(conid, { error }) {
|
||||
console.log(`Error in server connection ${conid}: ${error}`);
|
||||
},
|
||||
handle_ping() {},
|
||||
|
||||
async ensureOpened(conid) {
|
||||
const existing = this.opened.find((x) => x.conid == conid);
|
||||
if (existing) return existing;
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['serverConnectionProcess']);
|
||||
const subprocess = fork(process.argv[1], ['serverConnectionProcess', ...process.argv.slice(3)]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
subprocess,
|
||||
|
||||
@@ -64,7 +64,7 @@ module.exports = {
|
||||
async create({ conid, database }) {
|
||||
const sesid = uuidv1();
|
||||
const connection = await connections.get({ conid });
|
||||
const subprocess = fork(process.argv[1], ['sessionProcess']);
|
||||
const subprocess = fork(process.argv[1], ['sessionProcess', ...process.argv.slice(3)]);
|
||||
const newOpened = {
|
||||
conid,
|
||||
database,
|
||||
@@ -95,15 +95,15 @@ module.exports = {
|
||||
return { state: 'ok' };
|
||||
},
|
||||
|
||||
cancel_meta: 'post',
|
||||
async cancel({ sesid }) {
|
||||
const session = this.opened.find((x) => x.sesid == sesid);
|
||||
if (!session) {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
session.subprocess.send({ msgtype: 'cancel' });
|
||||
return { state: 'ok' };
|
||||
},
|
||||
// cancel_meta: 'post',
|
||||
// async cancel({ sesid }) {
|
||||
// const session = this.opened.find((x) => x.sesid == sesid);
|
||||
// if (!session) {
|
||||
// throw new Error('Invalid session');
|
||||
// }
|
||||
// session.subprocess.send({ msgtype: 'cancel' });
|
||||
// return { state: 'ok' };
|
||||
// },
|
||||
|
||||
kill_meta: 'post',
|
||||
async kill({ sesid }) {
|
||||
@@ -112,6 +112,7 @@ module.exports = {
|
||||
throw new Error('Invalid session');
|
||||
}
|
||||
session.subprocess.kill();
|
||||
this.dispatchMessage(sesid, 'Connection closed');
|
||||
return { state: 'ok' };
|
||||
},
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
|
||||
module.exports = {
|
||||
version: '3.8.6',
|
||||
buildTime: '2020-12-10T11:14:01.053Z'
|
||||
};
|
||||
@@ -23,6 +23,8 @@ const config = require('./controllers/config');
|
||||
const archive = require('./controllers/archive');
|
||||
const uploads = require('./controllers/uploads');
|
||||
const plugins = require('./controllers/plugins');
|
||||
const files = require('./controllers/files');
|
||||
const scheduler = require('./controllers/scheduler');
|
||||
|
||||
const { rundir } = require('./utility/directories');
|
||||
|
||||
@@ -67,10 +69,12 @@ function start(argument = null) {
|
||||
useController(app, '/archive', archive);
|
||||
useController(app, '/uploads', uploads);
|
||||
useController(app, '/plugins', plugins);
|
||||
useController(app, '/files', files);
|
||||
useController(app, '/scheduler', scheduler);
|
||||
|
||||
if (process.env.PAGES_DIRECTORY) {
|
||||
app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||
}
|
||||
// if (process.env.PAGES_DIRECTORY) {
|
||||
// app.use('/pages', express.static(process.env.PAGES_DIRECTORY));
|
||||
// }
|
||||
|
||||
app.use('/runners/data', express.static(rundir()));
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
const argIndex = process.argv.indexOf('--native-modules');
|
||||
const redirectFile = argIndex > 0 ? process.argv[argIndex + 1] : null;
|
||||
|
||||
// @ts-ignore
|
||||
module.exports = redirectFile ? __non_webpack_require__(redirectFile) : require('./nativeModulesContent');
|
||||
@@ -38,6 +38,7 @@ async function handleIncrementalRefresh() {
|
||||
analysedStructure = newStructure;
|
||||
process.send({ msgtype: 'structure', structure: analysedStructure });
|
||||
}
|
||||
setStatusName('ok');
|
||||
}
|
||||
|
||||
function setStatus(status) {
|
||||
|
||||
@@ -96,8 +96,11 @@ function start() {
|
||||
process.on('message', async (message) => {
|
||||
try {
|
||||
await handleMessage(message);
|
||||
} catch (e) {
|
||||
process.send({ msgtype: 'error', error: e.message });
|
||||
} catch (err) {
|
||||
setStatus({
|
||||
name: 'error',
|
||||
message: err.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
let systemConnection;
|
||||
let storedConnection;
|
||||
let afterConnectCallbacks = [];
|
||||
let currentHandlers = [];
|
||||
// let currentHandlers = [];
|
||||
|
||||
class TableWriter {
|
||||
constructor(columns, resultIndex) {
|
||||
@@ -64,17 +64,20 @@ class TableWriter {
|
||||
}
|
||||
|
||||
class StreamHandler {
|
||||
constructor(resultIndex) {
|
||||
constructor(resultIndexHolder, resolve) {
|
||||
this.recordset = this.recordset.bind(this);
|
||||
this.row = this.row.bind(this);
|
||||
// this.error = this.error.bind(this);
|
||||
this.done = this.done.bind(this);
|
||||
this.info = this.info.bind(this);
|
||||
// use this for cancelling
|
||||
this.stream = null;
|
||||
|
||||
// use this for cancelling - not implemented
|
||||
// this.stream = null;
|
||||
|
||||
this.plannedStats = false;
|
||||
this.resultIndex = resultIndex;
|
||||
currentHandlers = [...currentHandlers, this];
|
||||
this.resultIndexHolder = resultIndexHolder;
|
||||
this.resolve = resolve;
|
||||
// currentHandlers = [...currentHandlers, this];
|
||||
}
|
||||
|
||||
closeCurrentWriter() {
|
||||
@@ -86,7 +89,8 @@ class StreamHandler {
|
||||
|
||||
recordset(columns) {
|
||||
this.closeCurrentWriter();
|
||||
this.currentWriter = new TableWriter(columns, this.resultIndex);
|
||||
this.currentWriter = new TableWriter(columns, this.resultIndexHolder.value);
|
||||
this.resultIndexHolder.value += 1;
|
||||
|
||||
// this.writeCurrentStats();
|
||||
|
||||
@@ -107,14 +111,21 @@ class StreamHandler {
|
||||
// }
|
||||
done(result) {
|
||||
this.closeCurrentWriter();
|
||||
process.send({ msgtype: 'done', result });
|
||||
currentHandlers = currentHandlers.filter((x) => x != this);
|
||||
// currentHandlers = currentHandlers.filter((x) => x != this);
|
||||
this.resolve();
|
||||
}
|
||||
info(info) {
|
||||
process.send({ msgtype: 'info', info });
|
||||
}
|
||||
}
|
||||
|
||||
function handleStream(driver, resultIndexHolder, sql) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const handler = new StreamHandler(resultIndexHolder, resolve);
|
||||
driver.stream(systemConnection, sql, handler);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleConnect(connection) {
|
||||
storedConnection = connection;
|
||||
|
||||
@@ -126,11 +137,11 @@ async function handleConnect(connection) {
|
||||
afterConnectCallbacks = [];
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
for (const handler of currentHandlers) {
|
||||
if (handler.stream) handler.stream.cancel();
|
||||
}
|
||||
}
|
||||
// function handleCancel() {
|
||||
// for (const handler of currentHandlers) {
|
||||
// if (handler.stream) handler.stream.cancel();
|
||||
// }
|
||||
// }
|
||||
|
||||
function waitConnected() {
|
||||
if (systemConnection) return Promise.resolve();
|
||||
@@ -143,19 +154,23 @@ async function handleExecuteQuery({ sql }) {
|
||||
await waitConnected();
|
||||
const driver = requireEngineDriver(storedConnection);
|
||||
|
||||
let resultIndex = 0;
|
||||
const resultIndexHolder = {
|
||||
value: 0,
|
||||
};
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
const handler = new StreamHandler(resultIndex);
|
||||
const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
handler.stream = stream;
|
||||
resultIndex += 1;
|
||||
await handleStream(driver, resultIndexHolder, sqlItem);
|
||||
// const handler = new StreamHandler(resultIndex);
|
||||
// const stream = await driver.stream(systemConnection, sqlItem, handler);
|
||||
// handler.stream = stream;
|
||||
// resultIndex = handler.resultIndex;
|
||||
}
|
||||
process.send({ msgtype: 'done' });
|
||||
}
|
||||
|
||||
const messageHandlers = {
|
||||
connect: handleConnect,
|
||||
executeQuery: handleExecuteQuery,
|
||||
cancel: handleCancel,
|
||||
// cancel: handleCancel,
|
||||
};
|
||||
|
||||
async function handleMessage({ msgtype, ...other }) {
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
const path = require('path');
|
||||
const uuidv1 = require('uuid/v1');
|
||||
const { uploadsdir } = require('../utility/directories');
|
||||
const { downloadFile } = require('../utility/downloader');
|
||||
|
||||
async function download(url) {
|
||||
if (url && url.match(/(^http:\/\/)|(^https:\/\/)/)) {
|
||||
const tmpFile = path.join(uploadsdir(), uuidv1());
|
||||
await downloadFile(url, tmpFile);
|
||||
return tmpFile;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
module.exports = download;
|
||||
@@ -0,0 +1,19 @@
|
||||
const goSplit = require('../utility/goSplit');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
async function executeQuery({ connection, sql }) {
|
||||
console.log(`Execute query ${sql}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(connection);
|
||||
console.log(`Connected.`);
|
||||
|
||||
for (const sqlItem of goSplit(sql)) {
|
||||
console.log('Executing query', sqlItem);
|
||||
await driver.query(pool, sqlItem);
|
||||
}
|
||||
|
||||
console.log(`Query finished`);
|
||||
}
|
||||
|
||||
module.exports = executeQuery;
|
||||
@@ -14,6 +14,9 @@ const collectorWriter = require('./collectorWriter');
|
||||
const finalizer = require('./finalizer');
|
||||
const registerPlugins = require('./registerPlugins');
|
||||
const requirePlugin = require('./requirePlugin');
|
||||
const download = require('./download');
|
||||
const executeQuery = require('./executeQuery');
|
||||
const loadFile = require('./loadFile');
|
||||
|
||||
const dbgateApi = {
|
||||
queryReader,
|
||||
@@ -30,9 +33,12 @@ const dbgateApi = {
|
||||
archiveReader,
|
||||
collectorWriter,
|
||||
finalizer,
|
||||
download,
|
||||
registerPlugins,
|
||||
executeQuery,
|
||||
loadFile,
|
||||
};
|
||||
|
||||
requirePlugin.initialize(dbgateApi);
|
||||
requirePlugin.initializeDbgateApi(dbgateApi);
|
||||
|
||||
module.exports = dbgateApi;
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
const fs = require('fs-extra');
|
||||
const path = require('path');
|
||||
const { filesdir } = require('../utility/directories');
|
||||
|
||||
async function loadFile(file) {
|
||||
const text = await fs.readFile(path.join(filesdir(), file), { encoding: 'utf-8' });
|
||||
return text;
|
||||
}
|
||||
|
||||
module.exports = loadFile;
|
||||
@@ -1,10 +1,12 @@
|
||||
const path = require('path');
|
||||
const { pluginsdir } = require('../utility/directories');
|
||||
const nativeModules = require('../nativeModules');
|
||||
|
||||
const loadedPlugins = {};
|
||||
|
||||
const dbgateEnv = {
|
||||
dbgateApi: null,
|
||||
nativeModules,
|
||||
};
|
||||
|
||||
function requirePlugin(packageName, requiredPlugin = null) {
|
||||
@@ -30,7 +32,7 @@ function requirePlugin(packageName, requiredPlugin = null) {
|
||||
return requiredPlugin;
|
||||
}
|
||||
|
||||
requirePlugin.initialize = (value) => {
|
||||
requirePlugin.initializeDbgateApi = value => {
|
||||
dbgateEnv.dbgateApi = value;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { quoteFullName } = require('dbgate-tools');
|
||||
const { quoteFullName, fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
async function tableReader({ connection, pureName, schemaName }) {
|
||||
@@ -11,13 +11,15 @@ async function tableReader({ connection, pureName, schemaName }) {
|
||||
const table = await driver.analyseSingleObject(pool, fullName, 'tables');
|
||||
const query = `select * from ${quoteFullName(driver.dialect, fullName)}`;
|
||||
if (table) {
|
||||
console.log(`Reading table ${table.pureName}`);
|
||||
// @ts-ignore
|
||||
console.log(`Reading table ${fullNameToString(table)}`);
|
||||
// @ts-ignore
|
||||
return await driver.readQuery(pool, query, table);
|
||||
}
|
||||
const view = await driver.analyseSingleObject(pool, fullName, 'views');
|
||||
if (view) {
|
||||
console.log(`Reading view ${view.pureName}`);
|
||||
// @ts-ignore
|
||||
console.log(`Reading view ${fullNameToString(view)}`);
|
||||
// @ts-ignore
|
||||
return await driver.readQuery(pool, query, view);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
const requireEngineDriver = require("../utility/requireEngineDriver");
|
||||
const { fullNameToString } = require('dbgate-tools');
|
||||
const requireEngineDriver = require('../utility/requireEngineDriver');
|
||||
|
||||
async function tableWriter({ connection, schemaName, pureName, ...options }) {
|
||||
console.log(`Write table ${schemaName}.${pureName}`);
|
||||
console.log(`Writing table ${fullNameToString({ schemaName, pureName })}`);
|
||||
|
||||
const driver = requireEngineDriver(connection);
|
||||
const pool = await driver.connect(connection);
|
||||
|
||||
@@ -28,7 +28,7 @@ class DatastoreProxy {
|
||||
|
||||
async ensureSubprocess() {
|
||||
if (!this.subprocess) {
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess']);
|
||||
this.subprocess = fork(process.argv[1], ['jslDatastoreProcess', ...process.argv.slice(3)]);
|
||||
|
||||
// @ts-ignore
|
||||
this.subprocess.on('message', ({ msgtype, ...message }) => {
|
||||
|
||||
@@ -37,6 +37,7 @@ const rundir = dirFunc('run', true);
|
||||
const uploadsdir = dirFunc('uploads', true);
|
||||
const pluginsdir = dirFunc('plugins');
|
||||
const archivedir = dirFunc('archive');
|
||||
const filesdir = dirFunc('files');
|
||||
|
||||
module.exports = {
|
||||
datadir,
|
||||
@@ -46,4 +47,5 @@ module.exports = {
|
||||
archivedir,
|
||||
ensureDirectory,
|
||||
pluginsdir,
|
||||
filesdir,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ const zlib = require('zlib');
|
||||
const tar = require('tar');
|
||||
const ncp = require('ncp').ncp;
|
||||
const { uploadsdir } = require('./directories');
|
||||
const { downloadFile } = require('./downloader');
|
||||
|
||||
function extractTarball(tmpFile, destination) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -19,13 +20,6 @@ function extractTarball(tmpFile, destination) {
|
||||
});
|
||||
}
|
||||
|
||||
function saveStreamToFile(pipedStream, fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileStream = fs.createWriteStream(fileName);
|
||||
fileStream.on('close', () => resolve());
|
||||
pipedStream.pipe(fileStream);
|
||||
});
|
||||
}
|
||||
|
||||
function copyDirectory(source, target) {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -46,13 +40,7 @@ async function downloadPackage(packageName, directory) {
|
||||
const tarball = infoResp.data.versions[latest].dist.tarball;
|
||||
|
||||
const tmpFile = path.join(uploadsdir(), uuidv1() + '.tgz');
|
||||
console.log(`Downloading tarball ${tarball} into ${tmpFile}`);
|
||||
const tarballResp = await axios.default({
|
||||
method: 'get',
|
||||
url: tarball,
|
||||
responseType: 'stream',
|
||||
});
|
||||
await saveStreamToFile(tarballResp.data, tmpFile);
|
||||
await downloadFile(tarball, tmpFile);
|
||||
const tmpDir = path.join(uploadsdir(), uuidv1());
|
||||
fs.mkdirSync(tmpDir);
|
||||
await extractTarball(tmpFile, tmpDir);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
|
||||
function saveStreamToFile(pipedStream, fileName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const fileStream = fs.createWriteStream(fileName);
|
||||
fileStream.on('close', () => resolve());
|
||||
pipedStream.pipe(fileStream);
|
||||
});
|
||||
}
|
||||
|
||||
async function downloadFile(url, file) {
|
||||
console.log(`Downloading ${url} into ${file}`);
|
||||
const tarballResp = await axios.default({
|
||||
method: 'get',
|
||||
url,
|
||||
responseType: 'stream',
|
||||
});
|
||||
await saveStreamToFile(tarballResp.data, file);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
saveStreamToFile,
|
||||
downloadFile,
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
const { compilePermissions, testPermission } = require('dbgate-tools');
|
||||
|
||||
let compiled = undefined;
|
||||
|
||||
function hasPermission(tested) {
|
||||
if (compiled === undefined) {
|
||||
compiled = compilePermissions(process.env.PERMISSIONS);
|
||||
}
|
||||
return testPermission(tested, compiled);
|
||||
}
|
||||
|
||||
module.exports = hasPermission;
|
||||
@@ -8,24 +8,27 @@ var config = {
|
||||
app: './index.js',
|
||||
},
|
||||
target: 'node',
|
||||
node: {
|
||||
__dirname: false,
|
||||
},
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
},
|
||||
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
// },
|
||||
// optimization: {
|
||||
// minimize: false,
|
||||
// },
|
||||
|
||||
// module: {
|
||||
// rules: [
|
||||
// {
|
||||
// test: /\.js$/,
|
||||
// exclude: /node_modules/
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.node$/,
|
||||
use: 'node-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.IgnorePlugin({
|
||||
checkResource(resource) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import _ from 'lodash';
|
||||
import { GridConfig, GridCache, GridConfigColumns, createGridCache, GroupFunc } from './GridConfig';
|
||||
import { ForeignKeyInfo, TableInfo, ColumnInfo, EngineDriver, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { parseFilter, getFilterType, getFilterValueExpression } from 'dbgate-filterparser';
|
||||
import { filterName } from './filterName';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
import { Expression, Select, treeToSql, dumpSqlSelect, Condition } from 'dbgate-sqltree';
|
||||
import { isTypeLogical } from 'dbgate-tools';
|
||||
import { ChangeCacheFunc, ChangeConfigFunc, DisplayColumn } from './GridDisplay';
|
||||
|
||||
export class FormViewDisplay {
|
||||
isLoadedCorrectly = true;
|
||||
columns: DisplayColumn[];
|
||||
public baseTable: TableInfo;
|
||||
|
||||
constructor(
|
||||
public config: GridConfig,
|
||||
protected setConfig: ChangeConfigFunc,
|
||||
public cache: GridCache,
|
||||
protected setCache: ChangeCacheFunc,
|
||||
public driver?: EngineDriver,
|
||||
public dbinfo: DatabaseInfo = null
|
||||
) {}
|
||||
|
||||
addFilterColumn(column) {
|
||||
if (!column) return;
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
formFilterColumns: [...(cfg.formFilterColumns || []), column.uniqueName],
|
||||
}));
|
||||
}
|
||||
|
||||
filterCellValue(column, rowData) {
|
||||
if (!column || !rowData) return;
|
||||
const value = rowData[column.uniqueName];
|
||||
const expr = getFilterValueExpression(value, column.dataType);
|
||||
if (expr) {
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
filters: {
|
||||
...cfg.filters,
|
||||
[column.uniqueName]: expr,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
setFilter(uniqueName, value) {
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
filters: {
|
||||
...cfg.filters,
|
||||
[uniqueName]: value,
|
||||
},
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
removeFilter(uniqueName) {
|
||||
const reloadRequired = !!this.config.filters[uniqueName];
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
formFilterColumns: (cfg.formFilterColumns || []).filter((x) => x != uniqueName),
|
||||
filters: _.omit(cfg.filters || [], uniqueName),
|
||||
}));
|
||||
if (reloadRequired) this.reload();
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.setCache((cache) => ({
|
||||
// ...cache,
|
||||
...createGridCache(),
|
||||
refreshTime: new Date().getTime(),
|
||||
}));
|
||||
}
|
||||
|
||||
getKeyValue(columnName) {
|
||||
const { formViewKey, formViewKeyRequested } = this.config;
|
||||
if (formViewKeyRequested && formViewKeyRequested[columnName]) return formViewKeyRequested[columnName];
|
||||
if (formViewKey && formViewKey[columnName]) return formViewKey[columnName];
|
||||
return null;
|
||||
}
|
||||
|
||||
requestKeyValue(columnName, value) {
|
||||
if (this.getKeyValue(columnName) == value) return;
|
||||
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
formViewKeyRequested: {
|
||||
...cfg.formViewKey,
|
||||
...cfg.formViewKeyRequested,
|
||||
[columnName]: value,
|
||||
},
|
||||
}));
|
||||
this.reload();
|
||||
}
|
||||
|
||||
extractKey(row) {
|
||||
if (!row || !this.baseTable || !this.baseTable.primaryKey) {
|
||||
return null;
|
||||
}
|
||||
const formViewKey = _.pick(
|
||||
row,
|
||||
this.baseTable.primaryKey.columns.map((x) => x.columnName)
|
||||
);
|
||||
return formViewKey;
|
||||
}
|
||||
|
||||
cancelRequestKey(rowData) {
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
formViewKeyRequested: null,
|
||||
formViewKey: rowData ? this.extractKey(rowData) : cfg.formViewKey,
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,10 @@ export interface GridConfig extends GridConfigColumns {
|
||||
grouping: { [uniqueName: string]: GroupFunc };
|
||||
childConfig?: GridConfig;
|
||||
reference?: GridReferenceDefinition;
|
||||
isFormView?: boolean;
|
||||
formViewKey?: { [uniqueName: string]: string };
|
||||
formViewKeyRequested?: { [uniqueName: string]: string };
|
||||
formFilterColumns: string[];
|
||||
}
|
||||
|
||||
export interface GridCache {
|
||||
@@ -45,6 +49,7 @@ export function createGridConfig(): GridConfig {
|
||||
sort: [],
|
||||
focusedColumn: null,
|
||||
grouping: {},
|
||||
formFilterColumns: [],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -433,9 +433,10 @@ export abstract class GridDisplay {
|
||||
return sql;
|
||||
}
|
||||
|
||||
getExportQuery() {
|
||||
getExportQuery(postprocessSelect = null) {
|
||||
const select = this.createSelect({ isExport: true });
|
||||
if (!select) return null;
|
||||
if (postprocessSelect) postprocessSelect(select);
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
@@ -517,4 +518,23 @@ export abstract class GridDisplay {
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
switchToFormView(rowData) {
|
||||
if (!this.baseTable) return;
|
||||
const { primaryKey } = this.baseTable;
|
||||
if (!primaryKey) return;
|
||||
const { columns } = primaryKey;
|
||||
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
isFormView: true,
|
||||
formViewKey: rowData
|
||||
? _.pick(
|
||||
rowData,
|
||||
columns.map((x) => x.columnName)
|
||||
)
|
||||
: null,
|
||||
formViewKeyRequested: null,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import { FormViewDisplay } from './FormViewDisplay';
|
||||
import _ from 'lodash';
|
||||
import { GridDisplay, ChangeCacheFunc, DisplayColumn, DisplayedColumnInfo, ChangeConfigFunc } from './GridDisplay';
|
||||
import { TableInfo, EngineDriver, ViewInfo, ColumnInfo, NamedObjectInfo, DatabaseInfo } from 'dbgate-types';
|
||||
import { GridConfig, GridCache, createGridCache } from './GridConfig';
|
||||
import {
|
||||
Expression,
|
||||
Select,
|
||||
treeToSql,
|
||||
dumpSqlSelect,
|
||||
mergeConditions,
|
||||
Condition,
|
||||
OrderByExpression,
|
||||
} from 'dbgate-sqltree';
|
||||
import { filterName } from './filterName';
|
||||
import { TableGridDisplay } from './TableGridDisplay';
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { ChangeSetFieldDefinition, ChangeSetRowDefinition } from './ChangeSet';
|
||||
|
||||
export class TableFormViewDisplay extends FormViewDisplay {
|
||||
// use utility functions from GridDisplay and publish result in FromViewDisplat interface
|
||||
private gridDisplay: TableGridDisplay;
|
||||
|
||||
constructor(
|
||||
public tableName: NamedObjectInfo,
|
||||
driver: EngineDriver,
|
||||
config: GridConfig,
|
||||
setConfig: ChangeConfigFunc,
|
||||
cache: GridCache,
|
||||
setCache: ChangeCacheFunc,
|
||||
dbinfo: DatabaseInfo
|
||||
) {
|
||||
super(config, setConfig, cache, setCache, driver, dbinfo);
|
||||
this.gridDisplay = new TableGridDisplay(tableName, driver, config, setConfig, cache, setCache, dbinfo);
|
||||
|
||||
this.isLoadedCorrectly = this.gridDisplay.isLoadedCorrectly && !!this.driver;
|
||||
this.columns = this.gridDisplay.columns;
|
||||
this.baseTable = this.gridDisplay.baseTable;
|
||||
}
|
||||
|
||||
getPrimaryKeyEqualCondition(row = null): Condition {
|
||||
if (!row) row = this.config.formViewKeyRequested || this.config.formViewKey;
|
||||
if (!row) return null;
|
||||
const { primaryKey } = this.gridDisplay.baseTable;
|
||||
if (!primaryKey) return null;
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: primaryKey.columns.map(({ columnName }) => ({
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
alias: 'basetbl',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: row[columnName],
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
getPrimaryKeyOperatorCondition(operator): Condition {
|
||||
if (!this.config.formViewKey) return null;
|
||||
const conditions = [];
|
||||
|
||||
const { primaryKey } = this.gridDisplay.baseTable;
|
||||
if (!primaryKey) return null;
|
||||
for (let index = 0; index < primaryKey.columns.length; index++) {
|
||||
conditions.push({
|
||||
conditionType: 'and',
|
||||
conditions: [
|
||||
...primaryKey.columns.slice(0, index).map(({ columnName }) => ({
|
||||
conditionType: 'binary',
|
||||
operator: '=',
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
alias: 'basetbl',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: this.config.formViewKey[columnName],
|
||||
},
|
||||
})),
|
||||
...primaryKey.columns.slice(index).map(({ columnName }) => ({
|
||||
conditionType: 'binary',
|
||||
operator: operator,
|
||||
left: {
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
source: {
|
||||
alias: 'basetbl',
|
||||
},
|
||||
},
|
||||
right: {
|
||||
exprType: 'value',
|
||||
value: this.config.formViewKey[columnName],
|
||||
},
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (conditions.length == 1) {
|
||||
return conditions[0];
|
||||
}
|
||||
|
||||
return {
|
||||
conditionType: 'or',
|
||||
conditions,
|
||||
};
|
||||
}
|
||||
|
||||
getSelect() {
|
||||
if (!this.driver) return null;
|
||||
const select = this.gridDisplay.createSelect();
|
||||
if (!select) return null;
|
||||
select.topRecords = 1;
|
||||
return select;
|
||||
}
|
||||
|
||||
getCurrentRowQuery() {
|
||||
const select = this.getSelect();
|
||||
if (!select) return null;
|
||||
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyEqualCondition());
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
getCountSelect() {
|
||||
const select = this.getSelect();
|
||||
if (!select) return null;
|
||||
select.orderBy = null;
|
||||
select.columns = [
|
||||
{
|
||||
exprType: 'raw',
|
||||
sql: 'COUNT(*)',
|
||||
alias: 'count',
|
||||
},
|
||||
];
|
||||
select.topRecords = null;
|
||||
return select;
|
||||
}
|
||||
|
||||
getCountQuery() {
|
||||
if (!this.driver) return null;
|
||||
const select = this.getCountSelect();
|
||||
if (!select) return null;
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
getBeforeCountQuery() {
|
||||
if (!this.driver) return null;
|
||||
const select = this.getCountSelect();
|
||||
if (!select) return null;
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('<'));
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
navigate(row) {
|
||||
const formViewKey = this.extractKey(row);
|
||||
this.setConfig((cfg) => ({
|
||||
...cfg,
|
||||
formViewKey,
|
||||
}));
|
||||
}
|
||||
|
||||
isLoadedCurrentRow(row) {
|
||||
if (!row) return false;
|
||||
const formViewKey = this.extractKey(row);
|
||||
return stableStringify(formViewKey) == stableStringify(this.config.formViewKey);
|
||||
}
|
||||
|
||||
navigateRowQuery(commmand: 'begin' | 'previous' | 'next' | 'end') {
|
||||
if (!this.driver) return null;
|
||||
const select = this.gridDisplay.createSelect();
|
||||
if (!select) return null;
|
||||
const { primaryKey } = this.gridDisplay.baseTable;
|
||||
|
||||
function getOrderBy(direction): OrderByExpression[] {
|
||||
return primaryKey.columns.map(({ columnName }) => ({
|
||||
exprType: 'column',
|
||||
columnName,
|
||||
direction,
|
||||
}));
|
||||
}
|
||||
|
||||
select.topRecords = 1;
|
||||
switch (commmand) {
|
||||
case 'begin':
|
||||
select.orderBy = getOrderBy('ASC');
|
||||
break;
|
||||
case 'end':
|
||||
select.orderBy = getOrderBy('DESC');
|
||||
break;
|
||||
case 'previous':
|
||||
select.orderBy = getOrderBy('DESC');
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('<'));
|
||||
break;
|
||||
case 'next':
|
||||
select.orderBy = getOrderBy('ASC');
|
||||
select.where = mergeConditions(select.where, this.getPrimaryKeyOperatorCondition('>'));
|
||||
break;
|
||||
}
|
||||
|
||||
const sql = treeToSql(this.driver, select, dumpSqlSelect);
|
||||
return sql;
|
||||
}
|
||||
|
||||
getChangeSetRow(row): ChangeSetRowDefinition {
|
||||
if (!this.baseTable) return null;
|
||||
return {
|
||||
pureName: this.baseTable.pureName,
|
||||
schemaName: this.baseTable.schemaName,
|
||||
condition: this.extractKey(row),
|
||||
};
|
||||
}
|
||||
|
||||
getChangeSetField(row, uniqueName): ChangeSetFieldDefinition {
|
||||
const col = this.columns.find((x) => x.uniqueName == uniqueName);
|
||||
if (!col) return null;
|
||||
if (!this.baseTable) return null;
|
||||
if (this.baseTable.pureName != col.pureName || this.baseTable.schemaName != col.schemaName) return null;
|
||||
return {
|
||||
...this.getChangeSetRow(row),
|
||||
uniqueName: uniqueName,
|
||||
columnName: col.columnName,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
export * from "./GridDisplay";
|
||||
export * from "./GridConfig";
|
||||
export * from "./TableGridDisplay";
|
||||
export * from "./ViewGridDisplay";
|
||||
export * from "./JslGridDisplay";
|
||||
export * from "./ChangeSet";
|
||||
export * from "./filterName";
|
||||
export * from "./FreeTableGridDisplay";
|
||||
export * from "./FreeTableModel";
|
||||
export * from "./MacroDefinition";
|
||||
export * from "./runMacro";
|
||||
export * from './GridDisplay';
|
||||
export * from './GridConfig';
|
||||
export * from './TableGridDisplay';
|
||||
export * from './ViewGridDisplay';
|
||||
export * from './JslGridDisplay';
|
||||
export * from './ChangeSet';
|
||||
export * from './filterName';
|
||||
export * from './FreeTableGridDisplay';
|
||||
export * from './FreeTableModel';
|
||||
export * from './MacroDefinition';
|
||||
export * from './runMacro';
|
||||
export * from './FormViewDisplay';
|
||||
export * from './TableFormViewDisplay';
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-sqlitree-v
|
||||
version-git-message "packages-sqlitree v%s"
|
||||
@@ -8,7 +8,7 @@ dbgate-sqltree hold query definition in RAW JSON objects.
|
||||
|
||||
```javascript
|
||||
const { treeToSql, dumpSqlSelect } = require("dbgate-sqltree");
|
||||
const engines = require("dbgate-engines");
|
||||
const dbgatePluginMysql = require("dbgate-plugin-mysql");
|
||||
|
||||
const select = {
|
||||
commandType: "select",
|
||||
@@ -32,7 +32,7 @@ const select = {
|
||||
],
|
||||
};
|
||||
|
||||
const sql = treeToSql(engines("mysql"), select, dumpSqlSelect);
|
||||
const sql = treeToSql(dbgatePluginMysql.driver, select, dumpSqlSelect);
|
||||
console.log("Generated query:", sql);
|
||||
|
||||
```
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.3",
|
||||
"version": "1.0.4",
|
||||
"name": "dbgate-sqltree",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
|
||||
@@ -7,7 +7,7 @@ import { dumpSqlCondition } from './dumpSqlCondition';
|
||||
export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||
dmp.put('^select ');
|
||||
if (cmd.topRecords) {
|
||||
dmp.put('^top %s ', cmd.topRecords);
|
||||
if (!dmp.dialect.rangeSelect || dmp.dialect.offsetFetchRangeSyntax) dmp.put('^top %s ', cmd.topRecords);
|
||||
}
|
||||
if (cmd.distinct) {
|
||||
dmp.put('^distinct ');
|
||||
@@ -36,6 +36,11 @@ export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||
dmp.putCollection(', ', cmd.groupBy, (expr) => dumpSqlExpression(dmp, expr));
|
||||
dmp.put('&n');
|
||||
}
|
||||
if (cmd.having) {
|
||||
dmp.put('&n^having ');
|
||||
dumpSqlCondition(dmp, cmd.having);
|
||||
dmp.put('&n');
|
||||
}
|
||||
if (cmd.orderBy) {
|
||||
dmp.put('&n^order ^by ');
|
||||
dmp.putCollection(', ', cmd.orderBy, (expr) => {
|
||||
@@ -51,6 +56,9 @@ export function dumpSqlSelect(dmp: SqlDumper, cmd: Select) {
|
||||
dmp.put('^limit %s ^offset %s ', cmd.range.limit, cmd.range.offset);
|
||||
}
|
||||
}
|
||||
if (cmd.topRecords) {
|
||||
if (dmp.dialect.rangeSelect && !dmp.dialect.offsetFetchRangeSyntax) dmp.put('^limit %s ', cmd.topRecords);
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpSqlUpdate(dmp: SqlDumper, cmd: Update) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { SqlDumper } from 'dbgate-types';
|
||||
import { Condition, BinaryCondition } from './types';
|
||||
import { dumpSqlExpression } from './dumpSqlExpression';
|
||||
import { link } from 'fs';
|
||||
import { dumpSqlSelect } from './dumpSqlCommand';
|
||||
|
||||
export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
switch (condition.conditionType) {
|
||||
@@ -30,7 +31,7 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
break;
|
||||
case 'and':
|
||||
case 'or':
|
||||
dmp.putCollection(` ^${condition.conditionType} `, condition.conditions, cond => {
|
||||
dmp.putCollection(` ^${condition.conditionType} `, condition.conditions, (cond) => {
|
||||
dmp.putRaw('(');
|
||||
dumpSqlCondition(dmp, cond);
|
||||
dmp.putRaw(')');
|
||||
@@ -51,5 +52,15 @@ export function dumpSqlCondition(dmp: SqlDumper, condition: Condition) {
|
||||
dumpSqlCondition(dmp, condition.condition);
|
||||
dmp.put(')');
|
||||
break;
|
||||
case 'exists':
|
||||
dmp.put('^exists (');
|
||||
dumpSqlSelect(dmp, condition.subQuery);
|
||||
dmp.put(')');
|
||||
break;
|
||||
case 'notExists':
|
||||
dmp.put('^not ^exists (');
|
||||
dumpSqlSelect(dmp, condition.subQuery);
|
||||
dmp.put(')');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,8 @@ export function dumpSqlExpression(dmp: SqlDumper, expr: Expression) {
|
||||
break;
|
||||
|
||||
case 'call':
|
||||
dmp.put('%s(%s', expr.func, expr.argsPrefix);
|
||||
dmp.put('%s(', expr.func);
|
||||
if (expr.argsPrefix) dmp.put('%s ', expr.argsPrefix);
|
||||
dmp.putCollection(',', expr.args, (x) => dumpSqlExpression(dmp, x));
|
||||
dmp.put(')');
|
||||
break;
|
||||
|
||||
@@ -42,14 +42,14 @@ export function dumpSqlSourceRef(dmp: SqlDumper, source: Source) {
|
||||
export function dumpSqlRelation(dmp: SqlDumper, from: Relation) {
|
||||
dmp.put('&n %k ', from.joinType);
|
||||
dumpSqlSourceDef(dmp, from);
|
||||
if (from.conditions) {
|
||||
if (from.conditions && from.conditions.length > 0) {
|
||||
dmp.put(' ^on ');
|
||||
dmp.putCollection(' ^and ', from.conditions, cond => dumpSqlCondition(dmp, cond));
|
||||
dmp.putCollection(' ^and ', from.conditions, (cond) => dumpSqlCondition(dmp, cond));
|
||||
}
|
||||
}
|
||||
|
||||
export function dumpSqlFromDefinition(dmp: SqlDumper, from: FromDefinition) {
|
||||
dumpSqlSourceDef(dmp, from);
|
||||
dmp.put(' ');
|
||||
if (from.relations) from.relations.forEach(rel => dumpSqlRelation(dmp, rel));
|
||||
if (from.relations) from.relations.forEach((rel) => dumpSqlRelation(dmp, rel));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Select {
|
||||
orderBy?: OrderByExpression[];
|
||||
groupBy?: Expression[];
|
||||
where?: Condition;
|
||||
having?: Condition;
|
||||
}
|
||||
|
||||
export type UpdateField = Expression & { targetColumn: string };
|
||||
@@ -82,7 +83,23 @@ export interface CompoudCondition {
|
||||
conditions: Condition[];
|
||||
}
|
||||
|
||||
export type Condition = BinaryCondition | NotCondition | TestCondition | CompoudCondition | LikeCondition;
|
||||
export interface ExistsCondition {
|
||||
conditionType: 'exists';
|
||||
subQuery: Select;
|
||||
}
|
||||
export interface NotExistsCondition {
|
||||
conditionType: 'notExists';
|
||||
subQuery: Select;
|
||||
}
|
||||
|
||||
export type Condition =
|
||||
| BinaryCondition
|
||||
| NotCondition
|
||||
| TestCondition
|
||||
| CompoudCondition
|
||||
| LikeCondition
|
||||
| ExistsCondition
|
||||
| NotExistsCondition;
|
||||
|
||||
export interface Source {
|
||||
name?: NamedObjectInfo;
|
||||
@@ -91,10 +108,10 @@ export interface Source {
|
||||
subQueryString?: string;
|
||||
}
|
||||
|
||||
export type JoinType = 'LEFT JOIN' | 'INNER JOIN' | 'RIGHT JOIN';
|
||||
export type JoinType = 'LEFT JOIN' | 'INNER JOIN' | 'RIGHT JOIN' | 'CROSS JOIN';
|
||||
|
||||
export type Relation = Source & {
|
||||
conditions: Condition[];
|
||||
conditions?: Condition[];
|
||||
joinType: JoinType;
|
||||
};
|
||||
export type FromDefinition = Source & { relations?: Relation[] };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { EngineDriver, SqlDumper } from 'dbgate-types';
|
||||
import { Command } from './types';
|
||||
import { Command, Condition } from './types';
|
||||
import { dumpSqlCommand } from './dumpSqlCommand';
|
||||
|
||||
export function treeToSql<T>(driver: EngineDriver, object: T, func: (dmp: SqlDumper, obj: T) => void) {
|
||||
@@ -16,3 +16,30 @@ export function scriptToSql(driver: EngineDriver, script: Command[]): string {
|
||||
}
|
||||
return dmp.s;
|
||||
}
|
||||
|
||||
export function mergeConditions(condition1: Condition, condition2: Condition): Condition {
|
||||
if (!condition1) return condition2;
|
||||
if (!condition2) return condition1;
|
||||
if (condition1.conditionType == 'and' && condition2.conditionType == 'and') {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [...condition1.conditions, ...condition2.conditions],
|
||||
};
|
||||
}
|
||||
if (condition1.conditionType == 'and') {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [...condition1.conditions, condition2],
|
||||
};
|
||||
}
|
||||
if (condition2.conditionType == 'and') {
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [condition1, ...condition2.conditions],
|
||||
};
|
||||
}
|
||||
return {
|
||||
conditionType: 'and',
|
||||
conditions: [condition1, condition2],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-tools-v
|
||||
version-git-message "packages-tools v%s"
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.0.4",
|
||||
"version": "1.0.7",
|
||||
"name": "dbgate-tools",
|
||||
"main": "lib/index.js",
|
||||
"typings": "lib/index.d.ts",
|
||||
@@ -19,7 +19,8 @@
|
||||
"prepare": "yarn build",
|
||||
"build": "tsc",
|
||||
"start": "tsc --watch",
|
||||
"test": "jest"
|
||||
"test": "jest",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"files": [
|
||||
"lib"
|
||||
|
||||
@@ -43,7 +43,7 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
||||
await driver.query(pool, `TRUNCATE TABLE ${fullNameQuoted}`);
|
||||
}
|
||||
|
||||
this.columnNames = _intersection(
|
||||
writable.columnNames = _intersection(
|
||||
structure.columns.map((x) => x.columnName),
|
||||
writable.structure.columns.map((x) => x.columnName)
|
||||
);
|
||||
@@ -56,14 +56,14 @@ export function createBulkInsertStreamBase(driver, stream, pool, name, options):
|
||||
const dmp = driver.createDumper();
|
||||
|
||||
dmp.putRaw(`INSERT INTO ${fullNameQuoted} (`);
|
||||
dmp.putCollection(',', this.columnNames, (col) => dmp.putRaw(driver.dialect.quoteIdentifier(col)));
|
||||
dmp.putCollection(',', writable.columnNames, (col) => dmp.putRaw(driver.dialect.quoteIdentifier(col)));
|
||||
dmp.putRaw(')\n VALUES\n');
|
||||
|
||||
let wasRow = false;
|
||||
for (const row of rows) {
|
||||
if (wasRow) dmp.putRaw(',\n');
|
||||
dmp.putRaw('(');
|
||||
dmp.putCollection(',', this.columnNames, (col) => dmp.putValue(row[col]));
|
||||
dmp.putCollection(',', writable.columnNames, (col) => dmp.putValue(row[col]));
|
||||
dmp.putRaw(')');
|
||||
wasRow = true;
|
||||
}
|
||||
|
||||
@@ -6,3 +6,5 @@ export * from './createBulkInsertStreamBase';
|
||||
export * from './DatabaseAnalyser';
|
||||
export * from './driverBase';
|
||||
export * from './SqlDumper';
|
||||
export * from './testPermission';
|
||||
export * from './splitPostgresQuery';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseInfo, DatabaseInfoObjects } from 'dbgate-types';
|
||||
import { ColumnInfo, DatabaseInfo, DatabaseInfoObjects, TableInfo } from 'dbgate-types';
|
||||
|
||||
export function fullNameFromString(name) {
|
||||
const m = name.match(/\[([^\]]+)\]\.\[([^\]]+)\]/);
|
||||
@@ -45,3 +45,7 @@ export function findObjectLike(
|
||||
// @ts-ignore
|
||||
return dbinfo[objectTypeField].find((x) => equalStringLike(x.pureName, pureName));
|
||||
}
|
||||
|
||||
export function findForeignKeyForColumn(table: TableInfo, column: ColumnInfo) {
|
||||
return (table.foreignKeys || []).find((fk) => fk.columns.find((col) => col.columnName == column.columnName));
|
||||
}
|
||||
|
||||
@@ -18,6 +18,15 @@ export function extractShellApiPlugins(functionName, props): string[] {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function extractPackageName(name): string {
|
||||
if (!name) return null;
|
||||
const nsMatch = name.match(/^([^@]+)@([^@]+)/);
|
||||
if (nsMatch) {
|
||||
return nsMatch[2];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractShellApiFunctionName(functionName) {
|
||||
const nsMatch = functionName.match(/^([^@]+)@([^@]+)/);
|
||||
if (nsMatch) {
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
const SINGLE_QUOTE = "'";
|
||||
const DOUBLE_QUOTE = '"';
|
||||
// const BACKTICK = '`';
|
||||
const DOUBLE_DASH_COMMENT_START = '--';
|
||||
const HASH_COMMENT_START = '#';
|
||||
const C_STYLE_COMMENT_START = '/*';
|
||||
const SEMICOLON = ';';
|
||||
const LINE_FEED = '\n';
|
||||
const DELIMITER_KEYWORD = 'DELIMITER';
|
||||
|
||||
export interface SplitOptions {
|
||||
multipleStatements?: boolean;
|
||||
retainComments?: boolean;
|
||||
}
|
||||
|
||||
interface SqlStatement {
|
||||
value: string;
|
||||
supportMulti: boolean;
|
||||
}
|
||||
|
||||
interface SplitExecutionContext extends Required<SplitOptions> {
|
||||
unread: string;
|
||||
currentDelimiter: string;
|
||||
currentStatement: SqlStatement;
|
||||
output: SqlStatement[];
|
||||
}
|
||||
|
||||
interface FindExpResult {
|
||||
expIndex: number;
|
||||
exp: string | null;
|
||||
nextIndex: number;
|
||||
}
|
||||
|
||||
const regexEscapeSetRegex = /[-/\\^$*+?.()|[\]{}]/g;
|
||||
const singleQuoteStringEndRegex = /(?<!\\)'/;
|
||||
const doubleQuoteStringEndRegex = /(?<!\\)"/;
|
||||
// const backtickQuoteEndRegex = /(?<!`)`(?!`)/;
|
||||
const doubleDashCommentStartRegex = /--[ \f\n\r\t\v]/;
|
||||
const cStyleCommentStartRegex = /\/\*/;
|
||||
const cStyleCommentEndRegex = /(?<!\/)\*\//;
|
||||
const newLineRegex = /(?:[\r\n]+|$)/;
|
||||
const delimiterStartRegex = /(?:^|[\n\r]+)[ \f\t\v]*DELIMITER[ \t]+/i;
|
||||
// Best effort only, unable to find a syntax specification on delimiter
|
||||
const delimiterTokenRegex = /^(?:'(.+)'|"(.+)"|`(.+)`|([^\s]+))/;
|
||||
const semicolonKeyTokenRegex = buildKeyTokenRegex(SEMICOLON);
|
||||
const quoteEndRegexDict: Record<string, RegExp> = {
|
||||
[SINGLE_QUOTE]: singleQuoteStringEndRegex,
|
||||
[DOUBLE_QUOTE]: doubleQuoteStringEndRegex,
|
||||
// [BACKTICK]: backtickQuoteEndRegex,
|
||||
};
|
||||
|
||||
function escapeRegex(value: string): string {
|
||||
return value.replace(regexEscapeSetRegex, '\\$&');
|
||||
}
|
||||
|
||||
function buildKeyTokenRegex(delimiter: string): RegExp {
|
||||
return new RegExp(
|
||||
'(?:' +
|
||||
[
|
||||
escapeRegex(delimiter),
|
||||
SINGLE_QUOTE,
|
||||
DOUBLE_QUOTE,
|
||||
// BACKTICK,
|
||||
doubleDashCommentStartRegex.source,
|
||||
HASH_COMMENT_START,
|
||||
cStyleCommentStartRegex.source,
|
||||
delimiterStartRegex.source,
|
||||
].join('|') +
|
||||
')',
|
||||
'i'
|
||||
);
|
||||
}
|
||||
|
||||
function findExp(content: string, regex: RegExp): FindExpResult {
|
||||
const match = content.match(regex);
|
||||
let result: FindExpResult;
|
||||
if (match?.index !== undefined) {
|
||||
result = {
|
||||
expIndex: match.index,
|
||||
exp: match[0],
|
||||
nextIndex: match.index + match[0].length,
|
||||
};
|
||||
} else {
|
||||
result = {
|
||||
expIndex: -1,
|
||||
exp: null,
|
||||
nextIndex: content.length,
|
||||
};
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function findKeyToken(content: string, currentDelimiter: string): FindExpResult {
|
||||
let regex;
|
||||
if (currentDelimiter === SEMICOLON) {
|
||||
regex = semicolonKeyTokenRegex;
|
||||
} else {
|
||||
regex = buildKeyTokenRegex(currentDelimiter);
|
||||
}
|
||||
return findExp(content, regex);
|
||||
}
|
||||
|
||||
function findEndQuote(content: string, quote: string): FindExpResult {
|
||||
if (!(quote in quoteEndRegexDict)) {
|
||||
throw new TypeError(`Incorrect quote ${quote} supplied`);
|
||||
}
|
||||
return findExp(content, quoteEndRegexDict[quote]);
|
||||
}
|
||||
|
||||
function read(
|
||||
context: SplitExecutionContext,
|
||||
readToIndex: number,
|
||||
nextUnreadIndex?: number,
|
||||
checkSemicolon?: boolean
|
||||
): void {
|
||||
if (checkSemicolon === undefined) {
|
||||
checkSemicolon = true;
|
||||
}
|
||||
const readContent = context.unread.slice(0, readToIndex);
|
||||
if (checkSemicolon && readContent.includes(SEMICOLON)) {
|
||||
context.currentStatement.supportMulti = false;
|
||||
}
|
||||
context.currentStatement.value += readContent;
|
||||
if (nextUnreadIndex !== undefined && nextUnreadIndex > 0) {
|
||||
context.unread = context.unread.slice(nextUnreadIndex);
|
||||
} else {
|
||||
context.unread = context.unread.slice(readToIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function readTillNewLine(context: SplitExecutionContext, checkSemicolon?: boolean): void {
|
||||
const findResult = findExp(context.unread, newLineRegex);
|
||||
read(context, findResult.expIndex, findResult.expIndex, checkSemicolon);
|
||||
}
|
||||
|
||||
function discard(context: SplitExecutionContext, nextUnreadIndex: number): void {
|
||||
if (nextUnreadIndex > 0) {
|
||||
context.unread = context.unread.slice(nextUnreadIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function discardTillNewLine(context: SplitExecutionContext): void {
|
||||
const findResult = findExp(context.unread, newLineRegex);
|
||||
discard(context, findResult.expIndex);
|
||||
}
|
||||
|
||||
function publishStatementInMultiMode(splitOutput: SqlStatement[], currentStatement: SqlStatement): void {
|
||||
if (splitOutput.length === 0) {
|
||||
splitOutput.push({
|
||||
value: '',
|
||||
supportMulti: true,
|
||||
});
|
||||
}
|
||||
const lastSplitResult = splitOutput[splitOutput.length - 1];
|
||||
if (currentStatement.supportMulti) {
|
||||
if (lastSplitResult.supportMulti) {
|
||||
if (lastSplitResult.value !== '' && !lastSplitResult.value.endsWith(LINE_FEED)) {
|
||||
lastSplitResult.value += LINE_FEED;
|
||||
}
|
||||
lastSplitResult.value += currentStatement.value + SEMICOLON;
|
||||
} else {
|
||||
splitOutput.push({
|
||||
value: currentStatement.value + SEMICOLON,
|
||||
supportMulti: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
splitOutput.push({
|
||||
value: currentStatement.value,
|
||||
supportMulti: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function publishStatement(context: SplitExecutionContext): void {
|
||||
const trimmed = context.currentStatement.value.trim();
|
||||
if (trimmed !== '') {
|
||||
if (!context.multipleStatements) {
|
||||
context.output.push({
|
||||
value: trimmed,
|
||||
supportMulti: context.currentStatement.supportMulti,
|
||||
});
|
||||
} else {
|
||||
context.currentStatement.value = trimmed;
|
||||
publishStatementInMultiMode(context.output, context.currentStatement);
|
||||
}
|
||||
}
|
||||
context.currentStatement.value = '';
|
||||
context.currentStatement.supportMulti = true;
|
||||
}
|
||||
|
||||
function handleKeyTokenFindResult(context: SplitExecutionContext, findResult: FindExpResult): void {
|
||||
switch (findResult.exp?.trim()) {
|
||||
case context.currentDelimiter:
|
||||
read(context, findResult.expIndex, findResult.nextIndex);
|
||||
publishStatement(context);
|
||||
break;
|
||||
// case BACKTICK:
|
||||
case SINGLE_QUOTE:
|
||||
case DOUBLE_QUOTE: {
|
||||
read(context, findResult.nextIndex);
|
||||
const findQuoteResult = findEndQuote(context.unread, findResult.exp);
|
||||
read(context, findQuoteResult.nextIndex, undefined, false);
|
||||
break;
|
||||
}
|
||||
case DOUBLE_DASH_COMMENT_START: {
|
||||
if (context.retainComments) {
|
||||
read(context, findResult.nextIndex);
|
||||
readTillNewLine(context, false);
|
||||
} else {
|
||||
read(context, findResult.expIndex, findResult.expIndex + DOUBLE_DASH_COMMENT_START.length);
|
||||
discardTillNewLine(context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case HASH_COMMENT_START: {
|
||||
if (context.retainComments) {
|
||||
read(context, findResult.nextIndex);
|
||||
readTillNewLine(context, false);
|
||||
} else {
|
||||
read(context, findResult.expIndex, findResult.nextIndex);
|
||||
discardTillNewLine(context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case C_STYLE_COMMENT_START: {
|
||||
if (['!', '+'].includes(context.unread[findResult.nextIndex]) || context.retainComments) {
|
||||
// Should not be skipped, see https://dev.mysql.com/doc/refman/5.7/en/comments.html
|
||||
read(context, findResult.nextIndex);
|
||||
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
|
||||
read(context, findCommentResult.nextIndex);
|
||||
} else {
|
||||
read(context, findResult.expIndex, findResult.nextIndex);
|
||||
const findCommentResult = findExp(context.unread, cStyleCommentEndRegex);
|
||||
discard(context, findCommentResult.nextIndex);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case DELIMITER_KEYWORD: {
|
||||
read(context, findResult.expIndex, findResult.nextIndex);
|
||||
// MySQL client will return `DELIMITER cannot contain a backslash character` if backslash is used
|
||||
// Shall we reject backslash as well?
|
||||
const matched = context.unread.match(delimiterTokenRegex);
|
||||
if (matched?.index !== undefined) {
|
||||
context.currentDelimiter = matched[0].trim();
|
||||
discard(context, matched[0].length);
|
||||
}
|
||||
discardTillNewLine(context);
|
||||
break;
|
||||
}
|
||||
case undefined:
|
||||
case null:
|
||||
read(context, findResult.nextIndex);
|
||||
publishStatement(context);
|
||||
break;
|
||||
default:
|
||||
// This should never happen
|
||||
throw new Error(`Unknown token '${findResult.exp ?? '(null)'}'`);
|
||||
}
|
||||
}
|
||||
|
||||
export function splitPostgresQuery(sql: string, options?: SplitOptions): string[] {
|
||||
options = options ?? {};
|
||||
const context: SplitExecutionContext = {
|
||||
multipleStatements: options.multipleStatements ?? false,
|
||||
retainComments: options.retainComments ?? false,
|
||||
unread: sql,
|
||||
currentDelimiter: SEMICOLON,
|
||||
currentStatement: {
|
||||
value: '',
|
||||
supportMulti: true,
|
||||
},
|
||||
output: [],
|
||||
};
|
||||
let findResult: FindExpResult = {
|
||||
expIndex: -1,
|
||||
exp: null,
|
||||
nextIndex: 0,
|
||||
};
|
||||
let lastUnreadLength;
|
||||
do {
|
||||
lastUnreadLength = context.unread.length;
|
||||
findResult = findKeyToken(context.unread, context.currentDelimiter);
|
||||
handleKeyTokenFindResult(context, findResult);
|
||||
// Prevent infinite loop by returning incorrect result
|
||||
if (lastUnreadLength === context.unread.length) {
|
||||
read(context, context.unread.length);
|
||||
}
|
||||
} while (context.unread !== '');
|
||||
publishStatement(context);
|
||||
return context.output.map((v) => v.value);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import _escapeRegExp from 'lodash/escapeRegExp';
|
||||
import _isString from 'lodash/isString';
|
||||
|
||||
export function compilePermissions(permissions: string[] | string) {
|
||||
if (!permissions) return null;
|
||||
if (_isString(permissions)) permissions = permissions.split(',');
|
||||
return permissions.map((x) => new RegExp('^' + _escapeRegExp(x).replace(/\\\*/g, '.*') + '$'));
|
||||
}
|
||||
|
||||
export function testPermission(tested: string, permissions: RegExp[]) {
|
||||
if (!permissions) return true;
|
||||
for (const permission of permissions) {
|
||||
if (tested.match(permission)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
version-tag-prefix packages-types-v
|
||||
version-git-message "packages-types v%s"
|
||||
@@ -7,6 +7,7 @@ Typescript definitions for DbGate app
|
||||
- dumper.d.ts - SQL dumper - dump SQL commands independed on DB engine
|
||||
- engines.d.ts - definition of SQL engine driver
|
||||
- query.d.ts - query results definition
|
||||
- extensions.d.ts - plugin related definitions
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Vendored
+7
@@ -18,6 +18,12 @@ export interface WriteTableOptions {
|
||||
createIfNotExists?: boolean;
|
||||
}
|
||||
|
||||
export interface EngineAuthType {
|
||||
title: string;
|
||||
name: string;
|
||||
disabledFields: string[];
|
||||
}
|
||||
|
||||
export interface EngineDriver {
|
||||
engine: string;
|
||||
title: string;
|
||||
@@ -44,6 +50,7 @@ export interface EngineDriver {
|
||||
analyseIncremental(pool: any, structure: DatabaseInfo): Promise<DatabaseInfo>;
|
||||
dialect: SqlDialect;
|
||||
createDumper(): SqlDumper;
|
||||
getAuthTypes(): EngineAuthType[];
|
||||
|
||||
analyserClass?: any;
|
||||
dumperClass?: any;
|
||||
|
||||
Vendored
+4
-3
@@ -1,4 +1,4 @@
|
||||
import { EngineDriver } from "./engines";
|
||||
import { EngineDriver } from './engines';
|
||||
|
||||
export interface FileFormatDefinition {
|
||||
storageType: string;
|
||||
@@ -7,9 +7,10 @@ export interface FileFormatDefinition {
|
||||
readerFunc?: string;
|
||||
writerFunc?: string;
|
||||
args?: any[];
|
||||
addFilesToSourceList?: (
|
||||
addFileToSourceList?: (
|
||||
file: {
|
||||
full: string;
|
||||
fileName: string;
|
||||
shortName: string;
|
||||
},
|
||||
newSources: string[],
|
||||
newValues: {
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
{
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"name": "dbgate-types",
|
||||
|
||||
"homepage": "https://dbgate.org/",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dbshell/dbgate.git"
|
||||
},
|
||||
},
|
||||
"funding": "https://www.paypal.com/paypalme/JanProchazkaCz/30eur",
|
||||
"author": "Jan Prochazka",
|
||||
"license": "GPL",
|
||||
|
||||
"keywords": [
|
||||
"dbgate"
|
||||
],
|
||||
|
||||
"types": "index.d.ts",
|
||||
"main": "",
|
||||
"typeScriptVersion": "2.8"
|
||||
|
||||
@@ -10,21 +10,25 @@
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"ace-builds": "^1.4.8",
|
||||
"axios": "^0.19.0",
|
||||
"chart.js": "^2.9.4",
|
||||
"compare-versions": "^3.6.0",
|
||||
"cross-env": "^6.0.3",
|
||||
"dbgate-datalib": "^1.0.0",
|
||||
"dbgate-sqltree": "^1.0.0",
|
||||
"dbgate-tools": "^1.0.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"formik": "^2.1.0",
|
||||
"json-stable-stringify": "^1.0.1",
|
||||
"localforage": "^1.9.0",
|
||||
"markdown-to-jsx": "^7.1.0",
|
||||
"randomcolor": "^0.6.2",
|
||||
"react": "^16.12.0",
|
||||
"react-ace": "^8.0.0",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-dropzone": "^11.2.3",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-markdown": "^5.0.3",
|
||||
"react-modal": "^3.11.1",
|
||||
"react-scripts": "3.3.0",
|
||||
"react-select": "^3.1.0",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 182 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 137 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "DbGate",
|
||||
"name": "DbGate database tool",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
|
||||
+21
-21
@@ -5,7 +5,6 @@ import {
|
||||
CurrentWidgetProvider,
|
||||
CurrentDatabaseProvider,
|
||||
OpenedTabsProvider,
|
||||
SavedSqlFilesProvider,
|
||||
OpenedConnectionsProvider,
|
||||
LeftPanelWidthProvider,
|
||||
CurrentArchiveProvider,
|
||||
@@ -18,6 +17,7 @@ import UploadsProvider from './utility/UploadsProvider';
|
||||
import ThemeHelmet from './themes/ThemeHelmet';
|
||||
import PluginsProvider from './plugins/PluginsProvider';
|
||||
import { ExtensionsProvider } from './utility/useExtensions';
|
||||
import { MenuLayerProvider } from './modals/showMenu';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -25,28 +25,28 @@ function App() {
|
||||
<CurrentDatabaseProvider>
|
||||
<SocketProvider>
|
||||
<OpenedTabsProvider>
|
||||
<SavedSqlFilesProvider>
|
||||
<OpenedConnectionsProvider>
|
||||
<LeftPanelWidthProvider>
|
||||
<ConnectionsPinger>
|
||||
<PluginsProvider>
|
||||
<ExtensionsProvider>
|
||||
<ModalLayerProvider>
|
||||
<CurrentArchiveProvider>
|
||||
<CurrentThemeProvider>
|
||||
<UploadsProvider>
|
||||
<OpenedConnectionsProvider>
|
||||
<LeftPanelWidthProvider>
|
||||
<ConnectionsPinger>
|
||||
<PluginsProvider>
|
||||
<ExtensionsProvider>
|
||||
<CurrentArchiveProvider>
|
||||
<CurrentThemeProvider>
|
||||
<UploadsProvider>
|
||||
<ModalLayerProvider>
|
||||
<MenuLayerProvider>
|
||||
<ThemeHelmet />
|
||||
<Screen />
|
||||
</UploadsProvider>
|
||||
</CurrentThemeProvider>
|
||||
</CurrentArchiveProvider>
|
||||
</ModalLayerProvider>
|
||||
</ExtensionsProvider>
|
||||
</PluginsProvider>
|
||||
</ConnectionsPinger>
|
||||
</LeftPanelWidthProvider>
|
||||
</OpenedConnectionsProvider>
|
||||
</SavedSqlFilesProvider>
|
||||
</MenuLayerProvider>
|
||||
</ModalLayerProvider>
|
||||
</UploadsProvider>
|
||||
</CurrentThemeProvider>
|
||||
</CurrentArchiveProvider>
|
||||
</ExtensionsProvider>
|
||||
</PluginsProvider>
|
||||
</ConnectionsPinger>
|
||||
</LeftPanelWidthProvider>
|
||||
</OpenedConnectionsProvider>
|
||||
</OpenedTabsProvider>
|
||||
</SocketProvider>
|
||||
</CurrentDatabaseProvider>
|
||||
|
||||
@@ -15,6 +15,8 @@ import { ModalLayer } from './modals/showModal';
|
||||
import DragAndDropFileTarget from './DragAndDropFileTarget';
|
||||
import { useUploadsZone } from './utility/UploadsProvider';
|
||||
import useTheme from './theme/useTheme';
|
||||
import { MenuLayer } from './modals/showMenu';
|
||||
import ErrorBoundary from './utility/ErrorBoundary';
|
||||
|
||||
const BodyDiv = styled.div`
|
||||
position: fixed;
|
||||
@@ -112,7 +114,9 @@ export default function Screen() {
|
||||
</IconBar>
|
||||
{!!currentWidget && (
|
||||
<LeftPanel theme={theme}>
|
||||
<WidgetContainer />
|
||||
<ErrorBoundary>
|
||||
<WidgetContainer />
|
||||
</ErrorBoundary>
|
||||
</LeftPanel>
|
||||
)}
|
||||
{!!currentWidget && (
|
||||
@@ -132,6 +136,7 @@ export default function Screen() {
|
||||
<StatusBar />
|
||||
</StausBarContainer>
|
||||
<ModalLayer />
|
||||
<MenuLayer />
|
||||
|
||||
<DragAndDropFileTarget inputProps={getInputProps()} isDragActive={isDragActive} />
|
||||
</div>
|
||||
|
||||
@@ -3,19 +3,34 @@ import _ from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
import tabs from './tabs';
|
||||
import { useOpenedTabs } from './utility/globalState';
|
||||
import ErrorBoundary from './utility/ErrorBoundary';
|
||||
|
||||
const TabContainer = styled.div`
|
||||
const TabContainerStyled = styled.div`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
visibility: ${props =>
|
||||
visibility: ${(props) =>
|
||||
// @ts-ignore
|
||||
props.tabVisible ? 'visible' : 'hidden'};
|
||||
`;
|
||||
|
||||
function TabContainer({ TabComponent, ...props }) {
|
||||
const { tabVisible, tabid, toolbarPortalRef } = props;
|
||||
return (
|
||||
// @ts-ignore
|
||||
<TabContainerStyled tabVisible={tabVisible}>
|
||||
<ErrorBoundary>
|
||||
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
|
||||
</ErrorBoundary>
|
||||
</TabContainerStyled>
|
||||
);
|
||||
}
|
||||
|
||||
const TabContainerMemo = React.memo(TabContainer);
|
||||
|
||||
function createTabComponent(selectedTab) {
|
||||
const TabComponent = tabs[selectedTab.tabComponent];
|
||||
if (TabComponent) {
|
||||
@@ -33,11 +48,19 @@ export default function TabContent({ toolbarPortalRef }) {
|
||||
const [mountedTabs, setMountedTabs] = React.useState({});
|
||||
|
||||
// cleanup closed tabs
|
||||
if (_.difference(_.keys(mountedTabs), _.map(files, 'tabid')).length > 0) {
|
||||
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find(x => x.tabid == k)));
|
||||
if (
|
||||
_.difference(
|
||||
_.keys(mountedTabs),
|
||||
_.map(
|
||||
files.filter((x) => x.closedTime == null),
|
||||
'tabid'
|
||||
)
|
||||
).length > 0
|
||||
) {
|
||||
setMountedTabs(_.pickBy(mountedTabs, (v, k) => files.find((x) => x.tabid == k && x.closedTime == null)));
|
||||
}
|
||||
|
||||
const selectedTab = files.find(x => x.selected);
|
||||
const selectedTab = files.find((x) => x.selected);
|
||||
if (selectedTab) {
|
||||
const { tabid } = selectedTab;
|
||||
if (tabid && !mountedTabs[tabid])
|
||||
@@ -47,14 +70,18 @@ export default function TabContent({ toolbarPortalRef }) {
|
||||
});
|
||||
}
|
||||
|
||||
return _.keys(mountedTabs).map(tabid => {
|
||||
return _.keys(mountedTabs).map((tabid) => {
|
||||
const { TabComponent, props } = mountedTabs[tabid];
|
||||
const tabVisible = tabid == (selectedTab && selectedTab.tabid);
|
||||
return (
|
||||
// @ts-ignore
|
||||
<TabContainer key={tabid} tabVisible={tabVisible}>
|
||||
<TabComponent {...props} tabid={tabid} tabVisible={tabVisible} toolbarPortalRef={toolbarPortalRef} />
|
||||
</TabContainer>
|
||||
<TabContainerMemo
|
||||
key={tabid}
|
||||
{...props}
|
||||
tabid={tabid}
|
||||
tabVisible={tabVisible}
|
||||
toolbarPortalRef={toolbarPortalRef}
|
||||
TabComponent={TabComponent}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ import styled from 'styled-components';
|
||||
import { DropDownMenuItem, DropDownMenuDivider } from './modals/DropDownMenu';
|
||||
|
||||
import { useOpenedTabs, useSetOpenedTabs, useCurrentDatabase, useSetCurrentDatabase } from './utility/globalState';
|
||||
import { showMenu } from './modals/DropDownMenu';
|
||||
import { getConnectionInfo } from './utility/metadataLoaders';
|
||||
import { FontIcon } from './icons';
|
||||
import useTheme from './theme/useTheme';
|
||||
import usePropsCompare from './utility/usePropsCompare';
|
||||
import { useShowMenu } from './modals/showMenu';
|
||||
|
||||
// const files = [
|
||||
// { name: 'app.js' },
|
||||
@@ -126,6 +126,7 @@ function getDbIcon(key) {
|
||||
export default function TabsPanel() {
|
||||
// const formatDbKey = (conid, database) => `${database}-${conid}`;
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
|
||||
const tabs = useOpenedTabs();
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
@@ -4,9 +4,8 @@ import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { FontIcon } from '../icons';
|
||||
import { showMenu } from '../modals/DropDownMenu';
|
||||
import { useShowMenu } from '../modals/showMenu';
|
||||
import useTheme from '../theme/useTheme';
|
||||
import { useSetOpenedTabs, useAppObjectParams } from '../utility/globalState';
|
||||
|
||||
const AppObjectDiv = styled.div`
|
||||
padding: 5px;
|
||||
@@ -18,11 +17,6 @@ const AppObjectDiv = styled.div`
|
||||
font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')};
|
||||
`;
|
||||
|
||||
const AppObjectSpan = styled.span`
|
||||
white-space: nowrap;
|
||||
font-weight: ${(props) => (props.isBold ? 'bold' : 'normal')};
|
||||
`;
|
||||
|
||||
const IconWrap = styled.span`
|
||||
margin-right: 5px;
|
||||
`;
|
||||
@@ -32,49 +26,51 @@ const StatusIconWrap = styled.span`
|
||||
`;
|
||||
|
||||
const ExtInfoWrap = styled.span`
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
font-weight: normal;
|
||||
margin-left: 5px;
|
||||
color: ${(props) => props.theme.left_font3};
|
||||
`;
|
||||
|
||||
export function AppObjectCore({
|
||||
title,
|
||||
icon,
|
||||
Menu,
|
||||
data,
|
||||
makeAppObj,
|
||||
onClick,
|
||||
isBold,
|
||||
isBusy,
|
||||
component = 'div',
|
||||
prefix = null,
|
||||
statusIcon,
|
||||
extInfo,
|
||||
statusTitle,
|
||||
onClick = undefined,
|
||||
onClick2 = undefined,
|
||||
onClick3 = undefined,
|
||||
isBold = undefined,
|
||||
isBusy = undefined,
|
||||
prefix = undefined,
|
||||
statusIcon = undefined,
|
||||
extInfo = undefined,
|
||||
statusTitle = undefined,
|
||||
Menu = undefined,
|
||||
...other
|
||||
}) {
|
||||
const appObjectParams = useAppObjectParams();
|
||||
const theme = useTheme();
|
||||
const showMenu = useShowMenu();
|
||||
|
||||
const handleContextMenu = (event) => {
|
||||
if (!Menu) return;
|
||||
|
||||
event.preventDefault();
|
||||
showMenu(event.pageX, event.pageY, <Menu data={data} makeAppObj={makeAppObj} {...appObjectParams} />);
|
||||
showMenu(event.pageX, event.pageY, <Menu data={data} />);
|
||||
};
|
||||
|
||||
const Component = component == 'div' ? AppObjectDiv : AppObjectSpan;
|
||||
|
||||
let bold = false;
|
||||
if (_.isFunction(isBold)) bold = isBold(appObjectParams);
|
||||
else bold = !!isBold;
|
||||
|
||||
return (
|
||||
<Component
|
||||
<AppObjectDiv
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={onClick ? () => onClick(data) : undefined}
|
||||
isBold={bold}
|
||||
onClick={() => {
|
||||
if (onClick) onClick(data);
|
||||
if (onClick2) onClick2(data);
|
||||
if (onClick3) onClick3(data);
|
||||
}}
|
||||
theme={theme}
|
||||
isBold={isBold}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('app_object_drag_data', JSON.stringify(data));
|
||||
}}
|
||||
{...other}
|
||||
>
|
||||
{prefix}
|
||||
@@ -86,12 +82,6 @@ export function AppObjectCore({
|
||||
</StatusIconWrap>
|
||||
)}
|
||||
{extInfo && <ExtInfoWrap theme={theme}>{extInfo}</ExtInfoWrap>}
|
||||
</Component>
|
||||
</AppObjectDiv>
|
||||
);
|
||||
}
|
||||
|
||||
export function AppObjectControl({ data, makeAppObj, component = 'div' }) {
|
||||
const appObjectParams = useAppObjectParams();
|
||||
const appobj = makeAppObj(data, appObjectParams);
|
||||
return <AppObjectCore {...appobj} data={data} makeAppObj={makeAppObj} component={component} />;
|
||||
}
|
||||
@@ -1,7 +1,5 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { AppObjectCore } from './AppObjects';
|
||||
import { useSetOpenedTabs, useAppObjectParams } from '../utility/globalState';
|
||||
import styled from 'styled-components';
|
||||
import { ExpandIcon } from '../icons';
|
||||
import useTheme from '../theme/useTheme';
|
||||
@@ -31,53 +29,45 @@ const GroupDiv = styled.div`
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, SubItems }) {
|
||||
function AppObjectListItem({
|
||||
AppObjectComponent,
|
||||
data,
|
||||
filter,
|
||||
onObjectClick,
|
||||
isExpandable,
|
||||
SubItems,
|
||||
getCommonProps,
|
||||
}) {
|
||||
const [isExpanded, setIsExpanded] = React.useState(false);
|
||||
const [isHover, setIsHover] = React.useState(false);
|
||||
|
||||
const expandable = data && isExpandable && isExpandable(data);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!appobj.isExpandable) {
|
||||
// if (data._id == '6pOY2iFY8Gsq7mk6') console.log('COLLAPSE1');
|
||||
if (!expandable) {
|
||||
setIsExpanded(false);
|
||||
}
|
||||
}, [appobj && appobj.isExpandable]);
|
||||
}, [expandable]);
|
||||
|
||||
// const { matcher } = appobj;
|
||||
// if (matcher && !matcher(filter)) return null;
|
||||
let commonProps = {
|
||||
prefix: SubItems ? (
|
||||
<ExpandIconHolder2>
|
||||
{expandable ? <ExpandIcon isExpanded={isExpanded} /> : <ExpandIcon isBlank />}
|
||||
</ExpandIconHolder2>
|
||||
) : null,
|
||||
};
|
||||
|
||||
if (onObjectClick)
|
||||
appobj = {
|
||||
...appobj,
|
||||
onClick: onObjectClick,
|
||||
};
|
||||
if (SubItems) {
|
||||
const oldClick = appobj.onClick;
|
||||
appobj = {
|
||||
...appobj,
|
||||
onClick: () => {
|
||||
if (oldClick) oldClick();
|
||||
// if (data._id == '6pOY2iFY8Gsq7mk6') console.log('COLLAPSE2');
|
||||
setIsExpanded((v) => !v);
|
||||
},
|
||||
};
|
||||
commonProps.onClick2 = () => setIsExpanded((v) => !v);
|
||||
}
|
||||
if (onObjectClick) {
|
||||
commonProps.onClick3 = onObjectClick;
|
||||
}
|
||||
|
||||
let res = (
|
||||
<AppObjectCore
|
||||
data={data}
|
||||
makeAppObj={makeAppObj}
|
||||
{...appobj}
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
prefix={
|
||||
SubItems ? (
|
||||
<ExpandIconHolder2>
|
||||
{appobj.isExpandable ? <ExpandIcon isExpanded={isExpanded} /> : <ExpandIcon isBlank />}
|
||||
</ExpandIconHolder2>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
);
|
||||
if (getCommonProps) {
|
||||
commonProps = { ...commonProps, ...getCommonProps(data) };
|
||||
}
|
||||
|
||||
let res = <AppObjectComponent data={data} commonProps={commonProps} />;
|
||||
if (SubItems && isExpanded) {
|
||||
res = (
|
||||
<>
|
||||
@@ -93,16 +83,10 @@ function AppObjectListItem({ makeAppObj, data, filter, appobj, onObjectClick, Su
|
||||
|
||||
function AppObjectGroup({ group, items }) {
|
||||
const [isExpanded, setIsExpanded] = React.useState(true);
|
||||
const [isHover, setIsHover] = React.useState(false);
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<>
|
||||
<GroupDiv
|
||||
onMouseEnter={() => setIsHover(true)}
|
||||
onMouseLeave={() => setIsHover(false)}
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
theme={theme}
|
||||
>
|
||||
<GroupDiv onClick={() => setIsExpanded(!isExpanded)} theme={theme}>
|
||||
<ExpandIconHolder>
|
||||
<ExpandIcon isExpanded={isExpanded} />
|
||||
</ExpandIconHolder>
|
||||
@@ -115,36 +99,36 @@ function AppObjectGroup({ group, items }) {
|
||||
|
||||
export function AppObjectList({
|
||||
list,
|
||||
makeAppObj,
|
||||
AppObjectComponent,
|
||||
SubItems = undefined,
|
||||
onObjectClick = undefined,
|
||||
filter = undefined,
|
||||
groupFunc = undefined,
|
||||
groupOrdered = undefined,
|
||||
isExpandable = undefined,
|
||||
getCommonProps = undefined,
|
||||
}) {
|
||||
const appObjectParams = useAppObjectParams();
|
||||
|
||||
const createComponent = (data, appobj) => (
|
||||
const createComponent = (data) => (
|
||||
<AppObjectListItem
|
||||
key={appobj.key}
|
||||
appobj={appobj}
|
||||
makeAppObj={makeAppObj}
|
||||
key={AppObjectComponent.extractKey(data)}
|
||||
AppObjectComponent={AppObjectComponent}
|
||||
data={data}
|
||||
filter={filter}
|
||||
onObjectClick={onObjectClick}
|
||||
SubItems={SubItems}
|
||||
isExpandable={isExpandable}
|
||||
getCommonProps={getCommonProps}
|
||||
/>
|
||||
);
|
||||
|
||||
if (groupFunc) {
|
||||
const listGrouped = _.compact(
|
||||
(list || []).map((data) => {
|
||||
const appobj = makeAppObj(data, appObjectParams);
|
||||
const { matcher } = appobj;
|
||||
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
|
||||
if (matcher && !matcher(filter)) return null;
|
||||
const component = createComponent(data, appobj);
|
||||
const group = groupFunc(appobj);
|
||||
return { group, appobj, component };
|
||||
const component = createComponent(data);
|
||||
const group = groupFunc(data);
|
||||
return { group, data, component };
|
||||
})
|
||||
);
|
||||
const groups = _.groupBy(listGrouped, 'group');
|
||||
@@ -154,9 +138,8 @@ export function AppObjectList({
|
||||
}
|
||||
|
||||
return (list || []).map((data) => {
|
||||
const appobj = makeAppObj(data, appObjectParams);
|
||||
const { matcher } = appobj;
|
||||
const matcher = AppObjectComponent.createMatcher && AppObjectComponent.createMatcher(data);
|
||||
if (matcher && !matcher(filter)) return null;
|
||||
return createComponent(data, appobj);
|
||||
return createComponent(data);
|
||||
});
|
||||
}
|
||||
|
||||
+21
-17
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import axios from '../utility/axios';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
|
||||
function openArchive(setOpenedTabs, fileName, folderName) {
|
||||
openNewTab(setOpenedTabs, {
|
||||
function openArchive(openNewTab, fileName, folderName) {
|
||||
openNewTab({
|
||||
title: fileName,
|
||||
icon: 'img archive',
|
||||
tooltip: `${folderName}\n${fileName}`,
|
||||
@@ -19,23 +18,24 @@ function openArchive(setOpenedTabs, fileName, folderName) {
|
||||
});
|
||||
}
|
||||
|
||||
function Menu({ data, setOpenedTabs }) {
|
||||
function Menu({ data }) {
|
||||
const openNewTab = useOpenNewTab();
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-file', { file: data.fileName, folder: data.folderName });
|
||||
// setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
||||
};
|
||||
const handleOpenRead = () => {
|
||||
openArchive(setOpenedTabs, data.fileName, data.folderName);
|
||||
openArchive(openNewTab, data.fileName, data.folderName);
|
||||
};
|
||||
const handleOpenWrite = async () => {
|
||||
// const resp = await axios.post('archive/load-free-table', { file: data.fileName, folder: data.folderName });
|
||||
|
||||
openNewTab(setOpenedTabs, {
|
||||
openNewTab({
|
||||
title: data.fileName,
|
||||
icon: 'img archive',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialData: {
|
||||
initialArgs: {
|
||||
functionName: 'archiveReader',
|
||||
props: {
|
||||
fileName: data.fileName,
|
||||
@@ -57,15 +57,19 @@ function Menu({ data, setOpenedTabs }) {
|
||||
);
|
||||
}
|
||||
|
||||
const archiveFileAppObject = () => ({ fileName, folderName }, { setOpenedTabs }) => {
|
||||
const key = fileName;
|
||||
const icon = 'img archive';
|
||||
function ArchiveFileAppObject({ data, commonProps }) {
|
||||
const { fileName, folderName } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const onClick = () => {
|
||||
openArchive(setOpenedTabs, fileName, folderName);
|
||||
openArchive(openNewTab, fileName, folderName);
|
||||
};
|
||||
const matcher = (filter) => filterName(filter, fileName);
|
||||
|
||||
return { title: fileName, key, icon, Menu, onClick, matcher };
|
||||
};
|
||||
return (
|
||||
<AppObjectCore {...commonProps} data={data} title={fileName} icon="img archive" onClick={onClick} Menu={Menu} />
|
||||
);
|
||||
}
|
||||
|
||||
export default archiveFileAppObject;
|
||||
ArchiveFileAppObject.extractKey = (data) => data.fileName;
|
||||
ArchiveFileAppObject.createMatcher = ({ fileName }) => (filter) => filterName(filter, fileName);
|
||||
|
||||
export default ArchiveFileAppObject;
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import axios from '../utility/axios';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import { useCurrentArchive } from '../utility/globalState';
|
||||
|
||||
function Menu({ data }) {
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-folder', { folder: data.name });
|
||||
};
|
||||
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
|
||||
}
|
||||
|
||||
function ArchiveFolderAppObject({ data, commonProps }) {
|
||||
const { name } = data;
|
||||
const currentArchive = useCurrentArchive();
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={name}
|
||||
icon="img archive-folder"
|
||||
isBold={name == currentArchive}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
ArchiveFolderAppObject.extractKey = (data) => data.name;
|
||||
ArchiveFolderAppObject.createMatcher = (data) => (filter) => filterName(filter, data.name);
|
||||
|
||||
export default ArchiveFolderAppObject;
|
||||
+23
-7
@@ -2,8 +2,11 @@ import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
|
||||
function Menu({ data, setOpenedTabs }) {
|
||||
function Menu({ data }) {
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
const handleDelete = () => {
|
||||
setOpenedTabs((tabs) => tabs.filter((x) => x.tabid != data.tabid));
|
||||
};
|
||||
@@ -18,9 +21,9 @@ function Menu({ data, setOpenedTabs }) {
|
||||
);
|
||||
}
|
||||
|
||||
const closedTabAppObject = () => ({ tabid, props, selected, icon, title, closedTime, busy }, { setOpenedTabs }) => {
|
||||
const key = tabid;
|
||||
const isBold = !!selected;
|
||||
function ClosedTabAppObject({ data, commonProps }) {
|
||||
const { tabid, props, selected, icon, title, closedTime, busy } = data;
|
||||
const setOpenedTabs = useSetOpenedTabs();
|
||||
|
||||
const onClick = () => {
|
||||
setOpenedTabs((files) =>
|
||||
@@ -32,7 +35,20 @@ const closedTabAppObject = () => ({ tabid, props, selected, icon, title, closedT
|
||||
);
|
||||
};
|
||||
|
||||
return { title: `${title} ${moment(closedTime).fromNow()}`, key, icon, isBold, onClick, isBusy: busy, Menu };
|
||||
};
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={`${title} ${moment(closedTime).fromNow()}`}
|
||||
icon={icon}
|
||||
isBold={!!selected}
|
||||
onClick={onClick}
|
||||
isBusy={busy}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default closedTabAppObject;
|
||||
ClosedTabAppObject.extractKey = (data) => data.tabid;
|
||||
|
||||
export default ClosedTabAppObject;
|
||||
+50
-33
@@ -6,8 +6,18 @@ import axios from '../utility/axios';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import ConfirmModal from '../modals/ConfirmModal';
|
||||
import CreateDatabaseModal from '../modals/CreateDatabaseModal';
|
||||
import { useCurrentDatabase, useOpenedConnections, useSetOpenedConnections } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import { useConfig } from '../utility/metadataLoaders';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
|
||||
function Menu({ data }) {
|
||||
const openedConnections = useOpenedConnections();
|
||||
const setOpenedConnections = useSetOpenedConnections();
|
||||
const showModal = useShowModal();
|
||||
const config = useConfig();
|
||||
|
||||
function Menu({ data, setOpenedConnections, openedConnections, config, showModal }) {
|
||||
const handleEdit = () => {
|
||||
showModal((modalState) => <ConnectionModal modalState={modalState} connection={data} />);
|
||||
};
|
||||
@@ -54,49 +64,56 @@ function Menu({ data, setOpenedConnections, openedConnections, config, showModal
|
||||
);
|
||||
}
|
||||
|
||||
const connectionAppObject = (flags) => (
|
||||
{ _id, server, displayName, engine, status },
|
||||
{ openedConnections, setOpenedConnections }
|
||||
) => {
|
||||
const title = displayName || server;
|
||||
const key = _id;
|
||||
const isExpandable = openedConnections.includes(_id);
|
||||
const icon = 'img server';
|
||||
const matcher = (filter) => filterName(filter, displayName, server);
|
||||
const { boldCurrentDatabase } = flags || {};
|
||||
const isBold = boldCurrentDatabase
|
||||
? ({ currentDatabase }) => {
|
||||
return _.get(currentDatabase, 'connection._id') == _id;
|
||||
}
|
||||
: null;
|
||||
function ConnectionAppObject({ data, commonProps }) {
|
||||
const { _id, server, displayName, engine, status } = data;
|
||||
const openedConnections = useOpenedConnections();
|
||||
const setOpenedConnections = useSetOpenedConnections();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
const extensions = useExtensions();
|
||||
|
||||
const isBold = _.get(currentDatabase, 'connection._id') == _id;
|
||||
const onClick = () => setOpenedConnections((c) => [...c, _id]);
|
||||
|
||||
let statusIcon = null;
|
||||
let statusTitle = null;
|
||||
|
||||
let extInfo = null;
|
||||
if (extensions.drivers.find((x) => x.engine == engine)) {
|
||||
const match = (engine || '').match(/^([^@]*)@/);
|
||||
extInfo = match ? match[1] : engine;
|
||||
} else {
|
||||
extInfo = engine;
|
||||
statusIcon = 'img warn';
|
||||
statusTitle = `Engine driver ${engine} not found, review installed plugins and change engine in edit connection dialog`;
|
||||
}
|
||||
|
||||
if (openedConnections.includes(_id)) {
|
||||
if (!status) statusIcon = 'icon loading';
|
||||
else if (status.name == 'pending') statusIcon = 'icon loading';
|
||||
else if (status.name == 'ok') statusIcon = 'img green-ok';
|
||||
else if (status.name == 'ok') statusIcon = 'img ok';
|
||||
else statusIcon = 'img error';
|
||||
if (status && status.name == 'error') {
|
||||
statusTitle = status.message;
|
||||
}
|
||||
}
|
||||
const extInfo = engine;
|
||||
|
||||
return {
|
||||
title,
|
||||
key,
|
||||
icon,
|
||||
Menu,
|
||||
matcher,
|
||||
isBold,
|
||||
isExpandable,
|
||||
onClick,
|
||||
statusIcon,
|
||||
statusTitle,
|
||||
extInfo,
|
||||
};
|
||||
};
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
title={displayName || server}
|
||||
icon="img server"
|
||||
data={data}
|
||||
statusIcon={statusIcon}
|
||||
statusTitle={statusTitle}
|
||||
extInfo={extInfo}
|
||||
isBold={isBold}
|
||||
onClick={onClick}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connectionAppObject;
|
||||
ConnectionAppObject.extractKey = (data) => data._id;
|
||||
ConnectionAppObject.createMatcher = ({ displayName, server }) => (filter) => filterName(filter, displayName, server);
|
||||
|
||||
export default ConnectionAppObject;
|
||||
+33
-21
@@ -1,16 +1,25 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import { getDefaultFileFormat } from '../utility/fileformats';
|
||||
import { useCurrentDatabase } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
|
||||
function Menu({ data, setOpenedTabs, showModal, extensions }) {
|
||||
function Menu({ data }) {
|
||||
const { connection, name } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const extensions = useExtensions();
|
||||
const showModal = useShowModal();
|
||||
|
||||
const tooltip = `${connection.displayName || connection.server}\n${name}`;
|
||||
|
||||
const handleNewQuery = () => {
|
||||
openNewTab(setOpenedTabs, {
|
||||
openNewTab({
|
||||
title: 'Query',
|
||||
icon: 'img sql-file',
|
||||
tooltip,
|
||||
@@ -29,8 +38,8 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
|
||||
initialValues={{
|
||||
sourceStorageType: getDefaultFileFormat(extensions).storageType,
|
||||
targetStorageType: 'database',
|
||||
targetConnectionId: data.connection._id,
|
||||
targetDatabaseName: data.name,
|
||||
targetConnectionId: connection._id,
|
||||
targetDatabaseName: name,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
@@ -43,8 +52,8 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
|
||||
initialValues={{
|
||||
targetStorageType: getDefaultFileFormat(extensions).storageType,
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: data.connection._id,
|
||||
sourceDatabaseName: data.name,
|
||||
sourceConnectionId: connection._id,
|
||||
sourceDatabaseName: name,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
@@ -59,20 +68,23 @@ function Menu({ data, setOpenedTabs, showModal, extensions }) {
|
||||
);
|
||||
}
|
||||
|
||||
const databaseAppObject = (flags) => ({ name, connection }) => {
|
||||
const { boldCurrentDatabase } = flags || {};
|
||||
const title = name;
|
||||
const key = name;
|
||||
const icon = 'img database';
|
||||
const isBold = boldCurrentDatabase
|
||||
? ({ currentDatabase }) => {
|
||||
return (
|
||||
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
|
||||
);
|
||||
function DatabaseAppObject({ data, commonProps }) {
|
||||
const { name, connection } = data;
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={name}
|
||||
icon="img database"
|
||||
isBold={
|
||||
_.get(currentDatabase, 'connection._id') == _.get(connection, '_id') && _.get(currentDatabase, 'name') == name
|
||||
}
|
||||
: null;
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return { title, key, icon, Menu, isBold };
|
||||
};
|
||||
DatabaseAppObject.extractKey = (props) => props.name;
|
||||
|
||||
export default databaseAppObject;
|
||||
export default DatabaseAppObject;
|
||||
@@ -0,0 +1,308 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
import { useSetOpenedTabs } from '../utility/globalState';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import { findEngineDriver } from 'dbgate-tools';
|
||||
import useExtensions from '../utility/useExtensions';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import uuidv1 from 'uuid/v1';
|
||||
|
||||
const icons = {
|
||||
tables: 'img table',
|
||||
views: 'img view',
|
||||
procedures: 'img procedure',
|
||||
functions: 'img function',
|
||||
};
|
||||
|
||||
const menus = {
|
||||
tables: [
|
||||
{
|
||||
label: 'Open data',
|
||||
tab: 'TableDataTab',
|
||||
forceNewTab: true,
|
||||
},
|
||||
{
|
||||
label: 'Open form',
|
||||
tab: 'TableDataTab',
|
||||
forceNewTab: true,
|
||||
initialData: {
|
||||
grid: {
|
||||
isFormView: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Open structure',
|
||||
tab: 'TableStructureTab',
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE TABLE script',
|
||||
sqlTemplate: 'CREATE TABLE',
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open active chart',
|
||||
isActiveChart: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
],
|
||||
views: [
|
||||
{
|
||||
label: 'Open data',
|
||||
tab: 'ViewDataTab',
|
||||
forceNewTab: true,
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE VIEW script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE TABLE script',
|
||||
sqlTemplate: 'CREATE TABLE',
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open structure',
|
||||
tab: 'TableStructureTab',
|
||||
},
|
||||
{
|
||||
label: 'Open active chart',
|
||||
isActiveChart: true,
|
||||
},
|
||||
{
|
||||
label: 'Query designer',
|
||||
isQueryDesigner: true,
|
||||
},
|
||||
],
|
||||
procedures: [
|
||||
{
|
||||
label: 'Show CREATE PROCEDURE script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'Show EXECUTE script',
|
||||
sqlTemplate: 'EXECUTE PROCEDURE',
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
label: 'Show CREATE FUNCTION script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
views: 'ViewDataTab',
|
||||
};
|
||||
|
||||
export async function openDatabaseObjectDetail(
|
||||
openNewTab,
|
||||
tabComponent,
|
||||
sqlTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField },
|
||||
forceNewTab,
|
||||
initialData
|
||||
) {
|
||||
const connection = await getConnectionInfo({ conid });
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
|
||||
schemaName,
|
||||
pureName,
|
||||
})}`;
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: pureName,
|
||||
tooltip,
|
||||
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
|
||||
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
||||
},
|
||||
},
|
||||
initialData,
|
||||
{ forceNewTab }
|
||||
);
|
||||
}
|
||||
|
||||
function Menu({ data }) {
|
||||
const showModal = useShowModal();
|
||||
const openNewTab = useOpenNewTab();
|
||||
const extensions = useExtensions();
|
||||
|
||||
const getDriver = async () => {
|
||||
const conn = await getConnectionInfo(data);
|
||||
if (!conn) return;
|
||||
const driver = findEngineDriver(conn, extensions);
|
||||
return driver;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{menus[data.objectTypeField].map((menu) => (
|
||||
<DropDownMenuItem
|
||||
key={menu.label}
|
||||
onClick={async () => {
|
||||
if (menu.isExport) {
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
modalState={modalState}
|
||||
initialValues={{
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: data.conid,
|
||||
sourceDatabaseName: data.database,
|
||||
sourceSchemaName: data.schemaName,
|
||||
sourceList: [data.pureName],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
} else if (menu.isOpenFreeTable) {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
openNewTab({
|
||||
title: data.pureName,
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialArgs: {
|
||||
functionName: 'tableReader',
|
||||
props: {
|
||||
connection: {
|
||||
...coninfo,
|
||||
database: data.database,
|
||||
},
|
||||
schemaName: data.schemaName,
|
||||
pureName: data.pureName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else if (menu.isActiveChart) {
|
||||
const driver = await getDriver();
|
||||
const dmp = driver.createDumper();
|
||||
dmp.put('^select * from %f', data);
|
||||
openNewTab(
|
||||
{
|
||||
title: data.pureName,
|
||||
icon: 'img chart',
|
||||
tabComponent: 'ChartTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
config: { chartType: 'bar' },
|
||||
sql: dmp.s,
|
||||
},
|
||||
}
|
||||
);
|
||||
} else if (menu.isQueryDesigner) {
|
||||
openNewTab(
|
||||
{
|
||||
title: data.pureName,
|
||||
icon: 'img query-design',
|
||||
tabComponent: 'QueryDesignTab',
|
||||
props: {
|
||||
conid: data.conid,
|
||||
database: data.database,
|
||||
},
|
||||
},
|
||||
{
|
||||
editor: {
|
||||
tables: [
|
||||
{
|
||||
...data,
|
||||
designerId: uuidv1(),
|
||||
left: 50,
|
||||
top: 50,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
);
|
||||
} else {
|
||||
openDatabaseObjectDetail(
|
||||
openNewTab,
|
||||
menu.tab,
|
||||
menu.sqlTemplate,
|
||||
data,
|
||||
menu.forceNewTab,
|
||||
menu.initialData
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{menu.label}
|
||||
</DropDownMenuItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DatabaseObjectAppObject({ data, commonProps }) {
|
||||
const { conid, database, pureName, schemaName, objectTypeField } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const onClick = ({ schemaName, pureName }) => {
|
||||
openDatabaseObjectDetail(
|
||||
openNewTab,
|
||||
defaultTabs[objectTypeField],
|
||||
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
|
||||
{
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
},
|
||||
false
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={schemaName ? `${schemaName}.${pureName}` : pureName}
|
||||
icon={icons[objectTypeField]}
|
||||
onClick={onClick}
|
||||
Menu={Menu}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
DatabaseObjectAppObject.extractKey = ({ schemaName, pureName }) =>
|
||||
schemaName ? `${schemaName}.${pureName}` : pureName;
|
||||
|
||||
DatabaseObjectAppObject.createMatcher = ({ pureName }) => (filter) => filterName(filter, pureName);
|
||||
|
||||
export default DatabaseObjectAppObject;
|
||||
@@ -0,0 +1,104 @@
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import FavoriteModal from '../modals/FavoriteModal';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import axios from '../utility/axios';
|
||||
import { copyTextToClipboard } from '../utility/clipboard';
|
||||
import getElectron from '../utility/getElectron';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import { SavedFileAppObjectBase } from './SavedFileAppObject';
|
||||
|
||||
export function useOpenFavorite() {
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const openFavorite = React.useCallback(
|
||||
async (favorite) => {
|
||||
const { icon, tabComponent, title, props, tabdata } = favorite;
|
||||
let tabdataNew = tabdata;
|
||||
if (props.savedFile) {
|
||||
const resp = await axios.post('files/load', {
|
||||
folder: props.savedFolder,
|
||||
file: props.savedFile,
|
||||
format: props.savedFormat,
|
||||
});
|
||||
tabdataNew = {
|
||||
...tabdata,
|
||||
editor: resp.data,
|
||||
};
|
||||
}
|
||||
openNewTab(
|
||||
{
|
||||
title,
|
||||
icon: icon || 'img favorite',
|
||||
props,
|
||||
tabComponent,
|
||||
},
|
||||
tabdataNew
|
||||
);
|
||||
},
|
||||
[openNewTab]
|
||||
);
|
||||
|
||||
return openFavorite;
|
||||
}
|
||||
|
||||
export function FavoriteFileAppObject({ data, commonProps }) {
|
||||
const { icon, tabComponent, title, props, tabdata, urlPath } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
const showModal = useShowModal();
|
||||
const openFavorite = useOpenFavorite();
|
||||
const electron = getElectron();
|
||||
|
||||
const editFavorite = () => {
|
||||
showModal((modalState) => <FavoriteModal modalState={modalState} editingData={data} />);
|
||||
};
|
||||
|
||||
const editFavoriteJson = async () => {
|
||||
const resp = await axios.post('files/load', {
|
||||
folder: 'favorites',
|
||||
file: data.file,
|
||||
format: 'text',
|
||||
});
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
icon: 'icon favorite',
|
||||
title,
|
||||
tabComponent: 'FavoriteEditorTab',
|
||||
props: {
|
||||
savedFile: data.file,
|
||||
savedFormat: 'text',
|
||||
savedFolder: 'favorites',
|
||||
},
|
||||
},
|
||||
{ editor: JSON.stringify(JSON.parse(resp.data), null, 2) }
|
||||
);
|
||||
};
|
||||
|
||||
const copyLink = () => {
|
||||
copyTextToClipboard(`${document.location.origin}#favorite=${urlPath}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon={icon || 'img favorite'}
|
||||
title={title}
|
||||
disableRename
|
||||
onLoad={async (data) => {
|
||||
openFavorite(data);
|
||||
}}
|
||||
menuExt={
|
||||
<>
|
||||
<DropDownMenuItem onClick={editFavorite}>Edit</DropDownMenuItem>
|
||||
<DropDownMenuItem onClick={editFavoriteJson}>Edit JSON definition</DropDownMenuItem>
|
||||
{!electron && urlPath && <DropDownMenuItem onClick={copyLink}>Copy link</DropDownMenuItem>}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
FavoriteFileAppObject.extractKey = (data) => data.file;
|
||||
@@ -1,13 +1,15 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
|
||||
const macroAppObject = () => ({ name, type, title, group }, { setOpenedTabs }) => {
|
||||
const key = name;
|
||||
const icon = 'img macro';
|
||||
const matcher = (filter) => filterName(filter, name, title);
|
||||
const groupTitle = group;
|
||||
function MacroAppObject({ data, commonProps }) {
|
||||
const { name, type, title, group } = data;
|
||||
|
||||
return { title, key, icon, groupTitle, matcher };
|
||||
};
|
||||
return <AppObjectCore {...commonProps} data={data} title={title} icon={'img macro'} />;
|
||||
}
|
||||
|
||||
export default macroAppObject;
|
||||
MacroAppObject.extractKey = (data) => data.name;
|
||||
MacroAppObject.createMatcher = ({ name, title }) => (filter) => filterName(filter, name, title);
|
||||
|
||||
export default MacroAppObject;
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
import React from 'react';
|
||||
import axios from '../utility/axios';
|
||||
import _ from 'lodash';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { AppObjectCore } from './AppObjectCore';
|
||||
import useNewQuery from '../query/useNewQuery';
|
||||
import { useCurrentDatabase } from '../utility/globalState';
|
||||
import ScriptWriter from '../impexp/ScriptWriter';
|
||||
import { extractPackageName } from 'dbgate-tools';
|
||||
import useShowModal from '../modals/showModal';
|
||||
import InputTextModal from '../modals/InputTextModal';
|
||||
import useHasPermission from '../utility/useHasPermission';
|
||||
import useOpenNewTab from '../utility/useOpenNewTab';
|
||||
import ConfirmModal from '../modals/ConfirmModal';
|
||||
|
||||
function Menu({ data, menuExt = null, title = undefined, disableRename = false }) {
|
||||
const hasPermission = useHasPermission();
|
||||
const showModal = useShowModal();
|
||||
const handleDelete = () => {
|
||||
showModal((modalState) => (
|
||||
<ConfirmModal
|
||||
modalState={modalState}
|
||||
message={`Really delete file ${title || data.file}?`}
|
||||
onConfirm={() => {
|
||||
axios.post('files/delete', data);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
const handleRename = () => {
|
||||
showModal((modalState) => (
|
||||
<InputTextModal
|
||||
modalState={modalState}
|
||||
value={data.file}
|
||||
label="New file name"
|
||||
header="Rename file"
|
||||
onConfirm={(newFile) => {
|
||||
axios.post('files/rename', { ...data, newFile });
|
||||
}}
|
||||
/>
|
||||
));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{hasPermission(`files/${data.folder}/write`) && (
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
)}
|
||||
{hasPermission(`files/${data.folder}/write`) && !disableRename && (
|
||||
<DropDownMenuItem onClick={handleRename}>Rename</DropDownMenuItem>
|
||||
)}
|
||||
{menuExt}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedFileAppObjectBase({
|
||||
data,
|
||||
commonProps,
|
||||
format,
|
||||
icon,
|
||||
onLoad,
|
||||
title = undefined,
|
||||
menuExt = null,
|
||||
disableRename = false,
|
||||
}) {
|
||||
const { file, folder } = data;
|
||||
|
||||
const onClick = async () => {
|
||||
const resp = await axios.post('files/load', { folder, file, format });
|
||||
onLoad(resp.data);
|
||||
};
|
||||
|
||||
return (
|
||||
<AppObjectCore
|
||||
{...commonProps}
|
||||
data={data}
|
||||
title={title || file}
|
||||
icon={icon}
|
||||
onClick={onClick}
|
||||
Menu={(props) => <Menu {...props} menuExt={menuExt} title={title} disableRename={disableRename} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedSqlFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const newQuery = useNewQuery();
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection');
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const handleGenerateExecute = () => {
|
||||
const script = new ScriptWriter();
|
||||
const conn = {
|
||||
..._.omit(connection, ['displayName', '_id']),
|
||||
database,
|
||||
};
|
||||
script.put(`const sql = await dbgateApi.loadFile('${folder}/${file}');`);
|
||||
script.put(`await dbgateApi.executeQuery({ sql, connection: ${JSON.stringify(conn)} });`);
|
||||
// @ts-ignore
|
||||
script.requirePackage(extractPackageName(conn.engine));
|
||||
|
||||
openNewTab(
|
||||
{
|
||||
title: 'Shell',
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
},
|
||||
{ editor: script.getScript() }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img sql-file"
|
||||
menuExt={
|
||||
connection && database ? (
|
||||
<DropDownMenuItem onClick={handleGenerateExecute}>Generate shell execute</DropDownMenuItem>
|
||||
) : null
|
||||
}
|
||||
onLoad={(data) => {
|
||||
newQuery({
|
||||
title: file,
|
||||
initialData: data,
|
||||
// @ts-ignore
|
||||
savedFile: file,
|
||||
savedFolder: 'sql',
|
||||
savedFormat: 'text',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedShellFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img shell"
|
||||
onLoad={(data) => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img shell',
|
||||
tabComponent: 'ShellTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'shell',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedChartFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection') || {};
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon="img chart"
|
||||
onLoad={(data) => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img chart',
|
||||
tooltip,
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database,
|
||||
savedFile: file,
|
||||
savedFolder: 'charts',
|
||||
savedFormat: 'json',
|
||||
},
|
||||
tabComponent: 'ChartTab',
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedQueryFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const currentDatabase = useCurrentDatabase();
|
||||
|
||||
const connection = _.get(currentDatabase, 'connection') || {};
|
||||
const database = _.get(currentDatabase, 'name');
|
||||
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}`;
|
||||
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="json"
|
||||
icon="img query-design"
|
||||
onLoad={(data) => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img query-design',
|
||||
tooltip,
|
||||
props: {
|
||||
conid: connection._id,
|
||||
database,
|
||||
savedFile: file,
|
||||
savedFolder: 'query',
|
||||
savedFormat: 'json',
|
||||
},
|
||||
tabComponent: 'QueryDesignTab',
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedMarkdownFileAppObject({ data, commonProps }) {
|
||||
const { file, folder } = data;
|
||||
const openNewTab = useOpenNewTab();
|
||||
|
||||
const showPage = () => {
|
||||
openNewTab({
|
||||
title: file,
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownViewTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'markdown',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
});
|
||||
};
|
||||
return (
|
||||
<SavedFileAppObjectBase
|
||||
data={data}
|
||||
commonProps={commonProps}
|
||||
format="text"
|
||||
icon="img markdown"
|
||||
onLoad={(data) => {
|
||||
openNewTab(
|
||||
{
|
||||
title: file,
|
||||
icon: 'img markdown',
|
||||
tabComponent: 'MarkdownEditorTab',
|
||||
props: {
|
||||
savedFile: file,
|
||||
savedFolder: 'markdown',
|
||||
savedFormat: 'text',
|
||||
},
|
||||
},
|
||||
{ editor: data }
|
||||
);
|
||||
}}
|
||||
menuExt={<DropDownMenuItem onClick={showPage}>Show page</DropDownMenuItem>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function SavedFileAppObject({ data, commonProps }) {
|
||||
const { folder } = data;
|
||||
const folderTypes = {
|
||||
sql: SavedSqlFileAppObject,
|
||||
shell: SavedShellFileAppObject,
|
||||
charts: SavedChartFileAppObject,
|
||||
markdown: SavedMarkdownFileAppObject,
|
||||
query: SavedQueryFileAppObject,
|
||||
};
|
||||
const AppObject = folderTypes[folder];
|
||||
if (AppObject) {
|
||||
return <AppObject data={data} commonProps={commonProps} />;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
[
|
||||
SavedSqlFileAppObject,
|
||||
SavedShellFileAppObject,
|
||||
SavedChartFileAppObject,
|
||||
SavedMarkdownFileAppObject,
|
||||
SavedFileAppObject,
|
||||
].forEach((fn) => {
|
||||
// @ts-ignore
|
||||
fn.extractKey = (data) => data.file;
|
||||
});
|
||||
@@ -1,24 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import axios from '../utility/axios';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
|
||||
function Menu({ data, setOpenedTabs }) {
|
||||
const handleDelete = () => {
|
||||
axios.post('archive/delete-folder', { folder: data.name });
|
||||
};
|
||||
return <>{data.name != 'default' && <DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>}</>;
|
||||
}
|
||||
|
||||
const archiveFolderAppObject = () => ({ name }, { setOpenedTabs, currentArchive }) => {
|
||||
const key = name;
|
||||
const icon = 'img archive-folder';
|
||||
const isBold = name == currentArchive;
|
||||
const matcher = (filter) => filterName(filter, name);
|
||||
|
||||
return { title: name, key, icon, isBold, Menu, matcher };
|
||||
};
|
||||
|
||||
export default archiveFolderAppObject;
|
||||
@@ -1,15 +0,0 @@
|
||||
/** @param columnProps {import('dbgate-types').ColumnInfo} */
|
||||
function getColumnIcon(columnProps) {
|
||||
if (columnProps.autoIncrement) return 'img autoincrement';
|
||||
return 'img column';
|
||||
}
|
||||
|
||||
/** @param columnProps {import('dbgate-types').ColumnInfo} */
|
||||
export default function columnAppObject(columnProps, { setOpenedTabs }) {
|
||||
const title = columnProps.columnName;
|
||||
const key = title;
|
||||
const icon = getColumnIcon(columnProps);
|
||||
const isBold = columnProps.notNull;
|
||||
|
||||
return { title, key, icon, isBold };
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
/** @param props {import('dbgate-types').ConstraintInfo} */
|
||||
function getConstraintIcon(props) {
|
||||
if (props.constraintType == 'primaryKey') return 'img primary-key';
|
||||
if (props.constraintType == 'foreignKey') return 'img foreign-key';
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @param props {import('dbgate-types').ConstraintInfo} */
|
||||
export default function constraintAppObject(props, { setOpenedTabs }) {
|
||||
const title = props.constraintName;
|
||||
const key = title;
|
||||
const icon = getConstraintIcon(props);
|
||||
|
||||
return { title, key, icon };
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import React from 'react';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
import { openNewTab } from '../utility/common';
|
||||
import { getConnectionInfo } from '../utility/metadataLoaders';
|
||||
import fullDisplayName from '../utility/fullDisplayName';
|
||||
import { filterName } from 'dbgate-datalib';
|
||||
import ImportExportModal from '../modals/ImportExportModal';
|
||||
|
||||
const icons = {
|
||||
tables: 'img table',
|
||||
views: 'img view',
|
||||
procedures: 'img procedure',
|
||||
functions: 'img function',
|
||||
};
|
||||
|
||||
const menus = {
|
||||
tables: [
|
||||
{
|
||||
label: 'Open data',
|
||||
tab: 'TableDataTab',
|
||||
},
|
||||
{
|
||||
label: 'Open structure',
|
||||
tab: 'TableStructureTab',
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE TABLE script',
|
||||
sqlTemplate: 'CREATE TABLE',
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
],
|
||||
views: [
|
||||
{
|
||||
label: 'Open data',
|
||||
tab: 'ViewDataTab',
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE VIEW script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'Show CREATE TABLE script',
|
||||
sqlTemplate: 'CREATE TABLE',
|
||||
},
|
||||
{
|
||||
label: 'Export',
|
||||
isExport: true,
|
||||
},
|
||||
{
|
||||
label: 'Open in free table editor',
|
||||
isOpenFreeTable: true,
|
||||
},
|
||||
{
|
||||
label: 'Open structure',
|
||||
tab: 'TableStructureTab',
|
||||
},
|
||||
],
|
||||
procedures: [
|
||||
{
|
||||
label: 'Show CREATE PROCEDURE script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
{
|
||||
label: 'Show EXECUTE script',
|
||||
sqlTemplate: 'EXECUTE PROCEDURE',
|
||||
},
|
||||
],
|
||||
functions: [
|
||||
{
|
||||
label: 'Show CREATE FUNCTION script',
|
||||
sqlTemplate: 'CREATE OBJECT',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const defaultTabs = {
|
||||
tables: 'TableDataTab',
|
||||
views: 'ViewDataTab',
|
||||
};
|
||||
|
||||
export async function openDatabaseObjectDetail(
|
||||
setOpenedTabs,
|
||||
tabComponent,
|
||||
sqlTemplate,
|
||||
{ schemaName, pureName, conid, database, objectTypeField }
|
||||
) {
|
||||
const connection = await getConnectionInfo({ conid });
|
||||
const tooltip = `${connection.displayName || connection.server}\n${database}\n${fullDisplayName({
|
||||
schemaName,
|
||||
pureName,
|
||||
})}`;
|
||||
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: pureName,
|
||||
tooltip,
|
||||
icon: sqlTemplate ? 'img sql-file' : icons[objectTypeField],
|
||||
tabComponent: sqlTemplate ? 'QueryTab' : tabComponent,
|
||||
props: {
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
initialArgs: sqlTemplate ? { sqlTemplate } : null,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function Menu({ data, makeAppObj, setOpenedTabs, showModal }) {
|
||||
return (
|
||||
<>
|
||||
{menus[data.objectTypeField].map((menu) => (
|
||||
<DropDownMenuItem
|
||||
key={menu.label}
|
||||
onClick={async () => {
|
||||
if (menu.isExport) {
|
||||
showModal((modalState) => (
|
||||
<ImportExportModal
|
||||
modalState={modalState}
|
||||
initialValues={{
|
||||
sourceStorageType: 'database',
|
||||
sourceConnectionId: data.conid,
|
||||
sourceDatabaseName: data.database,
|
||||
sourceSchemaName: data.schemaName,
|
||||
sourceList: [data.pureName],
|
||||
}}
|
||||
/>
|
||||
));
|
||||
} else if (menu.isOpenFreeTable) {
|
||||
const coninfo = await getConnectionInfo(data);
|
||||
openNewTab(setOpenedTabs, {
|
||||
title: data.pureName,
|
||||
icon: 'img free-table',
|
||||
tabComponent: 'FreeTableTab',
|
||||
props: {
|
||||
initialData: {
|
||||
functionName: 'tableReader',
|
||||
props: {
|
||||
connection: {
|
||||
...coninfo,
|
||||
database: data.database,
|
||||
},
|
||||
schemaName: data.schemaName,
|
||||
pureName: data.pureName,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
} else {
|
||||
openDatabaseObjectDetail(setOpenedTabs, menu.tab, menu.sqlTemplate, data);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{menu.label}
|
||||
</DropDownMenuItem>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const databaseObjectAppObject = () => (
|
||||
{ conid, database, pureName, schemaName, objectTypeField },
|
||||
{ setOpenedTabs }
|
||||
) => {
|
||||
const title = schemaName ? `${schemaName}.${pureName}` : pureName;
|
||||
const key = title;
|
||||
const icon = icons[objectTypeField];
|
||||
// const Icon = (props) => getIconImage(icons[objectTypeField], props);
|
||||
const onClick = ({ schemaName, pureName }) => {
|
||||
openDatabaseObjectDetail(
|
||||
setOpenedTabs,
|
||||
defaultTabs[objectTypeField],
|
||||
defaultTabs[objectTypeField] ? null : 'CREATE OBJECT',
|
||||
{
|
||||
schemaName,
|
||||
pureName,
|
||||
conid,
|
||||
database,
|
||||
objectTypeField,
|
||||
}
|
||||
);
|
||||
};
|
||||
const matcher = (filter) => filterName(filter, pureName);
|
||||
const groupTitle = _.startCase(objectTypeField);
|
||||
|
||||
return { title, key, icon, Menu, onClick, matcher, groupTitle };
|
||||
};
|
||||
|
||||
export default databaseObjectAppObject;
|
||||
@@ -1,41 +0,0 @@
|
||||
import React from 'react';
|
||||
import _ from 'lodash';
|
||||
import { DropDownMenuItem } from '../modals/DropDownMenu';
|
||||
|
||||
function Menu({ data, setSavedSqlFiles }) {
|
||||
const handleDelete = () => {
|
||||
setSavedSqlFiles((files) => files.filter((x) => x.storageKey != data.storageKey));
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DropDownMenuItem onClick={handleDelete}>Delete</DropDownMenuItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const savedSqlFileAppObject = () => ({ name, storageKey }, { setOpenedTabs, newQuery, openedTabs }) => {
|
||||
const key = storageKey;
|
||||
const title = name;
|
||||
const icon = 'img sql-file';
|
||||
|
||||
const onClick = () => {
|
||||
const existing = openedTabs.find((x) => x.props && x.props.storageKey == storageKey);
|
||||
if (existing) {
|
||||
setOpenedTabs(
|
||||
openedTabs.map((x) => ({
|
||||
...x,
|
||||
selected: x == existing,
|
||||
}))
|
||||
);
|
||||
} else {
|
||||
newQuery({
|
||||
title,
|
||||
storageKey,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { title, key, icon, onClick, Menu };
|
||||
};
|
||||
|
||||
export default savedSqlFileAppObject;
|
||||
@@ -52,15 +52,18 @@ function autodetect(selection, grider, value) {
|
||||
return 'textWrap';
|
||||
}
|
||||
|
||||
export default function CellDataView({ selection, grider }) {
|
||||
export default function CellDataView({ selection = undefined, grider = undefined, selectedValue = undefined }) {
|
||||
const [selectedFormatType, setSelectedFormatType] = React.useState('autodetect');
|
||||
const theme = useTheme();
|
||||
let value = null;
|
||||
if (grider && selection.length == 1) {
|
||||
if (grider && selection && selection.length == 1) {
|
||||
const rowData = grider.getRowData(selection[0].row);
|
||||
const { column } = selection[0];
|
||||
if (rowData) value = rowData[column];
|
||||
}
|
||||
if (selectedValue) {
|
||||
value = selectedValue;
|
||||
}
|
||||
const autodetectFormatType = React.useMemo(() => autodetect(selection, grider, value), [selection, grider, value]);
|
||||
const autodetectFormat = formats.find((x) => x.type == autodetectFormatType);
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ const InnerWrapper = styled.div`
|
||||
export default function JsonCellView({ value }) {
|
||||
const theme = useTheme();
|
||||
try {
|
||||
const json = JSON.parse(value);
|
||||
const json = React.useMemo(() => JSON.parse(value), [value]);
|
||||
return (
|
||||
<OuterWrapper>
|
||||
<InnerWrapper>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user