Compare commits
161 Commits
v0.0.1-alp
...
v0.0.7
Author | SHA1 | Date | |
---|---|---|---|
5fc4df426c | |||
799a5fef5b | |||
54717e1f6a | |||
4288a1fd33 | |||
52449e0420 | |||
96f38297c1 | |||
3e737cba62 | |||
3d0a83f2cf | |||
c1cdd03938 | |||
437e41bff0 | |||
|
4516783b13 | ||
43c7072c1c | |||
530d1bd43f | |||
530907d097 | |||
ac4941fa5e | |||
5334a44271 | |||
4991291c2c | |||
2554444322 | |||
cff4f537de | |||
12fbe8c1a0 | |||
10b426b90b | |||
b29e07c3b7 | |||
ffef312b44 | |||
|
264829bec0 | ||
|
89c3dc9fed | ||
|
fe3d741601 | ||
1b04b216b2 | |||
78965d23e3 | |||
9b76c8eae0 | |||
|
c94ae8c9bc | ||
ad0bad8486 | |||
8e71f42a28 | |||
967a0aa6e3 | |||
ddc7d1ea26 | |||
4684b4114b | |||
3e08ba221d | |||
1d87ca959f | |||
023c6a633a | |||
48f77bae01 | |||
3385744260 | |||
86aec4f5e4 | |||
ba73d677b5 | |||
51ccce3da4 | |||
a1a6f51f2f | |||
801a0de186 | |||
264de9c568 | |||
8390f8aa55 | |||
af7c0e90b8 | |||
|
da33e77361 | ||
|
a4841ab63b | ||
de3f36a3fe | |||
8dc74ef2c3 | |||
256ec76588 | |||
196a3e0185 | |||
bc54fef0aa | |||
a5b478e53d | |||
|
2e235ad2fe | ||
|
950bb17b1e | ||
3a6ea76b93 | |||
d7ed00f4a3 | |||
fd6d5177ef | |||
9599b43f78 | |||
2cfb223ff6 | |||
69def94c88 | |||
e8141b6321 | |||
0b6a188d19 | |||
dca625fe5a | |||
|
a4b94bc19c | ||
744728a14f | |||
6d0724dc90 | |||
59e4a79f42 | |||
7bc10092fe | |||
eb348b3095 | |||
3c6e818ba0 | |||
2f1dfdc654 | |||
128a6cd9e8 | |||
5473858323 | |||
7651d05b37 | |||
c89c1ce83c | |||
771f8a2d68 | |||
13b0816837 | |||
a15e6249e1 | |||
bbde2bd994 | |||
949f7add8f | |||
968ec1edf7 | |||
a9d3a57281 | |||
|
f787439009 | ||
b03d461e21 | |||
5c05e3e9e9 | |||
0089c0cbac | |||
4fd72ec9e7 | |||
712fe9f00d | |||
092e8a0732 | |||
70908eb076 | |||
f8a0783769 | |||
|
cdf964ef07 | ||
413b56916c | |||
acd3310228 | |||
|
e014f81a9c | ||
530361144e | |||
d69e411581 | |||
9a3a0513e2 | |||
|
be8fa96c93 | ||
587116bd20 | |||
|
145d1dd1ad | ||
457410b65a | |||
|
b183eacae1 | ||
76cafdb69a | |||
|
2d63ddb9c7 | ||
f12f00dbb7 | |||
1ecb6d892c | |||
fcd83f35d8 | |||
|
7a4d8286a6 | ||
|
dd5ec2c661 | ||
3f0e5d3512 | |||
ac01511c10 | |||
4a83ae7e75 | |||
60132c94a1 | |||
fdf5bef5ad | |||
67f55fbeb9 | |||
|
fd5a8548c7 | ||
425ecf838d | |||
1a8a49eceb | |||
|
e9fffcc37e | ||
bba7c4af6f | |||
|
376d74c7dc | ||
307a32aff6 | |||
d1aaad276b | |||
|
b23b4f18b9 | ||
334cfa9047 | |||
|
2f6d16c730 | ||
b8e09e7003 | |||
9caf424331 | |||
|
9e5e545478 | ||
|
c9ab731bb4 | ||
57832c43aa | |||
|
184363369b | ||
b221dd12ff | |||
|
1324464aa3 | ||
187b4f50f9 | |||
262a476f50 | |||
|
f00c2600e5 | ||
75a7db9c05 | |||
50cd852d01 | |||
bb8cfa533b | |||
|
1ddc8b5dca | ||
8a4c628128 | |||
0076f146fa | |||
098d12f462 | |||
b619285d94 | |||
|
a5fed1bb64 | ||
55ec03bd8e | |||
|
d1977fbd75 | ||
3c20c0733c | |||
1d4a353d5c | |||
db71777cbc | |||
f350fe8203 | |||
28c3f87dd8 | |||
cc8dbb8df7 | |||
85ac1bc85f | |||
baea5c26e2 |
@@ -21,7 +21,7 @@
|
||||
],
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"windows"
|
||||
"unix"
|
||||
],
|
||||
"brace-style": [
|
||||
"error",
|
||||
|
12
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [fabio286]
|
||||
patreon: fabio286
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: # Replace with a single Liberapay username
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://paypal.me/fabiodistasio']
|
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Desktop (please complete the following information):**
|
||||
- OS: [e.g. iOS]
|
||||
- Browser [e.g. chrome, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Smartphone (please complete the following information):**
|
||||
- Device: [e.g. iPhone6]
|
||||
- OS: [e.g. iOS8.1]
|
||||
- Browser [e.g. stock browser, safari]
|
||||
- Version [e.g. 22]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
11
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
62
.github/workflows/codeql-analysis.yml
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [master]
|
||||
schedule:
|
||||
- cron: '0 15 * * 0'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Override automatic language detection by changing the below list
|
||||
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
|
||||
language: ['javascript']
|
||||
# Learn more...
|
||||
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
2
.gitignore
vendored
@@ -6,4 +6,4 @@ thumbs.db
|
||||
.vscode
|
||||
TODO.md
|
||||
*.txt
|
||||
dev-app-update.yml
|
||||
package-lock.json
|
14
.stylelintrc
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"extends": [
|
||||
"stylelint-config-standard"
|
||||
],
|
||||
"fix": true,
|
||||
"formatter": "verbose",
|
||||
"plugins": [
|
||||
"stylelint-scss"
|
||||
],
|
||||
"rules": {
|
||||
"at-rule-no-unknown": null
|
||||
},
|
||||
"syntax": "scss"
|
||||
}
|
38
.travis.yml
Normal file
@@ -0,0 +1,38 @@
|
||||
language: node_js
|
||||
node_js: 12
|
||||
|
||||
before_install:
|
||||
- npm install
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- node_modules
|
||||
- app/node_modules
|
||||
- $HOME/.cache/electron
|
||||
- $HOME/.cache/electron-builder
|
||||
- $HOME/.npm/_prebuilds
|
||||
|
||||
env:
|
||||
global:
|
||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
||||
|
||||
jobs:
|
||||
include:
|
||||
- stage: Test
|
||||
script:
|
||||
- npm test
|
||||
- stage: Deploy Linux & Windows
|
||||
if: tag IS present
|
||||
os: linux
|
||||
services: docker
|
||||
script:
|
||||
- docker run --rm --env-file <(env | grep -iE 'DEBUG|NODE_|ELECTRON_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_') -v ${PWD}:/project -v ~/.cache/electron:/root/.cache/electron -v ~/.cache/electron-builder:/root/.cache/electron-builder electronuserland/builder:wine /bin/bash -c "npm run build -- --linux --win -p always"
|
||||
before_cache:
|
||||
- rm -rf $HOME/.cache/electron-builder/wine
|
||||
- stage: Deploy Mac
|
||||
if: tag IS present
|
||||
os: osx
|
||||
osx_image: xcode10.2
|
||||
script:
|
||||
- npm run build -- -p always
|
76
CHANGELOG.md
@@ -1,7 +1,79 @@
|
||||
# Changelog
|
||||
|
||||
## [0.0.1-alpha]() - Coming Soon
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.0.7](https://github.com/EStarium/antares/compare/v0.0.6...v0.0.7) (2020-10-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
- **Initial release:**
|
||||
* database creation ([3d0a83f](https://github.com/EStarium/antares/commit/3d0a83f2cf68c4dd412fd7679c39d63f081b7c19))
|
||||
* databases deletion ([4288a1f](https://github.com/EStarium/antares/commit/4288a1fd331f4a28de2e756f898d208a6a6599c4))
|
||||
* edit database collation ([54717e1](https://github.com/EStarium/antares/commit/54717e1f6a36ec0b3dd096d0e1e747512f6dda09))
|
||||
* field comment on mouse over a table field name ([2554444](https://github.com/EStarium/antares/commit/2554444322b59a6b1ab3ff05ccf8604bf6f8c8b8))
|
||||
* support to multiple queries in the same tab ([48f77ba](https://github.com/EStarium/antares/commit/48f77bae01efbff40bd0f5ce8c66e2619f44bf3a))
|
||||
* update italian translation ([89c3dc9](https://github.com/EStarium/antares/commit/89c3dc9fede63c77eb22b48df1a375ea44830306))
|
||||
* Update italian translation ([fe3d741](https://github.com/EStarium/antares/commit/fe3d7416013c44a4974471ab59b7c9a98afb7255))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* cell update soft reload doesn't apply changes ([1b04b21](https://github.com/EStarium/antares/commit/1b04b216b21b697e47062a9366bc1b6a040a1a72))
|
||||
* empty databases not shown in explore bar ([3e737cb](https://github.com/EStarium/antares/commit/3e737cba62f795f225e944939c6bff04b27fa3d4))
|
||||
* glitch on table data tab ([10b426b](https://github.com/EStarium/antares/commit/10b426b90b6b9461cfffce3026c982463f6e0599))
|
||||
* lack of loading progressbar when an update is available ([86aec4f](https://github.com/EStarium/antares/commit/86aec4f5e41c059e88066a01f0d85155de99a5ee))
|
||||
* missing schema when queryng INFORMATION_SCHEMA ([530d1bd](https://github.com/EStarium/antares/commit/530d1bd43fa95de05f594b9b5cae2f4b397f96e0))
|
||||
* prevent multiple app instances ([12fbe8c](https://github.com/EStarium/antares/commit/12fbe8c1a03259648554f2a5c69b5abbedc18a48))
|
||||
* several fix on data and query tabs ([530907d](https://github.com/EStarium/antares/commit/530907d097ac4d995e1bfcb02e6c890fd6007e21))
|
||||
* unable to obtain fields informations for some queries ([43c7072](https://github.com/EStarium/antares/commit/43c7072c1c83a2455ae48a37be69b444b3eb6560))
|
||||
* unable to obtain keyUsage informations when adding new row ([023c6a6](https://github.com/EStarium/antares/commit/023c6a633a7f268b1a97b748ad08d2416cc30ffe))
|
||||
* value overridden when join tables with fields with same name ([78965d2](https://github.com/EStarium/antares/commit/78965d23e3efb7d8d6d110d79142966e57200757))
|
||||
* wrong field names when join tables ([ad0bad8](https://github.com/EStarium/antares/commit/ad0bad8486c3d67ec14ec1aed3d8aff6cce9df87))
|
||||
* wrong italian translation ([b29e07c](https://github.com/EStarium/antares/commit/b29e07c3b722aec7e78f3cef2e357a53cbcac474))
|
||||
* wrong schema fetching table fields and key usage ([8e71f42](https://github.com/EStarium/antares/commit/8e71f42a28060fdfeeb81502b0759d0d11f5bcfd))
|
||||
* wrong table and schema when more than one query in a tab ([4684b41](https://github.com/EStarium/antares/commit/4684b4114b9c9c253120292d7d164d7676011f86))
|
||||
|
||||
### [0.0.6](https://github.com/EStarium/antares/compare/v0.0.5...v0.0.6) (2020-09-03)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Aliases support ([264de9c](https://github.com/EStarium/antares/commit/264de9c5686fb3a2ef22d96171f45b915ba1b34b))
|
||||
* Middle click to close tabs ([256ec76](https://github.com/EStarium/antares/commit/256ec765883fcf247355190827e943c76e95f13b))
|
||||
* Monaco-editor as query editor ([196a3e0](https://github.com/EStarium/antares/commit/196a3e0185a3d68b7c4ade8dbf187d2b216cc00b))
|
||||
* Sql suggestions in query editor ([8dc74ef](https://github.com/EStarium/antares/commit/8dc74ef2c335e8ae4a69f5d2651df65939139b1b))
|
||||
* Support to multiple query tabs ([d7ed00f](https://github.com/EStarium/antares/commit/d7ed00f4a3613da9015c9fc48c4d8062d292e416))
|
||||
* Tabs horizontal scroll with mouse wheel ([3a6ea76](https://github.com/EStarium/antares/commit/3a6ea76b93682ebd50908df7368c62c2c1e27958))
|
||||
* **Arabic translation** thanks to [Mohd-PH](https://github.com/Mohd-PH) ([#29](https://github.com/EStarium/antares/pull/29))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Error when launching queries without a result from query tabs ([a1a6f51](https://github.com/EStarium/antares/commit/a1a6f51f2fba5140f5e3bd9cd6557c8a13dfaa2c))
|
||||
* Field name displayed instead of alias ([801a0de](https://github.com/EStarium/antares/commit/801a0de1865dea2a59ff057b7c2cc988cc9c87ed))
|
||||
* Wrong table height calc in some cases ([fd6d517](https://github.com/EStarium/antares/commit/fd6d5177efb6161aab01f9e108eda60df6c7d8c4))
|
||||
|
||||
### [0.0.5](https://github.com/EStarium/antares/compare/v0.0.4...v0.0.5) (2020-08-17)
|
||||
|
||||
### Features
|
||||
|
||||
* Badge on setting icon and update tab when update is available ([e8141b6](https://github.com/EStarium/antares/commit/e8141b632154f765ca73fa50b9b7120dc592ead0))
|
||||
* Foreign key support in add/edit row ([0b6a188](https://github.com/EStarium/antares/commit/0b6a188d1959b80b4a66946cc79d2dd3853a428b))
|
||||
* Option to insert table rows ([2f1dfdc](https://github.com/EStarium/antares/commit/2f1dfdc6543b4a6c1d595f0daa00c0832be49c77))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Insert files via add row option ([3c6e818](https://github.com/EStarium/antares/commit/3c6e818ba06f1b8b5db0ecf80c3b7498d6d2a841))
|
||||
* Newline replaced with undefined inside queries ([59e4a79](https://github.com/EStarium/antares/commit/59e4a79f42076b3fce98a764e9ad6a01c674555b))
|
||||
* Query result header didn't show just selected fields ([7bc1009](https://github.com/EStarium/antares/commit/7bc10092fe4823e03133e69e0a7bf86e44fde43b))
|
||||
* Table header not fixed on top when fast scrolling ([13b0816](https://github.com/EStarium/antares/commit/13b0816837461119eaab79fdb7e92223e0950630))
|
||||
* Time and datetime precision ([771f8a2](https://github.com/EStarium/antares/commit/771f8a2d682c64105231e3fef199f05150596298))
|
||||
* Update a row with a string key value ([eb348b3](https://github.com/EStarium/antares/commit/eb348b3095b6905321b62eed6cea228374ebc3d1))
|
||||
* Window title not perfectly centered ([7651d05](https://github.com/EStarium/antares/commit/7651d05b37970574d6ae4bdf75c20c69d59c1e6d))
|
||||
* Wrong schema passed in query tab when a different database was selected ([6d0724d](https://github.com/EStarium/antares/commit/6d0724dc90cdebb10e0342d2c472bdd07aa345f8))
|
||||
|
||||
### [0.0.4](https://github.com/EStarium/antares/compare/v0.0.3-alpha...v0.0.4) (2020-08-06)
|
||||
|
||||
### Features
|
||||
|
||||
* Blob fields edit/view/download ([712fe9f](https://github.com/EStarium/antares/commit/712fe9f00d210db0f2317eca61e7fb648383e3fe))
|
||||
* Window title in app title bar ([0089c0c](https://github.com/EStarium/antares/commit/0089c0cbac6caf0a6fd195849099f18713580228))
|
||||
|
2
LICENSE
@@ -1,6 +1,6 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 Jakub Szwacz
|
||||
Copyright (c) 2020 Fabio Di Stasio
|
||||
|
||||
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:
|
||||
|
||||
|
75
README.md
@@ -1,7 +1,76 @@
|
||||
<p align="center">
|
||||
<img width="256" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/logo.png">
|
||||
<img width="800" src="https://raw.githubusercontent.com/Fabio286/antares/master/docs/screen-alpha.png">
|
||||
</p>
|
||||
|
||||
# Antares
|
||||
# Antares SQL Client
|
||||
|
||||
🚧 Work in progress! 🚧
|
||||
 [](https://travis-ci.com/EStarium/antares)  
|
||||
|
||||
Antares is an SQL client based on [Electron.js](https://github.com/electron/electron) and [Vue.js](https://github.com/vuejs/vue) that aims to become a useful tool, especially for developers.
|
||||
My target is to support as many databases as possible, and all major operating systems, including the ARM versions.
|
||||
|
||||
**At the moment this application is an alpha, it lacks many features, and isn't ready as a main SQL client**. However i'm actively working on it, hoping to provide all essential features as soon as possible.
|
||||
|
||||
🔗 If you are curious to try this early state of Antares you can download and install the [latest release](https://github.com/EStarium/antares/releases).
|
||||
👁 To stay tuned for new releases watch this repo on **Release only** channel.
|
||||
🌟 Don't forget to **leave a star** if you appreciate this project.
|
||||
|
||||
## Philosophy
|
||||
|
||||
Why am I developing an SQL client when there are a lot of them on the market?
|
||||
The main goal is to develop a totally free, cross platform and open source alternative, empowered by JavaScript's ecosystem.
|
||||
An application created with minimalism and semplicity in mind, with features in the right places, not hundreds of tiny buttons or submenu.
|
||||
|
||||
## How to contribute
|
||||
|
||||
- [Translate Antares](https://github.com/EStarium/antares/wiki/Translate-Antares)
|
||||
|
||||
## Roadmap
|
||||
|
||||
This is a roadmap with major features will come in near future.
|
||||
|
||||
- Improvements of query editor area.
|
||||
- Database management (add/edit/delete).
|
||||
- Tables management (add/edit/delete).
|
||||
- Users management (add/edit/delete).
|
||||
- Stored procedures, views, schedulers and trigger support.
|
||||
- Database tools.
|
||||
- Context menu shortcuts.
|
||||
- Keyboard shortcuts.
|
||||
- More secure password storage.
|
||||
- Query logs console.
|
||||
- Fake data filler.
|
||||
- Import/export and migration.
|
||||
- SSH tunnel.
|
||||
- Themes.
|
||||
|
||||
## Currently supported
|
||||
|
||||
### Databases
|
||||
|
||||
- [x] MySQL/MariaDB
|
||||
- [ ] PostrgreSQL
|
||||
- [ ] MSSQL
|
||||
- [ ] SQLite
|
||||
- [ ] OracleDB
|
||||
- [ ] More...
|
||||
|
||||
### Operating Systems
|
||||
|
||||
#### • x86
|
||||
|
||||
- [x] Windows
|
||||
- [x] Linux
|
||||
- [x] MacOS (needs tests)
|
||||
|
||||
#### • ARM
|
||||
|
||||
- [ ] Windows
|
||||
- [ ] Linux
|
||||
- [ ] MacOS
|
||||
|
||||
## Translations
|
||||
|
||||
[Giuseppe Gigliotti](https://github.com/ReverbOD) / [Italian Translation](https://github.com/EStarium/antares/pull/20)
|
||||
[Mohd-PH](https://github.com/Mohd-PH) / [Arabic Translation](https://github.com/EStarium/antares/pull/29)
|
||||
[hongkfui](https://github.com/hongkfui) / [Spanish Translation](https://github.com/EStarium/antares/pull/32)
|
||||
|
BIN
build/icon.icns
Normal file
BIN
build/icon.ico
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
build/icon.png
Before Width: | Height: | Size: 13 KiB |
BIN
docs/screen-alpha.png
Normal file
After Width: | Height: | Size: 260 KiB |
13749
package-lock.json
generated
89
package.json
@@ -1,69 +1,94 @@
|
||||
{
|
||||
"name": "antares",
|
||||
"productName": "Antares",
|
||||
"version": "0.0.1-alpha",
|
||||
"version": "0.0.7",
|
||||
"description": "A cross-platform easy to use SQL client.",
|
||||
"license": "MIT",
|
||||
"repository": "https://github.com/Fabio286/antares.git",
|
||||
"repository": "https://github.com/EStarium/antares.git",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development electron-webpack dev",
|
||||
"compile": "electron-webpack",
|
||||
"dist": "cross-env NODE_ENV=production npm run compile && electron-builder",
|
||||
"dist:dir": "cross-env NODE_ENV=production npm run dist --dir -c.compression=store -c.mac.identity=null",
|
||||
"publish": "build -p always"
|
||||
"build": "cross-env NODE_ENV=production npm run compile && electron-builder",
|
||||
"release": "standard-version",
|
||||
"release:pre": "npm run release -- --prerelease alpha",
|
||||
"test": "npm run lint",
|
||||
"lint": "eslint . --ext .js,.vue && stylelint \"./src/**/*.{css,scss,sass,vue}\"",
|
||||
"lint:fix": "eslint . --ext .js,.vue --fix && stylelint \"./src/**/*.{css,scss,sass,vue}\" --fix"
|
||||
},
|
||||
"author": "Fabio Di Stasio <fabio286@gmail.com>",
|
||||
"build": {
|
||||
"appId": "com.estarium.antares",
|
||||
"artifactName": "${productName}-${version}-${os}_${arch}.${ext}",
|
||||
"files": [
|
||||
"static/*"
|
||||
]
|
||||
"dmg": {
|
||||
"contents": [
|
||||
{
|
||||
"x": 130,
|
||||
"y": 220
|
||||
},
|
||||
{
|
||||
"x": 410,
|
||||
"y": 220,
|
||||
"type": "link",
|
||||
"path": "/Applications"
|
||||
}
|
||||
]
|
||||
},
|
||||
"linux": {
|
||||
"target": [
|
||||
"deb",
|
||||
"AppImage"
|
||||
],
|
||||
"category": "Development"
|
||||
}
|
||||
},
|
||||
"electronWebpack": {
|
||||
"whiteListedModules": [
|
||||
"codemirror"
|
||||
],
|
||||
"renderer": {
|
||||
"webpackConfig": "webpack.config.js"
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.54.0",
|
||||
"electron-log": "^4.2.1",
|
||||
"electron-updater": "^4.3.1",
|
||||
"lodash": "^4.17.15",
|
||||
"material-design-icons": "^3.0.1",
|
||||
"moment": "^2.26.0",
|
||||
"mssql": "^6.2.0",
|
||||
"@mdi/font": "^5.6.55",
|
||||
"electron-log": "^4.2.4",
|
||||
"electron-updater": "^4.3.5",
|
||||
"lodash": "^4.17.20",
|
||||
"moment": "^2.29.0",
|
||||
"monaco-editor": "^0.20.0",
|
||||
"mssql": "^6.2.2",
|
||||
"mysql": "^2.18.1",
|
||||
"pg": "^8.2.1",
|
||||
"pg": "^8.3.3",
|
||||
"source-map-support": "^0.5.16",
|
||||
"spectre.css": "^0.5.8",
|
||||
"spectre.css": "^0.5.9",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-i18n": "^8.18.2",
|
||||
"vuedraggable": "^2.23.2",
|
||||
"vuex": "^3.4.0",
|
||||
"vuex-persist": "^2.2.0"
|
||||
"vue-i18n": "^8.21.0",
|
||||
"vue-the-mask": "^0.11.1",
|
||||
"vuedraggable": "^2.24.1",
|
||||
"vuex": "^3.5.1",
|
||||
"vuex-persist": "^3.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^10.1.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"electron": "^8.3.0",
|
||||
"electron-builder": "^22.7.0",
|
||||
"electron-devtools-installer": "^3.0.0",
|
||||
"electron": "^10.1.0",
|
||||
"electron-builder": "^22.8.1",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-webpack": "^2.8.2",
|
||||
"electron-webpack-vue": "^2.4.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint": "^7.8.1",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.21.1",
|
||||
"eslint-plugin-import": "^2.22.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"monaco-editor-webpack-plugin": "^1.9.1",
|
||||
"node-sass": "^4.14.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"vue": "^2.6.11",
|
||||
"webpack": "^4.43.0"
|
||||
"sass-loader": "^10.0.2",
|
||||
"standard-version": "^9.0.0",
|
||||
"stylelint": "^13.7.0",
|
||||
"stylelint-config-standard": "^20.0.0",
|
||||
"stylelint-scss": "^3.18.0",
|
||||
"vue": "^2.6.12",
|
||||
"vue-template-compiler": "^2.6.12",
|
||||
"webpack": "^4.44.2"
|
||||
}
|
||||
}
|
||||
|
12
src/common/fieldTypes.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const TEXT = ['char', 'varchar'];
|
||||
export const LONG_TEXT = ['text', 'mediumtext', 'longtext'];
|
||||
|
||||
export const NUMBER = ['int', 'tinyint', 'smallint', 'mediumint', 'bigint', 'float', 'double', 'decimal'];
|
||||
|
||||
export const DATE = ['date'];
|
||||
export const TIME = ['time'];
|
||||
export const DATETIME = ['datetime', 'timestamp'];
|
||||
|
||||
export const BLOB = ['blob', 'mediumblob', 'longblob'];
|
||||
|
||||
export const BIT = ['bit'];
|
7
src/common/libs/bufferToBase64.js
Normal file
@@ -0,0 +1,7 @@
|
||||
'use strict';
|
||||
export function bufferToBase64 (buf) {
|
||||
const binstr = Array.prototype.map.call(buf, ch => {
|
||||
return String.fromCharCode(ch);
|
||||
}).join('');
|
||||
return btoa(binstr);
|
||||
}
|
12
src/common/libs/formatBytes.js
Normal file
@@ -0,0 +1,12 @@
|
||||
'use strict';
|
||||
export function formatBytes (bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
@@ -1,13 +1,10 @@
|
||||
export function uidGen () {
|
||||
return Math.random().toString(36).substr(2, 9).toUpperCase();
|
||||
};
|
||||
|
||||
'use strict';
|
||||
export function mimeFromHex (hex) {
|
||||
switch (hex.substring(0, 4)) { // 2 bytes
|
||||
case '424D':
|
||||
return { ext: 'bmp', mime: 'image/bmp' };
|
||||
case '1F8B':
|
||||
return { ext: 'gz', mime: 'application/gzip' };
|
||||
return { ext: 'tar.gz', mime: 'application/gzip' };
|
||||
case '0B77':
|
||||
return { ext: 'ac3', mime: 'audio/vnd.dolby.dd-raw' };
|
||||
case '7801':
|
||||
@@ -20,7 +17,7 @@ export function mimeFromHex (hex) {
|
||||
default:
|
||||
switch (hex.substring(0, 6)) { // 3 bytes
|
||||
case 'FFD8FF':
|
||||
return { ext: 'jpj', mime: 'image/jpeg' };
|
||||
return { ext: 'jpg', mime: 'image/jpeg' };
|
||||
case '4949BC':
|
||||
return { ext: 'jxr', mime: 'image/vnd.ms-photo' };
|
||||
case '425A68':
|
||||
@@ -39,21 +36,11 @@ export function mimeFromHex (hex) {
|
||||
return { ext: 'bpg', mime: 'image/bpg' };
|
||||
case '4D4D002A':
|
||||
return { ext: 'tif', mime: 'image/tiff' };
|
||||
case '00000100':
|
||||
return { ext: 'ico', mime: 'image/x-icon' };
|
||||
default:
|
||||
return { ext: '???', mime: 'unknown ' + hex };
|
||||
return { ext: '', mime: 'unknown ' + hex };
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function formatBytes (bytes, decimals = 2) {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const dm = decimals < 0 ? 0 : decimals;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||
}
|
19
src/common/libs/sqlEscaper.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/* eslint-disable no-useless-escape */
|
||||
// eslint-disable-next-line no-control-regex
|
||||
const regex = new RegExp(/[\0\x08\x09\x1a\n\r"'\\\%]/gm);
|
||||
|
||||
/**
|
||||
* Escapes a string
|
||||
*
|
||||
* @param {String} string
|
||||
* @returns {String}
|
||||
*/
|
||||
function sqlEscaper (string) {
|
||||
return string.replace(regex, char => {
|
||||
const m = ['\\0', '\\x08', '\\x09', '\\x1a', '\\n', '\\r', '\'', '"', '\\', '\\\\', '%'];
|
||||
const r = ['\\\\0', '\\\\b', '\\\\t', '\\\\z', '\\\\n', '\\\\r', '\'\'', '""', '\\\\', '\\\\\\\\', '\\%'];
|
||||
return r[m.indexOf(char)] || char;
|
||||
});
|
||||
}
|
||||
|
||||
export { sqlEscaper };
|
8
src/common/libs/uidGen.js
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* @export
|
||||
* @param {String} [prefix]
|
||||
* @returns {String} Unique ID
|
||||
*/
|
||||
export function uidGen (prefix) {
|
||||
return (prefix ? `${prefix}:` : '') + Math.random().toString(36).substr(2, 9).toUpperCase();
|
||||
};
|
@@ -3,44 +3,39 @@
|
||||
import { app, BrowserWindow, nativeImage } from 'electron';
|
||||
import * as path from 'path';
|
||||
import { format as formatUrl } from 'url';
|
||||
|
||||
import ipcHandlers from './ipc-handlers';
|
||||
|
||||
const isDevelopment = process.env.NODE_ENV !== 'production';
|
||||
const gotTheLock = app.requestSingleInstanceLock();
|
||||
|
||||
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = 'true';
|
||||
|
||||
// global reference to mainWindow (necessary to prevent window from being garbage collected)
|
||||
let mainWindow;
|
||||
|
||||
function createMainWindow () {
|
||||
async function createMainWindow () {
|
||||
const icon = require('../renderer/images/logo-32.png');
|
||||
const window = new BrowserWindow({
|
||||
width: 1600,
|
||||
height: 1000,
|
||||
minHeight: 550,
|
||||
width: 1024,
|
||||
height: 800,
|
||||
minWidth: 900,
|
||||
minHeight: 550,
|
||||
title: 'Antares',
|
||||
autoHideMenuBar: true,
|
||||
icon: nativeImage.createFromDataURL(icon.default),
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
'web-security': false
|
||||
'web-security': false,
|
||||
enableRemoteModule: true,
|
||||
spellcheck: false
|
||||
},
|
||||
frame: false,
|
||||
backgroundColor: '#1d1d1d'
|
||||
});
|
||||
|
||||
if (isDevelopment)
|
||||
window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
|
||||
else {
|
||||
window.loadURL(formatUrl({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file',
|
||||
slashes: true
|
||||
}));
|
||||
}
|
||||
|
||||
if (isDevelopment) {
|
||||
await window.loadURL(`http://localhost:${process.env.ELECTRON_WEBPACK_WDS_PORT}`);
|
||||
|
||||
const { default: installExtension, VUEJS_DEVTOOLS } = require('electron-devtools-installer');
|
||||
window.webContents.openDevTools();
|
||||
|
||||
@@ -52,6 +47,13 @@ function createMainWindow () {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
else {
|
||||
await window.loadURL(formatUrl({
|
||||
pathname: path.join(__dirname, 'index.html'),
|
||||
protocol: 'file',
|
||||
slashes: true
|
||||
}));
|
||||
}
|
||||
|
||||
window.on('closed', () => {
|
||||
mainWindow = null;
|
||||
@@ -64,26 +66,30 @@ function createMainWindow () {
|
||||
});
|
||||
});
|
||||
|
||||
// Initialize ipcHandlers
|
||||
ipcHandlers();
|
||||
|
||||
return window;
|
||||
};
|
||||
|
||||
// quit application when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// on macOS it is common for applications to stay open until the user explicitly quits
|
||||
if (process.platform !== 'darwin')
|
||||
app.quit();
|
||||
});
|
||||
if (!gotTheLock)
|
||||
app.quit();
|
||||
else {
|
||||
// Initialize ipcHandlers
|
||||
ipcHandlers();
|
||||
|
||||
app.on('activate', () => {
|
||||
// on macOS it is common to re-create a window even after all windows have been closed
|
||||
if (mainWindow === null)
|
||||
// quit application when all windows are closed
|
||||
app.on('window-all-closed', () => {
|
||||
// on macOS it is common for applications to stay open until the user explicitly quits
|
||||
if (process.platform !== 'darwin')
|
||||
app.quit();
|
||||
});
|
||||
|
||||
app.on('activate', () => {
|
||||
// on macOS it is common to re-create a window even after all windows have been closed
|
||||
if (mainWindow === null)
|
||||
mainWindow = createMainWindow();
|
||||
});
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', () => {
|
||||
mainWindow = createMainWindow();
|
||||
});
|
||||
|
||||
// create main BrowserWindow when electron is ready
|
||||
app.on('ready', () => {
|
||||
mainWindow = createMainWindow();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
7
src/main/ipc-handlers/application.js
Normal file
@@ -0,0 +1,7 @@
|
||||
import { app, ipcMain } from 'electron';
|
||||
|
||||
export default () => {
|
||||
ipcMain.on('close-app', () => {
|
||||
app.exit();
|
||||
});
|
||||
};
|
@@ -1,12 +1,10 @@
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
import { AntaresConnector } from '../libs/AntaresConnector';
|
||||
import InformationSchema from '../models/InformationSchema';
|
||||
import Generic from '../models/Generic';
|
||||
import { ClientsFactory } from '../libs/ClientsFactory';
|
||||
|
||||
export default (connections) => {
|
||||
ipcMain.handle('testConnection', async (event, conn) => {
|
||||
const Connection = new AntaresConnector({
|
||||
export default connections => {
|
||||
ipcMain.handle('test-connection', async (event, conn) => {
|
||||
const connection = ClientsFactory.getConnection({
|
||||
client: conn.client,
|
||||
params: {
|
||||
host: conn.host,
|
||||
@@ -16,10 +14,11 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
await Connection.connect();
|
||||
await connection.connect();
|
||||
|
||||
try {
|
||||
await InformationSchema.testConnection(Connection);
|
||||
await connection.select('1+1').run();
|
||||
connection.destroy();
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
@@ -28,12 +27,12 @@ export default (connections) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('checkConnection', async (event, uid) => {
|
||||
ipcMain.handle('check-connection', async (event, uid) => {
|
||||
return uid in connections;
|
||||
});
|
||||
|
||||
ipcMain.handle('connect', async (event, conn) => {
|
||||
const Connection = new AntaresConnector({
|
||||
const connection = ClientsFactory.getConnection({
|
||||
client: conn.client,
|
||||
params: {
|
||||
host: conn.host,
|
||||
@@ -41,14 +40,16 @@ export default (connections) => {
|
||||
user: conn.user,
|
||||
password: conn.password
|
||||
},
|
||||
poolSize: 3
|
||||
poolSize: 1
|
||||
});
|
||||
|
||||
try {
|
||||
await Connection.connect();
|
||||
await connection.connect();
|
||||
|
||||
const structure = await connection.getStructure();
|
||||
|
||||
connections[conn.uid] = connection;
|
||||
|
||||
const { rows: structure } = await InformationSchema.getStructure(Connection);
|
||||
connections[conn.uid] = Connection;
|
||||
return { status: 'success', response: structure };
|
||||
}
|
||||
catch (err) {
|
||||
@@ -60,25 +61,4 @@ export default (connections) => {
|
||||
connections[uid].destroy();
|
||||
delete connections[uid];
|
||||
});
|
||||
|
||||
ipcMain.handle('refresh', async (event, uid) => {
|
||||
try {
|
||||
const { rows: structure } = await InformationSchema.getStructure(connections[uid]);
|
||||
return { status: 'success', response: structure };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('rawQuery', async (event, { uid, query, schema }) => {
|
||||
if (!query) return;
|
||||
try {
|
||||
const result = await Generic.raw(connections[uid], query, schema);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
110
src/main/ipc-handlers/database.js
Normal file
@@ -0,0 +1,110 @@
|
||||
|
||||
import { ipcMain } from 'electron';
|
||||
|
||||
export default connections => {
|
||||
ipcMain.handle('create-database', async (event, params) => {
|
||||
try {
|
||||
const query = `CREATE DATABASE \`${params.name}\` COLLATE ${params.collation}`;
|
||||
await connections[params.uid].raw(query);
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('update-database', async (event, params) => {
|
||||
try {
|
||||
const query = `ALTER DATABASE \`${params.name}\` COLLATE ${params.collation}`;
|
||||
await connections[params.uid].raw(query);
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-database', async (event, params) => {
|
||||
try {
|
||||
const query = `DROP DATABASE \`${params.database}\``;
|
||||
await connections[params.uid].raw(query);
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-database-collation', async (event, params) => {
|
||||
try {
|
||||
const query = `SELECT \`DEFAULT_COLLATION_NAME\` FROM \`information_schema\`.\`SCHEMATA\` WHERE \`SCHEMA_NAME\`='${params.database}'`;
|
||||
const collation = await connections[params.uid].raw(query);
|
||||
|
||||
return { status: 'success', response: collation.rows.length ? collation.rows[0].DEFAULT_COLLATION_NAME : '' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-structure', async (event, uid) => {
|
||||
try {
|
||||
const structure = await connections[uid].getStructure();
|
||||
|
||||
return { status: 'success', response: structure };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-collations', async (event, uid) => {
|
||||
try {
|
||||
const result = await connections[uid].getCollations();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-variables', async (event, uid) => {
|
||||
try {
|
||||
const result = await connections[uid].getVariables();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('use-schema', async (event, { uid, schema }) => {
|
||||
if (!schema) return;
|
||||
|
||||
try {
|
||||
await connections[uid].use(schema);
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('raw-query', async (event, { uid, query, schema }) => {
|
||||
if (!query) return;
|
||||
|
||||
try {
|
||||
const result = await connections[uid].raw(query, true);
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
@@ -1,11 +1,15 @@
|
||||
import connection from './connection';
|
||||
import structure from './structure';
|
||||
import tables from './tables';
|
||||
import updates from './updates';
|
||||
import application from './application';
|
||||
import database from './database';
|
||||
|
||||
const connections = {};
|
||||
|
||||
export default () => {
|
||||
connection(connections);
|
||||
structure(connections);
|
||||
tables(connections);
|
||||
database(connections);
|
||||
updates();
|
||||
application();
|
||||
};
|
||||
|
@@ -1,27 +0,0 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import InformationSchema from '../models/InformationSchema';
|
||||
import Generic from '../models/Generic';
|
||||
|
||||
// TODO: remap objects based on client
|
||||
|
||||
export default (connections) => {
|
||||
ipcMain.handle('getTableColumns', async (event, { uid, schema, table }) => {
|
||||
try {
|
||||
const result = await InformationSchema.getTableColumns(connections[uid], schema, table);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('getTableData', async (event, { uid, schema, table }) => {
|
||||
try {
|
||||
const result = await Generic.getTableData(connections[uid], schema, table);
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
202
src/main/ipc-handlers/tables.js
Normal file
@@ -0,0 +1,202 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { sqlEscaper } from 'common/libs/sqlEscaper';
|
||||
import { TEXT, LONG_TEXT, NUMBER, BLOB } from 'common/fieldTypes';
|
||||
import fs from 'fs';
|
||||
|
||||
// TODO: remap objects based on client
|
||||
|
||||
export default (connections) => {
|
||||
ipcMain.handle('get-table-columns', async (event, { uid, schema, table }) => {
|
||||
try {
|
||||
const { rows } = await connections[uid]
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('COLUMNS')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
|
||||
.orderBy({ ORDINAL_POSITION: 'ASC' })
|
||||
.run();
|
||||
|
||||
const result = rows.map(field => {
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: field.DATA_TYPE,
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
numPrecision: field.NUMERIC_PRECISION,
|
||||
datePrecision: field.DATETIME_PRECISION,
|
||||
charLength: field.CHARACTER_MAXIMUM_LENGTH,
|
||||
isNullable: field.IS_NULLABLE,
|
||||
default: field.COLUMN_DEFAULT,
|
||||
charset: field.CHARACTER_SET_NAME,
|
||||
collation: field.COLLATION_NAME,
|
||||
autoIncrement: field.EXTRA.includes('auto_increment'),
|
||||
comment: field.COLUMN_COMMENT
|
||||
};
|
||||
});
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-table-data', async (event, { uid, schema, table }) => {
|
||||
try {
|
||||
const result = await connections[uid]
|
||||
.select('*')
|
||||
.schema(schema)
|
||||
.from(table)
|
||||
.limit(1000)
|
||||
.run();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-key-usage', async (event, { uid, schema, table }) => {
|
||||
try {
|
||||
const { rows } = await connections[uid]
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('KEY_COLUMN_USAGE')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'`, REFERENCED_TABLE_NAME: 'IS NOT NULL' })
|
||||
.run();
|
||||
|
||||
const result = rows.map(field => {
|
||||
return {
|
||||
schema: field.TABLE_SCHEMA,
|
||||
table: field.TABLE_NAME,
|
||||
column: field.COLUMN_NAME,
|
||||
position: field.ORDINAL_POSITION,
|
||||
constraintPosition: field.POSITION_IN_UNIQUE_CONSTRAINT,
|
||||
constraintName: field.CONSTRAINT_NAME,
|
||||
refSchema: field.REFERENCED_TABLE_SCHEMA,
|
||||
refTable: field.REFERENCED_TABLE_NAME,
|
||||
refColumn: field.REFERENCED_COLUMN_NAME
|
||||
};
|
||||
});
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('update-table-cell', async (event, params) => {
|
||||
try {
|
||||
let escapedParam;
|
||||
let reload = false;
|
||||
const id = typeof params.id === 'number' ? params.id : `"${params.id}"`;
|
||||
|
||||
if (NUMBER.includes(params.type))
|
||||
escapedParam = params.content;
|
||||
else if ([...TEXT, ...LONG_TEXT].includes(params.type))
|
||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||
else if (BLOB.includes(params.type)) {
|
||||
if (params.content) {
|
||||
const fileBlob = fs.readFileSync(params.content);
|
||||
escapedParam = `0x${fileBlob.toString('hex')}`;
|
||||
reload = true;
|
||||
}
|
||||
else
|
||||
escapedParam = '""';
|
||||
}
|
||||
else
|
||||
escapedParam = `"${sqlEscaper(params.content)}"`;
|
||||
|
||||
await connections[params.uid]
|
||||
.update({ [params.field]: `= ${escapedParam}` })
|
||||
.schema(params.schema)
|
||||
.from(params.table)
|
||||
.where({ [params.primary]: `= ${id}` })
|
||||
.run();
|
||||
|
||||
return { status: 'success', response: { reload } };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('delete-table-rows', async (event, params) => {
|
||||
try {
|
||||
const result = await connections[params.uid]
|
||||
.schema(params.schema)
|
||||
.delete(params.table)
|
||||
.where({ [params.primary]: `IN (${params.rows.join(',')})` })
|
||||
.run();
|
||||
|
||||
return { status: 'success', response: result };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('insert-table-rows', async (event, params) => {
|
||||
try {
|
||||
const insertObj = {};
|
||||
for (const key in params.row) {
|
||||
const type = params.fields[key];
|
||||
let escapedParam;
|
||||
|
||||
if (params.row[key] === null)
|
||||
escapedParam = 'NULL';
|
||||
else if (NUMBER.includes(type))
|
||||
escapedParam = params.row[key];
|
||||
else if ([...TEXT, ...LONG_TEXT].includes(type))
|
||||
escapedParam = `"${sqlEscaper(params.row[key])}"`;
|
||||
else if (BLOB.includes(type)) {
|
||||
if (params.row[key]) {
|
||||
const fileBlob = fs.readFileSync(params.row[key]);
|
||||
escapedParam = `0x${fileBlob.toString('hex')}`;
|
||||
}
|
||||
else
|
||||
escapedParam = '""';
|
||||
}
|
||||
else
|
||||
escapedParam = `"${sqlEscaper(params.row[key])}"`;
|
||||
|
||||
insertObj[key] = escapedParam;
|
||||
}
|
||||
|
||||
for (let i = 0; i < params.repeat; i++) {
|
||||
await connections[params.uid]
|
||||
.schema(params.schema)
|
||||
.into(params.table)
|
||||
.insert(insertObj)
|
||||
.run();
|
||||
}
|
||||
|
||||
return { status: 'success' };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('get-foreign-list', async (event, params) => {
|
||||
try {
|
||||
const query = connections[params.uid]
|
||||
.select(`${params.column} AS foreignColumn`)
|
||||
.schema(params.schema)
|
||||
.from(params.table)
|
||||
.orderBy('foreignColumn ASC');
|
||||
|
||||
if (params.description)
|
||||
query.select(`LEFT(${params.description}, 20) AS foreignDescription`);
|
||||
|
||||
const results = await query.run();
|
||||
|
||||
return { status: 'success', response: results };
|
||||
}
|
||||
catch (err) {
|
||||
return { status: 'error', response: err.toString() };
|
||||
}
|
||||
});
|
||||
};
|
@@ -2,39 +2,40 @@ import { ipcMain } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
|
||||
let mainWindow;
|
||||
autoUpdater.allowPrerelease = true;
|
||||
|
||||
export default () => {
|
||||
ipcMain.on('checkForUpdates', event => {
|
||||
ipcMain.on('check-for-updates', event => {
|
||||
mainWindow = event;
|
||||
|
||||
autoUpdater.checkForUpdatesAndNotify().catch(() => {
|
||||
mainWindow.reply('checkFailed');
|
||||
mainWindow.reply('check-failed');
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on('restartToUpdate', () => {
|
||||
ipcMain.on('restart-to-update', () => {
|
||||
autoUpdater.quitAndInstall();
|
||||
});
|
||||
|
||||
// auto-updater events
|
||||
autoUpdater.on('checking-for-update', () => {
|
||||
mainWindow.reply('checkingForUpdate');
|
||||
mainWindow.reply('checking-for-update');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-available', () => {
|
||||
mainWindow.reply('updateAvailable');
|
||||
mainWindow.reply('update-available');
|
||||
});
|
||||
|
||||
autoUpdater.on('update-not-available', () => {
|
||||
mainWindow.reply('updateNotAvailable');
|
||||
mainWindow.reply('update-not-available');
|
||||
});
|
||||
|
||||
autoUpdater.on('download-progress', (data) => {
|
||||
mainWindow.reply('downloadProgress', data);
|
||||
autoUpdater.on('download-progress', data => {
|
||||
mainWindow.reply('download-progress', data);
|
||||
});
|
||||
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
mainWindow.reply('updateDownloaded');
|
||||
mainWindow.reply('update-downloaded');
|
||||
});
|
||||
|
||||
autoUpdater.logger = require('electron-log');
|
||||
|
@@ -1,268 +0,0 @@
|
||||
'use strict';
|
||||
import mysql from 'mysql';
|
||||
import mssql from 'mssql';
|
||||
// import pg from 'pg'; TODO: PostgreSQL
|
||||
|
||||
/**
|
||||
* As Simple As Possible Query Builder
|
||||
*
|
||||
* @export
|
||||
* @class AntaresConnector
|
||||
*/
|
||||
export class AntaresConnector {
|
||||
/**
|
||||
*Creates an instance of AntaresConnector.
|
||||
* @param {Object} args connection params
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
constructor (args) {
|
||||
this._client = args.client;
|
||||
this._params = args.params;
|
||||
this._poolSize = args.poolSize || false;
|
||||
this._connection = null;
|
||||
|
||||
this._queryDefaults = {
|
||||
schema: '',
|
||||
select: [],
|
||||
from: '',
|
||||
where: [],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
limit: [],
|
||||
join: [],
|
||||
update: [],
|
||||
insert: [],
|
||||
delete: []
|
||||
};
|
||||
this._query = Object.assign({}, this._queryDefaults);
|
||||
}
|
||||
|
||||
_reducer (acc, curr) {
|
||||
const type = typeof curr;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
return [...acc, curr];
|
||||
case 'object':
|
||||
if (Array.isArray(curr))
|
||||
return [...acc, ...curr];
|
||||
else {
|
||||
const clausoles = [];
|
||||
for (const key in curr)
|
||||
clausoles.push(`${key} ${curr[key]}`);
|
||||
|
||||
return clausoles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the query object after a query
|
||||
*
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
_resetQuery () {
|
||||
this._query = Object.assign({}, this._queryDefaults);
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
async connect () {
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
if (!this._poolSize) {
|
||||
const connection = mysql.createConnection(this._params);
|
||||
this._connection = connection.promise();
|
||||
}
|
||||
else
|
||||
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
|
||||
// this._connection = pool.promise();
|
||||
|
||||
break;
|
||||
case 'mssql': {
|
||||
const mssqlParams = {
|
||||
user: this._params.user,
|
||||
password: this._params.password,
|
||||
server: this._params.host
|
||||
};
|
||||
this._connection = await mssql.connect(mssqlParams);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
schema (schema) {
|
||||
this._query.schema = schema;
|
||||
return this;
|
||||
}
|
||||
|
||||
select (...args) {
|
||||
this._query.select = [...this._query.select, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
from (table) {
|
||||
this._query.from = table;
|
||||
return this;
|
||||
}
|
||||
|
||||
where (...args) {
|
||||
this._query.where = [...this._query.where, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
groupBy (...args) {
|
||||
this._query.groupBy = [...this._query.groupBy, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
orderBy (...args) {
|
||||
this._query.orderBy = [...this._query.orderBy, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
limit (...args) {
|
||||
this._query.limit = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
use (schema) {
|
||||
let sql;
|
||||
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
sql = `USE \`${schema}\``;
|
||||
break;
|
||||
case 'mssql':
|
||||
sql = `USE "${schema}"`;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return this.raw(sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} SQL string
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
getSQL () {
|
||||
// SELECT
|
||||
const selectArray = this._query.select.reduce(this._reducer, []);
|
||||
let selectRaw;
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
|
||||
break;
|
||||
case 'mssql': {
|
||||
const topRaw = this._query.limit.length ? ` TOP (${this._query.limit[0]}) ` : '';
|
||||
selectRaw = selectArray.length ? `SELECT${topRaw} ${selectArray.join(', ')} ` : 'SELECT * ';
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// FROM
|
||||
let fromRaw;
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
fromRaw = this._query.from ? `FROM ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : '';
|
||||
break;
|
||||
case 'mssql':
|
||||
fromRaw = this._query.from ? `FROM ${this._query.schema ? `${this._query.schema}.` : ''}${this._query.from} ` : '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
const whereArray = this._query.where.reduce(this._reducer, []);
|
||||
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
|
||||
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
|
||||
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
|
||||
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
|
||||
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
|
||||
|
||||
// LIMIT
|
||||
let limitRaw;
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
|
||||
break;
|
||||
case 'mssql':
|
||||
limitRaw = '';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return `${selectRaw}${fromRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
async run () {
|
||||
const rawQuery = this.getSQL();
|
||||
this._resetQuery();
|
||||
return this.raw(rawQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sql raw SQL query
|
||||
* @returns {Promise}
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
async raw (sql) {
|
||||
if (process.env.NODE_ENV === 'development') console.log(sql);
|
||||
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql': {
|
||||
const { rows, fields } = await new Promise((resolve, reject) => {
|
||||
this._connection.query(sql, (err, rows, fields) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else
|
||||
resolve({ rows, fields });
|
||||
});
|
||||
});
|
||||
return { rows, fields };
|
||||
}
|
||||
case 'mssql': {
|
||||
const results = await this._connection.request().query(sql);
|
||||
return { rows: results.recordsets[0] };// TODO: fields
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof AntaresConnector
|
||||
*/
|
||||
destroy () {
|
||||
switch (this._client) {
|
||||
case 'maria':
|
||||
case 'mysql':
|
||||
this._connection.end();
|
||||
break;
|
||||
case 'mssql':
|
||||
this._connection.close();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
141
src/main/libs/AntaresCore.js
Normal file
@@ -0,0 +1,141 @@
|
||||
'use strict';
|
||||
/**
|
||||
* As Simple As Possible Query Builder Core
|
||||
*
|
||||
* @class AntaresCore
|
||||
*/
|
||||
export class AntaresCore {
|
||||
/**
|
||||
* Creates an instance of AntaresCore.
|
||||
*
|
||||
* @param {Object} args connection params
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
constructor (args) {
|
||||
this._client = args.client;
|
||||
this._params = args.params;
|
||||
this._poolSize = args.poolSize || false;
|
||||
this._connection = null;
|
||||
this._logger = args.logger || console.log;
|
||||
|
||||
this._queryDefaults = {
|
||||
schema: '',
|
||||
select: [],
|
||||
from: '',
|
||||
where: [],
|
||||
groupBy: [],
|
||||
orderBy: [],
|
||||
limit: [],
|
||||
join: [],
|
||||
update: [],
|
||||
insert: {},
|
||||
delete: false
|
||||
};
|
||||
this._query = Object.assign({}, this._queryDefaults);
|
||||
}
|
||||
|
||||
_reducer (acc, curr) {
|
||||
const type = typeof curr;
|
||||
|
||||
switch (type) {
|
||||
case 'number':
|
||||
case 'string':
|
||||
return [...acc, curr];
|
||||
case 'object':
|
||||
if (Array.isArray(curr))
|
||||
return [...acc, ...curr];
|
||||
else {
|
||||
const clausoles = [];
|
||||
for (const key in curr)
|
||||
clausoles.push(`${key} ${curr[key]}`);
|
||||
|
||||
return clausoles;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the query object after a query
|
||||
*
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
_resetQuery () {
|
||||
this._query = Object.assign({}, this._queryDefaults);
|
||||
}
|
||||
|
||||
schema (schema) {
|
||||
this._query.schema = schema;
|
||||
return this;
|
||||
}
|
||||
|
||||
select (...args) {
|
||||
this._query.select = [...this._query.select, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
from (table) {
|
||||
this._query.from = table;
|
||||
return this;
|
||||
}
|
||||
|
||||
into (table) {
|
||||
this._query.from = table;
|
||||
return this;
|
||||
}
|
||||
|
||||
delete (table) {
|
||||
this._query.delete = true;
|
||||
this.from(table);
|
||||
return this;
|
||||
}
|
||||
|
||||
where (...args) {
|
||||
this._query.where = [...this._query.where, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
groupBy (...args) {
|
||||
this._query.groupBy = [...this._query.groupBy, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
orderBy (...args) {
|
||||
this._query.orderBy = [...this._query.orderBy, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
limit (...args) {
|
||||
this._query.limit = args;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {String | Array} args field = value
|
||||
* @returns
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
update (...args) {
|
||||
this._query.update = [...this._query.update, ...args];
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Object} obj field: value
|
||||
* @returns
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
insert (obj) {
|
||||
this._query.insert = { ...this._query.insert, ...obj };
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise}
|
||||
* @memberof AntaresCore
|
||||
*/
|
||||
async run () {
|
||||
const rawQuery = this.getSQL();
|
||||
this._resetQuery();
|
||||
return this.raw(rawQuery);
|
||||
}
|
||||
}
|
27
src/main/libs/ClientsFactory.js
Normal file
@@ -0,0 +1,27 @@
|
||||
'use strict';
|
||||
import { MySQLClient } from './clients/MySQLClient';
|
||||
|
||||
export class ClientsFactory {
|
||||
/**
|
||||
* Returns a database connection based on received args.
|
||||
*
|
||||
* @param {Object} args
|
||||
* @param {String} args.client
|
||||
* @param {Object} args.params
|
||||
* @param {String} args.params.host
|
||||
* @param {Number} args.params.port
|
||||
* @param {String} args.params.password
|
||||
* @param {Number=} args.poolSize
|
||||
* @returns Database Connection
|
||||
* @memberof ClientsFactory
|
||||
*/
|
||||
static getConnection (args) {
|
||||
switch (args.client) {
|
||||
case 'mysql':
|
||||
case 'maria':
|
||||
return new MySQLClient(args);
|
||||
default:
|
||||
return new Error(`Unknown database client: ${args.client}`);
|
||||
}
|
||||
}
|
||||
}
|
210
src/main/libs/clients/MySQLClient.js
Normal file
@@ -0,0 +1,210 @@
|
||||
'use strict';
|
||||
import mysql from 'mysql';
|
||||
import { AntaresCore } from '../AntaresCore';
|
||||
|
||||
export class MySQLClient extends AntaresCore {
|
||||
/**
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async connect () {
|
||||
if (!this._poolSize)
|
||||
this._connection = mysql.createConnection(this._params);
|
||||
else
|
||||
this._connection = mysql.createPool({ ...this._params, connectionLimit: this._poolSize });
|
||||
}
|
||||
|
||||
/**
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
destroy () {
|
||||
this._connection.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes an USE query
|
||||
*
|
||||
* @param {String} schema
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
use (schema) {
|
||||
return this.raw(`USE \`${schema}\``);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Array.<Object>} databases scructure
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getStructure () {
|
||||
const { rows: databases } = await this.raw('SHOW DATABASES');
|
||||
// TODO: SHOW TABLE STATUS FROM `{DATABASE_NAME}`;
|
||||
|
||||
const { rows: tables } = await this
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('TABLES')
|
||||
.orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' })
|
||||
.run();
|
||||
|
||||
const { rows: functions } = await this.raw('SHOW FUNCTION STATUS');
|
||||
const { rows: procedures } = await this.raw('SHOW PROCEDURE STATUS');
|
||||
const { rows: schedulers } = await this.raw('SELECT *, EVENT_SCHEMA AS `Db`, EVENT_NAME AS `Name` FROM information_schema.`EVENTS`');
|
||||
|
||||
const triggersArr = [];
|
||||
for (const db of databases) {
|
||||
let { rows: triggers } = await this.raw(`SHOW TRIGGERS FROM \`${db.Database}\``);
|
||||
if (triggers.length) {
|
||||
triggers = triggers.map(trigger => {
|
||||
trigger.Db = db.Database;
|
||||
return trigger;
|
||||
});
|
||||
triggersArr.push(...triggers);
|
||||
}
|
||||
}
|
||||
|
||||
return databases.map(db => { // TODO: remap all objects,
|
||||
return {
|
||||
name: db.Database,
|
||||
tables: tables.filter(table => table.TABLE_SCHEMA === db.Database),
|
||||
functions: functions.filter(func => func.Db === db.Database),
|
||||
procedures: procedures.filter(procedure => procedure.Db === db.Database),
|
||||
triggers: triggersArr.filter(trigger => trigger.Db === db.Database),
|
||||
schedulers: schedulers.filter(scheduler => scheduler.Db === db.Database)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW COLLATION
|
||||
*
|
||||
* @returns {Array.<Object>} collations list
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getCollations () {
|
||||
const results = await this.raw('SHOW COLLATION');
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
charset: row.Charset,
|
||||
collation: row.Collation,
|
||||
compiled: row.Compiled.includes('Yes'),
|
||||
default: row.Default.includes('Yes'),
|
||||
id: row.Id,
|
||||
sortLen: row.Sortlen
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SHOW VARIABLES
|
||||
*
|
||||
* @returns {Array.<Object>} variables list
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async getVariables () {
|
||||
const sql = 'SHOW VARIABLES';
|
||||
const results = await this.raw(sql);
|
||||
|
||||
return results.rows.map(row => {
|
||||
return {
|
||||
name: row.Variable_name,
|
||||
value: row.Value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {String} SQL string
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
getSQL () {
|
||||
// SELECT
|
||||
const selectArray = this._query.select.reduce(this._reducer, []);
|
||||
let selectRaw = '';
|
||||
|
||||
if (selectArray.length)
|
||||
selectRaw = selectArray.length ? `SELECT ${selectArray.join(', ')} ` : 'SELECT * ';
|
||||
|
||||
// FROM
|
||||
let fromRaw = '';
|
||||
|
||||
if (!this._query.update.length && !Object.keys(this._query.insert).length && !!this._query.from)
|
||||
fromRaw = 'FROM';
|
||||
else if (Object.keys(this._query.insert).length)
|
||||
fromRaw = 'INTO';
|
||||
|
||||
fromRaw += this._query.from ? ` ${this._query.schema ? `\`${this._query.schema}\`.` : ''}\`${this._query.from}\` ` : '';
|
||||
|
||||
// WHERE
|
||||
const whereArray = this._query.where.reduce(this._reducer, []);
|
||||
const whereRaw = whereArray.length ? `WHERE ${whereArray.join(' AND ')} ` : '';
|
||||
|
||||
// UPDATE
|
||||
const updateArray = this._query.update.reduce(this._reducer, []);
|
||||
const updateRaw = updateArray.length ? `SET ${updateArray.join(', ')} ` : '';
|
||||
|
||||
// INSERT
|
||||
let insertRaw = '';
|
||||
|
||||
if (Object.keys(this._query.insert).length) {
|
||||
const fieldsList = [];
|
||||
const valueList = [];
|
||||
const fields = this._query.insert;
|
||||
|
||||
for (const key in fields) {
|
||||
if (fields[key] === null) continue;
|
||||
fieldsList.push(key);
|
||||
valueList.push(fields[key]);
|
||||
}
|
||||
|
||||
insertRaw = `(${fieldsList.join(', ')}) VALUES (${valueList.join(', ')}) `;
|
||||
}
|
||||
|
||||
// GROUP BY
|
||||
const groupByArray = this._query.groupBy.reduce(this._reducer, []);
|
||||
const groupByRaw = groupByArray.length ? `GROUP BY ${groupByArray.join(', ')} ` : '';
|
||||
|
||||
// ORDER BY
|
||||
const orderByArray = this._query.orderBy.reduce(this._reducer, []);
|
||||
const orderByRaw = orderByArray.length ? `ORDER BY ${orderByArray.join(', ')} ` : '';
|
||||
|
||||
// LIMIT
|
||||
const limitRaw = this._query.limit.length ? `LIMIT ${this._query.limit.join(', ')} ` : '';
|
||||
|
||||
return `${selectRaw}${updateRaw ? 'UPDATE' : ''}${insertRaw ? 'INSERT ' : ''}${this._query.delete ? 'DELETE ' : ''}${fromRaw}${updateRaw}${whereRaw}${groupByRaw}${orderByRaw}${limitRaw}${insertRaw}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} sql raw SQL query
|
||||
* @param {boolean} [nest]
|
||||
* @returns {Promise}
|
||||
* @memberof MySQLClient
|
||||
*/
|
||||
async raw (sql, nest) {
|
||||
const nestTables = nest ? '.' : false;
|
||||
const resultsArr = [];
|
||||
const queries = sql.split(';');
|
||||
|
||||
if (process.env.NODE_ENV === 'development') this._logger(sql);// TODO: replace BLOB content with a placeholder
|
||||
|
||||
for (const query of queries) {
|
||||
if (!query) continue;
|
||||
|
||||
const { rows, report, fields } = await new Promise((resolve, reject) => {
|
||||
this._connection.query({ sql: query, nestTables }, (err, response, fields) => {
|
||||
if (err)
|
||||
reject(err);
|
||||
else {
|
||||
resolve({
|
||||
rows: Array.isArray(response) ? response : false,
|
||||
report: !Array.isArray(response) ? response : false,
|
||||
fields
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
resultsArr.push({ rows, report, fields });
|
||||
}
|
||||
|
||||
return resultsArr.length === 1 ? resultsArr[0] : resultsArr;
|
||||
}
|
||||
}
|
@@ -1,23 +0,0 @@
|
||||
'use strict';
|
||||
export default class {
|
||||
static async raw (connection, query, schema) {
|
||||
if (schema) {
|
||||
try {
|
||||
await connection.use(schema);
|
||||
}
|
||||
catch (err) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return connection.raw(query);
|
||||
}
|
||||
|
||||
static async getTableData (connection, schema, table) {
|
||||
return connection
|
||||
.select('*')
|
||||
.schema(schema)
|
||||
.from(table)
|
||||
.limit(1000)
|
||||
.run();
|
||||
}
|
||||
}
|
@@ -1,25 +0,0 @@
|
||||
'use strict';
|
||||
export default class {
|
||||
static testConnection (connection) {
|
||||
return connection.select('1+1').run();
|
||||
}
|
||||
|
||||
static getStructure (connection) {
|
||||
return connection
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('TABLES')
|
||||
.orderBy({ TABLE_SCHEMA: 'ASC', TABLE_NAME: 'ASC' })
|
||||
.run();
|
||||
}
|
||||
|
||||
static getTableColumns (connection, schema, table) {
|
||||
return connection
|
||||
.select('*')
|
||||
.schema('information_schema')
|
||||
.from('COLUMNS')
|
||||
.where({ TABLE_SCHEMA: `= '${schema}'`, TABLE_NAME: `= '${table}'` })
|
||||
.orderBy({ ORDINAL_POSITION: 'ASC' })
|
||||
.run();
|
||||
}
|
||||
}
|
@@ -4,7 +4,7 @@
|
||||
<div id="window-content">
|
||||
<TheSettingBar />
|
||||
<div id="main-content" class="container">
|
||||
<TheAppWelcome v-if="!connections.length" @newConn="showNewConnModal" />
|
||||
<TheAppWelcome v-if="!connections.length" @new-conn="showNewConnModal" />
|
||||
<div v-else class="columns col-gapless">
|
||||
<Workspace
|
||||
v-for="connection in connections"
|
||||
@@ -40,8 +40,7 @@ export default {
|
||||
ModalSettings: () => import(/* webpackChunkName: "ModalSettings" */'@/components/ModalSettings')
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
};
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
@@ -53,7 +52,7 @@ export default {
|
||||
})
|
||||
},
|
||||
mounted () {
|
||||
ipcRenderer.send('checkForUpdates');
|
||||
ipcRenderer.send('check-for-updates');
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
@@ -64,30 +63,30 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
html,
|
||||
body{
|
||||
height: 100%;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#wrapper{
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
#wrapper {
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#window-content{
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#window-content {
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#main-content {
|
||||
padding: 0;
|
||||
justify-content: flex-start;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
width: calc(100% - #{$settingbar-width});
|
||||
#main-content {
|
||||
padding: 0;
|
||||
justify-content: flex-start;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
width: calc(100% - #{$settingbar-width});
|
||||
|
||||
> .columns{
|
||||
height: calc(100vh - #{$footer-height});
|
||||
}
|
||||
}
|
||||
> .columns {
|
||||
height: calc(100vh - #{$footer-height});
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="modal modal-sm active">
|
||||
<div class="modal active" :class="modalSizeClass">
|
||||
<a class="modal-overlay" @click="hideModal" />
|
||||
<div class="modal-container">
|
||||
<div v-if="hasHeader" class="modal-header">
|
||||
<div v-if="hasHeader" class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
@@ -27,15 +27,15 @@
|
||||
<div class="modal-footer">
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
@click="confirmModal"
|
||||
@click.stop="confirmModal"
|
||||
>
|
||||
{{ $t('word.confirm') }}
|
||||
{{ confirmText || $t('word.confirm') }}
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link"
|
||||
@click="hideModal"
|
||||
>
|
||||
{{ $t('word.cancel') }}
|
||||
{{ cancelText || $t('word.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,6 +45,15 @@
|
||||
<script>
|
||||
export default {
|
||||
name: 'BaseConfirmModal',
|
||||
props: {
|
||||
size: {
|
||||
type: String,
|
||||
validator: prop => ['small', 'medium', 'large'].includes(prop),
|
||||
default: 'small'
|
||||
},
|
||||
confirmText: String,
|
||||
cancelText: String
|
||||
},
|
||||
computed: {
|
||||
hasHeader () {
|
||||
return !!this.$slots.header;
|
||||
@@ -54,6 +63,13 @@ export default {
|
||||
},
|
||||
hasDefault () {
|
||||
return !!this.$slots.default;
|
||||
},
|
||||
modalSizeClass () {
|
||||
if (this.size === 'small')
|
||||
return 'modal-sm';
|
||||
else if (this.size === 'large')
|
||||
return 'modal-lg';
|
||||
else return '';
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -70,7 +86,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal.modal-sm .modal-container{
|
||||
padding: 0;
|
||||
}
|
||||
.modal.modal-sm .modal-container {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
@@ -31,63 +31,65 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
close () {
|
||||
this.$emit('closeContext');
|
||||
this.$emit('close-context');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.context{
|
||||
.context {
|
||||
display: flex;
|
||||
color: $body-font-color;
|
||||
font-size: 16px;
|
||||
z-index: 400;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
height: 100vh;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.context-container {
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
z-index: 10;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
padding: 0;
|
||||
background: #1d1d1d;
|
||||
border-radius: 0.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
pointer-events: initial;
|
||||
|
||||
.context-element {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem 0.3rem;
|
||||
cursor: pointer;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
background: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-overlay {
|
||||
background: transparent;
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
z-index: 400;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
padding: 0.4rem;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
pointer-events: none;
|
||||
|
||||
.context-container{
|
||||
min-width: 100px;
|
||||
max-width: 150px;
|
||||
z-index: 1;
|
||||
box-shadow: 0px 0px 1px 0px #000;
|
||||
padding: 0;
|
||||
background: #1d1d1d;
|
||||
border-radius: 0.1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
pointer-events: initial;
|
||||
|
||||
.context-element{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .1rem .3rem;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover{
|
||||
background: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-overlay{
|
||||
background: transparent;
|
||||
bottom: 0;
|
||||
cursor: default;
|
||||
display: block;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,14 +1,15 @@
|
||||
<template>
|
||||
<div class="toast mt-2" :class="notificationStatus.className">
|
||||
<span class="p-vcentered text-left" :class="{'expanded': isExpanded}">
|
||||
<i class="material-icons mr-1">{{ notificationStatus.iconName }}</i>
|
||||
<i class="mdi mdi-24px mr-2" :class="notificationStatus.iconName" />
|
||||
<span class="notification-message">{{ message }}</span>
|
||||
</span>
|
||||
<i
|
||||
v-if="isExpandable"
|
||||
class="material-icons c-hand"
|
||||
class="mdi mdi-24px c-hand expand-btn"
|
||||
:class="isExpanded ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
@click="toggleExpand"
|
||||
>{{ isExpanded ? 'expand_less' : 'expand_more' }}</i>
|
||||
/>
|
||||
<button class="btn btn-clear ml-2" @click="hideToast" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -38,19 +39,19 @@ export default {
|
||||
switch (this.status) {
|
||||
case 'success':
|
||||
className = 'toast-success';
|
||||
iconName = 'done';
|
||||
iconName = 'mdi-check';
|
||||
break;
|
||||
case 'error':
|
||||
className = 'toast-error';
|
||||
iconName = 'error';
|
||||
iconName = 'mdi-alert-rhombus';
|
||||
break;
|
||||
case 'warning':
|
||||
className = 'toast-warning';
|
||||
iconName = 'warning';
|
||||
iconName = 'mdi-alert';
|
||||
break;
|
||||
case 'primary':
|
||||
className = 'toast-primary';
|
||||
iconName = 'info_outline';
|
||||
iconName = 'mdi-information-outline';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -71,25 +72,28 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.toast{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
width: fit-content;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.notification-message{
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: 30rem;
|
||||
user-select: none;
|
||||
}
|
||||
.notification-message {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-block;
|
||||
max-width: 30rem;
|
||||
}
|
||||
|
||||
.expanded .notification-message{
|
||||
white-space: initial;
|
||||
}
|
||||
.expand-btn {
|
||||
align-items: initial;
|
||||
}
|
||||
|
||||
.expanded .notification-message {
|
||||
white-space: initial;
|
||||
}
|
||||
</style>
|
||||
|
@@ -34,19 +34,19 @@ export default {
|
||||
switch (this.status) {
|
||||
case 'success':
|
||||
className = 'toast-success';
|
||||
iconTag = '<i class="material-icons mr-1">done</i>';
|
||||
iconTag = '<i class="mdi mdi-24px mdi-check mr-1"></i>';
|
||||
break;
|
||||
case 'error':
|
||||
className = 'toast-error';
|
||||
iconTag = '<i class="material-icons mr-1">error</i>';
|
||||
iconTag = '<i class="mdi mdi-24px mdi-alert-rhombus mr-1"></i>';
|
||||
break;
|
||||
case 'warning':
|
||||
className = 'toast-warning';
|
||||
iconTag = '<i class="material-icons mr-1">warning</i>';
|
||||
iconTag = '<i class="mdi mdi-24px mdi-alert mr-1"></i>';
|
||||
break;
|
||||
case 'primary':
|
||||
className = 'toast-primary';
|
||||
iconTag = '<i class="material-icons mr-1">info_outline</i>';
|
||||
iconTag = '<i class="mdi mdi-24px mdi-information-outline mr-1"></i>';
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -70,10 +70,10 @@ export default {
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.toast{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
}
|
||||
.toast {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
user-select: text;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
@@ -21,47 +21,50 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// credits: https://github.com/xrado 👼
|
||||
export default {
|
||||
name: 'BaseVirtualScroll',
|
||||
props: {
|
||||
items: Array,
|
||||
itemHeight: Number
|
||||
itemHeight: Number,
|
||||
visibleHeight: Number,
|
||||
scrollElement: {
|
||||
type: HTMLDivElement,
|
||||
default: null
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
topHeight: 0,
|
||||
bottomHeight: 0,
|
||||
visibleItems: []
|
||||
visibleItems: [],
|
||||
renderTimeout: null,
|
||||
localScrollElement: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this._checkScrollPosition = this.checkScrollPosition.bind(this);
|
||||
this.checkScrollPosition();
|
||||
this.$el.addEventListener('scroll', this._checkScrollPosition);
|
||||
this.$el.addEventListener('wheel', this._checkScrollPosition);
|
||||
this.localScrollElement = this.scrollElement ? this.scrollElement : this.$el;
|
||||
this.updateWindow();
|
||||
this.localScrollElement.addEventListener('scroll', this._checkScrollPosition);
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.$el.removeEventListener('scroll', this._checkScrollPosition);
|
||||
this.$el.removeEventListener('wheel', this._checkScrollPosition);
|
||||
this.localScrollElement.removeEventListener('scroll', this._checkScrollPosition);
|
||||
},
|
||||
methods: {
|
||||
checkScrollPosition (e = {}) {
|
||||
const el = this.$el;
|
||||
checkScrollPosition (e) {
|
||||
clearTimeout(this.renderTimeout);
|
||||
|
||||
// prevent parent scroll
|
||||
if ((el.scrollTop === 0 && e.deltaY < 0) || (Math.abs(el.scrollTop - (el.scrollHeight - el.clientHeight)) <= 1 && e.deltaY > 0))
|
||||
e.preventDefault();
|
||||
|
||||
this.updateWindow(e);
|
||||
this.renderTimeout = setTimeout(() => {
|
||||
this.updateWindow(e);
|
||||
}, 200);
|
||||
},
|
||||
|
||||
updateWindow (e) {
|
||||
const visibleItemsCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
|
||||
const visibleItemsCount = Math.ceil(this.visibleHeight / this.itemHeight);
|
||||
const totalScrollHeight = this.items.length * this.itemHeight;
|
||||
const offset = 50;
|
||||
|
||||
const scrollTop = this.localScrollElement.scrollTop;
|
||||
|
||||
const scrollTop = this.$el.scrollTop;
|
||||
const offset = 5;
|
||||
const firstVisibleIndex = Math.floor(scrollTop / this.itemHeight);
|
||||
const lastVisibleIndex = firstVisibleIndex + visibleItemsCount;
|
||||
const firstCutIndex = Math.max(firstVisibleIndex - offset, 0);
|
||||
|
89
src/renderer/components/ForeignKeySelect.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<select
|
||||
ref="editField"
|
||||
class="px-1"
|
||||
@change="onChange"
|
||||
@blur="$emit('blur')"
|
||||
>
|
||||
<option
|
||||
v-for="row in foreignList"
|
||||
:key="row.foreignColumn"
|
||||
:value="row.foreignColumn"
|
||||
:selected="row.foreignColumn === value"
|
||||
>
|
||||
{{ row.foreignColumn }} {{ 'foreignDescription' in row ? ` - ${row.foreignDescription}` : '' | cutText }}
|
||||
</option>
|
||||
</select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { TEXT, LONG_TEXT } from 'common/fieldTypes';
|
||||
export default {
|
||||
name: 'ForeignKeySelect',
|
||||
filters: {
|
||||
cutText (val) {
|
||||
if (typeof val !== 'string') return val;
|
||||
return val.length > 15 ? `${val.substring(0, 15)}...` : val;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: [String, Number],
|
||||
keyUsage: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
foreignList: []
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected'
|
||||
})
|
||||
},
|
||||
async created () {
|
||||
let firstTextField;
|
||||
const params = {
|
||||
uid: this.selectedWorkspace,
|
||||
schema: this.keyUsage.refSchema,
|
||||
table: this.keyUsage.refTable
|
||||
};
|
||||
|
||||
try { // Field data
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
if (status === 'success')
|
||||
firstTextField = response.find(field => [...TEXT, ...LONG_TEXT].includes(field.type)).name || false;
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Foregn list
|
||||
const { status, response } = await Tables.getForeignList({
|
||||
...params,
|
||||
column: this.keyUsage.refColumn,
|
||||
description: firstTextField
|
||||
});
|
||||
|
||||
if (status === 'success')
|
||||
this.foreignList = response.rows;
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
onChange () {
|
||||
this.$emit('update:value', this.$refs.editField.value);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -2,13 +2,15 @@
|
||||
<div class="modal active modal-sm">
|
||||
<a class="modal-overlay" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
{{ $t('word.credentials') }}
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-key-variant mr-1" /> {{ $t('word.credentials') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
@@ -63,7 +65,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
closeModal () {
|
||||
this.$emit('closeAsking');
|
||||
this.$emit('close-asking');
|
||||
},
|
||||
sendCredentials () {
|
||||
this.$emit('credentials', this.credentials);
|
||||
@@ -71,7 +73,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
@@ -2,13 +2,15 @@
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay c-hand" @click="closeModal" />
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
{{ $t('message.editConnection') }}
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-server mr-1" /> {{ $t('message.editConnection') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<fieldset class="m-0" :disabled="isTesting">
|
||||
@@ -36,7 +38,7 @@
|
||||
<option value="maria">
|
||||
MariaDB
|
||||
</option>
|
||||
<option value="mssql">
|
||||
<!-- <option value="mssql">
|
||||
Microsoft SQL
|
||||
</option>
|
||||
<option value="pg">
|
||||
@@ -44,7 +46,7 @@
|
||||
</option>
|
||||
<option value="oracledb">
|
||||
Oracle DB
|
||||
</option>
|
||||
</option> -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -111,13 +113,13 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<BaseToast
|
||||
class="mb-2"
|
||||
:message="toast.message"
|
||||
:status="toast.status"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<button
|
||||
class="btn btn-gray mr-2"
|
||||
:class="{'loading': isTesting}"
|
||||
@@ -135,7 +137,7 @@
|
||||
</div>
|
||||
<ModalAskCredentials
|
||||
v-if="isAsking"
|
||||
@closeAsking="closeAsking"
|
||||
@close-asking="closeAsking"
|
||||
@credentials="continueTest"
|
||||
/>
|
||||
</div>
|
||||
@@ -230,7 +232,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container{
|
||||
max-width: 450px;
|
||||
}
|
||||
.modal-container {
|
||||
max-width: 450px;
|
||||
}
|
||||
</style>
|
||||
|
151
src/renderer/components/ModalEditDatabase.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-edit mr-1" /> {{ $t('message.editDatabase') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('word.name') }}:</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input
|
||||
v-model="database.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
required
|
||||
:placeholder="$t('message.databaseName')"
|
||||
readonly
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('word.collation') }}:</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<select v-model="database.collation" class="form-select">
|
||||
<option
|
||||
v-for="collation in collations"
|
||||
:key="collation.id"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<button class="btn btn-primary mr-2" @click.stop="updateDatabase">
|
||||
{{ $t('word.update') }}
|
||||
</button>
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Database from '@/ipc-api/Database';
|
||||
|
||||
export default {
|
||||
name: 'ModalEditDatabase',
|
||||
props: {
|
||||
selectedDatabase: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
database: {
|
||||
name: '',
|
||||
prevName: '',
|
||||
collation: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
collations () {
|
||||
return this.getWorkspace(this.selectedWorkspace).collations;
|
||||
},
|
||||
defaultCollation () {
|
||||
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
|
||||
}
|
||||
},
|
||||
async created () {
|
||||
let actualCollation;
|
||||
try {
|
||||
const { status, response } = await Database.getDatabaseCollation({ uid: this.selectedWorkspace, database: this.selectedDatabase });
|
||||
|
||||
if (status === 'success')
|
||||
actualCollation = response;
|
||||
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.database = {
|
||||
name: this.selectedDatabase,
|
||||
prevName: this.selectedDatabase,
|
||||
collation: actualCollation || this.defaultCollation,
|
||||
prevCollation: actualCollation || this.defaultCollation
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
async updateDatabase () {
|
||||
if (this.database.collation !== this.database.prevCollation) {
|
||||
try {
|
||||
const { status, response } = await Database.updateDatabase({
|
||||
uid: this.selectedWorkspace,
|
||||
...this.database
|
||||
});
|
||||
|
||||
if (status === 'success')
|
||||
this.closeModal();
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
else
|
||||
this.closeModal();
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container {
|
||||
max-width: 360px;
|
||||
}
|
||||
</style>
|
@@ -2,13 +2,15 @@
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay c-hand" @click="closeModal" />
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
{{ $t('message.createNewConnection') }}
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-server-plus mr-1" /> {{ $t('message.createNewConnection') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<fieldset class="m-0" :disabled="isTesting">
|
||||
@@ -40,7 +42,7 @@
|
||||
<option value="maria">
|
||||
MariaDB
|
||||
</option>
|
||||
<option value="mssql">
|
||||
<!-- <option value="mssql">
|
||||
Microsoft SQL
|
||||
</option>
|
||||
<option value="pg">
|
||||
@@ -48,7 +50,7 @@
|
||||
</option>
|
||||
<option value="oracledb">
|
||||
Oracle DB
|
||||
</option>
|
||||
</option> -->
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
@@ -115,13 +117,13 @@
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<BaseToast
|
||||
class="mb-2"
|
||||
:message="toast.message"
|
||||
:status="toast.status"
|
||||
/>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<button
|
||||
class="btn btn-gray mr-2"
|
||||
:class="{'loading': isTesting}"
|
||||
@@ -139,7 +141,7 @@
|
||||
</div>
|
||||
<ModalAskCredentials
|
||||
v-if="isAsking"
|
||||
@closeAsking="closeAsking"
|
||||
@close-asking="closeAsking"
|
||||
@credentials="continueTest"
|
||||
/>
|
||||
</div>
|
||||
@@ -148,7 +150,7 @@
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import Connection from '@/ipc-api/Connection';
|
||||
import { uidGen } from 'common/libs/utilities';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import ModalAskCredentials from '@/components/ModalAskCredentials';
|
||||
import BaseToast from '@/components/BaseToast';
|
||||
|
||||
@@ -168,7 +170,7 @@ export default {
|
||||
user: 'root',
|
||||
password: '',
|
||||
ask: false,
|
||||
uid: uidGen()
|
||||
uid: uidGen('C')
|
||||
},
|
||||
toast: {
|
||||
status: '',
|
||||
@@ -253,7 +255,7 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container{
|
||||
max-width: 450px;
|
||||
}
|
||||
.modal-container {
|
||||
max-width: 450px;
|
||||
}
|
||||
</style>
|
||||
|
125
src/renderer/components/ModalNewDatabase.vue
Normal file
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-plus mr-1" /> {{ $t('message.createNewDatabase') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('word.name') }}:</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<input
|
||||
v-model="database.name"
|
||||
class="form-input"
|
||||
type="text"
|
||||
required
|
||||
:placeholder="$t('message.databaseName')"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-3">
|
||||
<label class="form-label">{{ $t('word.collation') }}:</label>
|
||||
</div>
|
||||
<div class="col-9">
|
||||
<select v-model="database.collation" class="form-select">
|
||||
<option
|
||||
v-for="collation in collations"
|
||||
:key="collation.id"
|
||||
:value="collation.collation"
|
||||
>
|
||||
{{ collation.collation }}
|
||||
</option>
|
||||
</select>
|
||||
<small>{{ $t('message.serverDefault') }}: {{ defaultCollation }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<button class="btn btn-primary mr-2" @click.stop="createDatabase">
|
||||
{{ $t('word.add') }}
|
||||
</button>
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Database from '@/ipc-api/Database';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewDatabase',
|
||||
data () {
|
||||
return {
|
||||
database: {
|
||||
name: '',
|
||||
collation: ''
|
||||
}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getDatabaseVariable: 'workspaces/getDatabaseVariable'
|
||||
}),
|
||||
collations () {
|
||||
return this.getWorkspace(this.selectedWorkspace).collations;
|
||||
},
|
||||
defaultCollation () {
|
||||
return this.getDatabaseVariable(this.selectedWorkspace, 'collation_server').value || '';
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.database = { ...this.database, collation: this.defaultCollation };
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
async createDatabase () {
|
||||
try {
|
||||
const { status, response } = await Database.createDatabase({
|
||||
uid: this.selectedWorkspace,
|
||||
...this.database
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
this.closeModal();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('close');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container {
|
||||
max-width: 360px;
|
||||
}
|
||||
</style>
|
326
src/renderer/components/ModalNewTableRow.vue
Normal file
@@ -0,0 +1,326 @@
|
||||
<template>
|
||||
<div class="modal active">
|
||||
<a class="modal-overlay" @click.stop="closeModal" />
|
||||
<div class="modal-container p-0">
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-plus mr-1" /> {{ $t('message.addNewRow') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click.stop="closeModal" />
|
||||
</div>
|
||||
<div class="modal-body pb-0">
|
||||
<div class="content">
|
||||
<form class="form-horizontal">
|
||||
<fieldset :disabled="isInserting">
|
||||
<div
|
||||
v-for="(field, key) in fields"
|
||||
:key="field.name"
|
||||
class="form-group"
|
||||
>
|
||||
<div class="col-4 col-sm-12">
|
||||
<label class="form-label" :title="field.name">{{ field.name }}</label>
|
||||
</div>
|
||||
<div class="input-group col-8 col-sm-12">
|
||||
<ForeignKeySelect
|
||||
v-if="foreignKeys.includes(field.name)"
|
||||
class="form-select"
|
||||
:value.sync="localRow[field.name]"
|
||||
:key-usage="getKeyUsage(field.name)"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
/>
|
||||
<input
|
||||
v-else-if="inputProps(field).mask"
|
||||
v-model="localRow[field.name]"
|
||||
v-mask="inputProps(field).mask"
|
||||
class="form-input"
|
||||
:type="inputProps(field).type"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
:tabindex="key+1"
|
||||
>
|
||||
<input
|
||||
v-else-if="inputProps(field).type === 'file'"
|
||||
class="form-input"
|
||||
type="file"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
:tabindex="key+1"
|
||||
@change="filesChange($event,field.name)"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
v-model="localRow[field.name]"
|
||||
class="form-input"
|
||||
:type="inputProps(field).type"
|
||||
:disabled="fieldsToExclude.includes(field.name)"
|
||||
:tabindex="key+1"
|
||||
>
|
||||
<span class="input-group-addon" :class="`type-${field.type}`">
|
||||
{{ field.type }} {{ fieldLength(field) | wrapNumber }}
|
||||
</span>
|
||||
<label class="form-checkbox ml-3" :title="$t('word.insert')">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="!field.autoIncrement"
|
||||
@change.prevent="toggleFields($event, field)"
|
||||
><i class="form-icon" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer text-light">
|
||||
<div class="input-group col-3 tooltip tooltip-right" :data-tooltip="$t('message.numberOfInserts')">
|
||||
<input
|
||||
v-model="nInserts"
|
||||
type="number"
|
||||
class="form-input"
|
||||
min="1"
|
||||
:disabled="isInserting"
|
||||
>
|
||||
<span class="input-group-addon">
|
||||
<i class="mdi mdi-24px mdi-repeat" />
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
class="btn btn-primary mr-2"
|
||||
:class="{'loading': isInserting}"
|
||||
@click.stop="insertRows"
|
||||
>
|
||||
{{ $t('word.insert') }}
|
||||
</button>
|
||||
<button class="btn btn-link" @click.stop="closeModal">
|
||||
{{ $t('word.close') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import ForeignKeySelect from '@/components/ForeignKeySelect';
|
||||
|
||||
export default {
|
||||
name: 'ModalNewTableRow',
|
||||
components: {
|
||||
ForeignKeySelect
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
},
|
||||
filters: {
|
||||
wrapNumber (num) {
|
||||
if (!num) return '';
|
||||
return `(${num})`;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
connection: Object,
|
||||
tabUid: [String, Number]
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
localRow: {},
|
||||
fieldsToExclude: [],
|
||||
nInserts: 1,
|
||||
isInserting: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace',
|
||||
getWorkspaceTab: 'workspaces/getWorkspaceTab'
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.selectedWorkspace);
|
||||
},
|
||||
foreignKeys () {
|
||||
return this.keyUsage.map(key => key.column);
|
||||
},
|
||||
fields () {
|
||||
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).fields[0] : [];
|
||||
},
|
||||
keyUsage () {
|
||||
return this.getWorkspaceTab(this.tabUid) ? this.getWorkspaceTab(this.tabUid).keyUsage[0] : [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
nInserts (val) {
|
||||
if (!val || val < 1)
|
||||
this.nInserts = 1;
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const rowObj = {};
|
||||
|
||||
for (const field of this.fields) {
|
||||
let fieldDefault;
|
||||
|
||||
if (field.default === 'NULL') fieldDefault = null;
|
||||
else {
|
||||
if (NUMBER.includes(field.type))
|
||||
fieldDefault = +field.default;
|
||||
|
||||
if ([...TEXT, ...LONG_TEXT].includes(field.type))
|
||||
fieldDefault = field.default ? field.default.substring(1, field.default.length - 1) : '';
|
||||
|
||||
if ([...TIME, ...DATE].includes(field.type))
|
||||
fieldDefault = field.default;
|
||||
|
||||
if (DATETIME.includes(field.type)) {
|
||||
if (field.default && field.default.toLowerCase().includes('current_timestamp')) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < field.datePrecision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
fieldDefault = moment().format(`YYYY-MM-DD HH:mm:ss${datePrecision}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rowObj[field.name] = fieldDefault;
|
||||
|
||||
if (field.autoIncrement)// Disable by default auto increment fields
|
||||
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
|
||||
}
|
||||
|
||||
this.localRow = { ...rowObj };
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
async insertRows () {
|
||||
this.isInserting = true;
|
||||
const rowToInsert = this.localRow;
|
||||
Object.keys(rowToInsert).forEach(key => {
|
||||
if (this.fieldsToExclude.includes(key))
|
||||
delete rowToInsert[key];
|
||||
if (typeof rowToInsert[key] === 'undefined')
|
||||
delete rowToInsert[key];
|
||||
});
|
||||
|
||||
const fieldTypes = {};
|
||||
this.fields.forEach(field => {
|
||||
fieldTypes[field.name] = field.type;
|
||||
});
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.insertTableRows({
|
||||
uid: this.selectedWorkspace,
|
||||
schema: this.workspace.breadcrumbs.schema,
|
||||
table: this.workspace.breadcrumbs.table,
|
||||
row: rowToInsert,
|
||||
repeat: this.nInserts,
|
||||
fields: fieldTypes
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
this.closeModal();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isInserting = false;
|
||||
},
|
||||
closeModal () {
|
||||
this.$emit('hide');
|
||||
},
|
||||
fieldLength (field) {
|
||||
if ([...BLOB, ...LONG_TEXT].includes(field.type)) return null;
|
||||
return field.numPrecision || field.datePrecision || field.charLength || 0;
|
||||
},
|
||||
inputProps (field) {
|
||||
if ([...TEXT, ...LONG_TEXT].includes(field.type))
|
||||
return { type: 'text', mask: false };
|
||||
|
||||
if (NUMBER.includes(field.type))
|
||||
return { type: 'number', mask: false };
|
||||
|
||||
if (TIME.includes(field.type)) {
|
||||
let timeMask = '##:##:##';
|
||||
const precision = this.fieldLength(field);
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
timeMask += i === 0 ? '.#' : '#';
|
||||
|
||||
return { type: 'text', mask: timeMask };
|
||||
}
|
||||
|
||||
if (DATE.includes(field.type))
|
||||
return { type: 'text', mask: '####-##-##' };
|
||||
|
||||
if (DATETIME.includes(field.type)) {
|
||||
let datetimeMask = '####-##-## ##:##:##';
|
||||
const precision = this.fieldLength(field);
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
datetimeMask += i === 0 ? '.#' : '#';
|
||||
|
||||
return { type: 'text', mask: datetimeMask };
|
||||
}
|
||||
|
||||
if (BLOB.includes(field.type))
|
||||
return { type: 'file', mask: false };
|
||||
|
||||
if (BIT.includes(field.type))
|
||||
return { type: 'text', mask: false };
|
||||
|
||||
return { type: 'text', mask: false };
|
||||
},
|
||||
toggleFields (event, field) {
|
||||
if (event.target.checked)
|
||||
this.fieldsToExclude = this.fieldsToExclude.filter(f => f !== field.name);
|
||||
else
|
||||
this.fieldsToExclude = [...this.fieldsToExclude, field.name];
|
||||
},
|
||||
filesChange (event, field) {
|
||||
const { files } = event.target;
|
||||
if (!files.length) return;
|
||||
|
||||
this.localRow[field] = files[0].path;
|
||||
},
|
||||
|
||||
getKeyUsage (keyName) {
|
||||
return this.keyUsage.find(key => key.column === keyName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-container {
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
overflow: hidden;
|
||||
white-space: normal;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.input-group-addon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
@@ -2,9 +2,12 @@
|
||||
<div id="settings" class="modal active">
|
||||
<a class="modal-overlay c-hand" @click="closeModal" />
|
||||
<div class="modal-container">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title h5">
|
||||
{{ $t('word.settings') }}
|
||||
<div class="modal-header pl-2">
|
||||
<div class="modal-title h6">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-cog mr-1" />
|
||||
{{ $t('word.settings') }}
|
||||
</div>
|
||||
</div>
|
||||
<a class="btn btn-clear c-hand" @click="closeModal" />
|
||||
</div>
|
||||
@@ -31,7 +34,7 @@
|
||||
:class="{'active': selectedTab === 'update'}"
|
||||
@click="selectTab('update')"
|
||||
>
|
||||
<a class="c-hand" :class="{'badge': isUpdate}">{{ $t('word.update') }}</a>
|
||||
<a class="c-hand" :class="{'badge badge-update': hasUpdates}">{{ $t('word.update') }}</a>
|
||||
</li>
|
||||
<li
|
||||
class="tab-item"
|
||||
@@ -45,11 +48,11 @@
|
||||
|
||||
<div v-if="selectedTab === 'general'" class="panel-body py-4">
|
||||
<form class="form-horizontal">
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="form-group">
|
||||
<div class="col-8 col-sm-12">
|
||||
<div class="form-group mb-4">
|
||||
<div class="col-6 col-sm-12">
|
||||
<label class="form-label">
|
||||
<i class="material-icons md-18 mr-1">translate</i>
|
||||
<i class="mdi mdi-18px mdi-translate mr-1" />
|
||||
{{ $t('word.language') }}:
|
||||
</label>
|
||||
</div>
|
||||
@@ -69,12 +72,33 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-6 col-sm-12">
|
||||
<label class="form-label">
|
||||
{{ $t('message.notificationsTimeout') }}:
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-6 col-sm-12">
|
||||
<div class="input-group">
|
||||
<input
|
||||
v-model="localTimeout"
|
||||
class="form-input"
|
||||
type="number"
|
||||
min="1"
|
||||
@focusout="checkNotificationsTimeout"
|
||||
>
|
||||
<span class="input-group-addon">{{ $t('word.seconds') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTab === 'themes'" class="panel-body py-4">
|
||||
<!-- -->
|
||||
<div class="text-center">
|
||||
<p>In future releases</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="selectedTab === 'update'" class="panel-body py-4">
|
||||
@@ -87,7 +111,7 @@
|
||||
<h4>{{ appName }}</h4>
|
||||
<p>
|
||||
{{ $t('word.version') }}: {{ appVersion }}<br>
|
||||
<a class="c-hand" @click="openOutside('https://github.com/Fabio286/antares')">GitHub</a><br>
|
||||
<a class="c-hand" @click="openOutside('https://github.com/EStarium/antares')">GitHub</a><br>
|
||||
<small>{{ $t('message.madeWithJS') }}</small>
|
||||
</p>
|
||||
</div>
|
||||
@@ -111,8 +135,8 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isUpdate: false,
|
||||
localLocale: null,
|
||||
localTimeout: null,
|
||||
selectedTab: 'general'
|
||||
};
|
||||
},
|
||||
@@ -121,7 +145,9 @@ export default {
|
||||
appName: 'application/appName',
|
||||
appVersion: 'application/appVersion',
|
||||
selectedSettingTab: 'application/selectedSettingTab',
|
||||
selectedLocale: 'settings/getLocale'
|
||||
selectedLocale: 'settings/getLocale',
|
||||
notificationsTimeout: 'settings/getNotificationsTimeout',
|
||||
updateStatus: 'application/getUpdateStatus'
|
||||
}),
|
||||
locales () {
|
||||
const locales = [];
|
||||
@@ -129,46 +155,61 @@ export default {
|
||||
locales.push({ code: locale, name: localesNames[locale] });
|
||||
|
||||
return locales;
|
||||
},
|
||||
hasUpdates () {
|
||||
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.localLocale = this.selectedLocale;
|
||||
this.localTimeout = this.notificationsTimeout;
|
||||
this.selectedTab = this.selectedSettingTab;
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
closeModal: 'application/hideSettingModal',
|
||||
changeLocale: 'settings/changeLocale'
|
||||
changeLocale: 'settings/changeLocale',
|
||||
updateNotificationsTimeout: 'settings/updateNotificationsTimeout'
|
||||
}),
|
||||
selectTab (tab) {
|
||||
this.selectedTab = tab;
|
||||
},
|
||||
openOutside (link) {
|
||||
shell.openExternal(link);
|
||||
},
|
||||
checkNotificationsTimeout () {
|
||||
if (!this.localTimeout)
|
||||
this.localTimeout = 10;
|
||||
|
||||
this.updateNotificationsTimeout(+this.localTimeout);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#settings{
|
||||
.modal-body{
|
||||
overflow: hidden;
|
||||
#settings {
|
||||
.modal-body {
|
||||
overflow: hidden;
|
||||
|
||||
.panel-body{
|
||||
height: calc(70vh - 70px);
|
||||
overflow: auto;
|
||||
}
|
||||
.panel-body {
|
||||
height: calc(70vh - 70px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.badge::after{
|
||||
background: #32b643;
|
||||
}
|
||||
.badge::after {
|
||||
background: #32b643;
|
||||
}
|
||||
|
||||
.form-label{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
.badge-update::after {
|
||||
bottom: initial;
|
||||
background: $primary-color;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="empty">
|
||||
<div class="empty-icon">
|
||||
<i class="material-icons md-48">system_update_alt</i>
|
||||
<i class="mdi mdi-48px mdi-cloud-download" />
|
||||
</div>
|
||||
<p class="empty-title h5">
|
||||
{{ updateMessage }}
|
||||
@@ -16,6 +16,9 @@
|
||||
{{ downloadPercentage }}%
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="updateStatus === 'available'">
|
||||
<progress class="progress" max="100" />
|
||||
</div>
|
||||
<div class="empty-action">
|
||||
<button
|
||||
v-if="['noupdate', 'checking', 'nocheck'].includes(updateStatus)"
|
||||
@@ -68,17 +71,17 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
checkForUpdates () {
|
||||
ipcRenderer.send('checkForUpdates');
|
||||
ipcRenderer.send('check-for-updates');
|
||||
},
|
||||
restartToUpdate () {
|
||||
ipcRenderer.send('restartToUpdate');
|
||||
ipcRenderer.send('restart-to-update');
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.empty{
|
||||
color: $body-font-color;
|
||||
.empty {
|
||||
color: $body-font-color;
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,25 +1,15 @@
|
||||
<template>
|
||||
<div class="editor-wrapper">
|
||||
<textarea
|
||||
ref="codemirror"
|
||||
:options="cmOptions"
|
||||
/>
|
||||
<div ref="editor" class="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import 'codemirror/lib/codemirror.css';
|
||||
import 'codemirror/theme/material-darker.css';
|
||||
import 'codemirror/mode/sql/sql';
|
||||
import 'codemirror/addon/edit/closebrackets';
|
||||
import 'codemirror/addon/selection/active-line';
|
||||
import 'codemirror/addon/hint/show-hint';
|
||||
import 'codemirror/addon/hint/show-hint.css';
|
||||
import 'codemirror/addon/hint/sql-hint';
|
||||
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||
import { completionItemProvider } from '@/suggestions/sql';
|
||||
|
||||
CodeMirror.defineOption('sql-hint');
|
||||
monaco.languages.registerCompletionItemProvider('sql', completionItemProvider(monaco));
|
||||
|
||||
export default {
|
||||
name: 'QueryEditor',
|
||||
@@ -28,62 +18,53 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
cminstance: null,
|
||||
content: '',
|
||||
cmOptions: {
|
||||
tabSize: 3,
|
||||
smartIndent: true,
|
||||
styleActiveLine: true,
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
mode: 'text/x-sql',
|
||||
theme: 'material-darker',
|
||||
extraKeys: {
|
||||
'Ctrl-Space': 'autocomplete'
|
||||
},
|
||||
hintOptions: {
|
||||
tables: {
|
||||
users: ['name', 'score', 'birthDate'],
|
||||
countries: ['name', 'population', 'size']
|
||||
}
|
||||
},
|
||||
autoCloseBrackets: true
|
||||
}
|
||||
editor: null
|
||||
};
|
||||
},
|
||||
mounted () {
|
||||
this.initialize();
|
||||
},
|
||||
methods: {
|
||||
initialize () {
|
||||
this.cminstance = CodeMirror.fromTextArea(this.$refs.codemirror, this.cmOptions);
|
||||
this.cminstance.setValue(this.value || this.content);
|
||||
this.editor = monaco.editor.create(this.$refs.editor, {
|
||||
value: this.value,
|
||||
language: 'sql',
|
||||
theme: 'vs-dark',
|
||||
autoIndent: true,
|
||||
minimap: {
|
||||
enabled: false
|
||||
},
|
||||
contextmenu: false,
|
||||
wordBasedSuggestions: true,
|
||||
acceptSuggestionOnEnter: 'smart',
|
||||
quickSuggestions: true
|
||||
});
|
||||
|
||||
this.cminstance.on('change', cm => {
|
||||
this.content = cm.getValue();
|
||||
this.$emit('input', this.content);
|
||||
});
|
||||
}
|
||||
this.editor.onDidChangeModelContent(e => {
|
||||
const content = this.editor.getValue();
|
||||
this.$emit('update:value', content);
|
||||
});
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.editor && this.editor.dispose();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editor-wrapper{
|
||||
border-bottom: 1px solid #444444;
|
||||
}
|
||||
.editor-wrapper {
|
||||
border-bottom: 1px solid #444;
|
||||
|
||||
.CodeMirror{
|
||||
.editor {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-scroll{
|
||||
max-width: 100%;
|
||||
}
|
||||
.CodeMirror {
|
||||
.CodeMirror-scroll {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.CodeMirror-line {
|
||||
word-break: break-word!important;
|
||||
white-space: pre-wrap!important;
|
||||
word-break: normal;
|
||||
}
|
||||
}
|
||||
.CodeMirror-line {
|
||||
word-break: break-word !important;
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@closeContext="$emit('closeContext')"
|
||||
@close-context="$emit('close-context')"
|
||||
>
|
||||
<div class="context-element" @click="showEditModal(contextConnection)">
|
||||
<i class="material-icons md-18 text-light pr-1">edit</i> {{ $t('word.edit') }}
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
|
||||
</div>
|
||||
<div class="context-element" @click="showConfirmModal">
|
||||
<i class="material-icons md-18 text-light pr-1">delete</i> {{ $t('word.delete') }}
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
@@ -16,11 +16,13 @@
|
||||
@hide="hideConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
{{ $t('message.deleteConnection') }}
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-server-remove mr-1" /> {{ $t('message.deleteConnection') }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteConnectionCorfirm') }} <b>{{ connectionName }}</b>?
|
||||
{{ $t('message.deleteCorfirm') }} <b>{{ connectionName }}</b>?
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
@@ -69,7 +71,3 @@ export default {
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
@@ -2,7 +2,7 @@
|
||||
<div class="columns">
|
||||
<div class="column col-12 empty text-light">
|
||||
<div class="empty-icon">
|
||||
<i class="material-icons md-48">mood</i>
|
||||
<i class="mdi mdi-48px mdi-emoticon" />
|
||||
</div>
|
||||
<p class="empty-title h5">
|
||||
{{ $t('message.appWelcome') }}
|
||||
@@ -11,7 +11,7 @@
|
||||
{{ $t('message.appFirstStep') }}
|
||||
</p>
|
||||
<div class="empty-action">
|
||||
<button class="btn btn-primary" @click="$emit('newConn')">
|
||||
<button class="btn btn-primary" @click="$emit('new-conn')">
|
||||
{{ $t('message.createConnection') }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -26,12 +26,12 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty{
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
.empty {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<div class="footer-left-elements">
|
||||
<ul class="footer-elements">
|
||||
<li class="footer-element">
|
||||
<i class="material-icons md-18 mr-1">memory</i>
|
||||
<i class="mdi mdi-18px mdi-memory mr-1" />
|
||||
<small>{{ appVersion }}</small>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -11,15 +11,15 @@
|
||||
|
||||
<div class="footer-right-elements">
|
||||
<ul class="footer-elements">
|
||||
<li class="footer-element footer-link">
|
||||
<i class="material-icons md-18 mr-1">favorite</i>
|
||||
<li class="footer-element footer-link" @click="openOutside('https://github.com/sponsors/Fabio286')">
|
||||
<i class="mdi mdi-18px mdi-coffee mr-1" />
|
||||
<small>{{ $t('word.donate') }}</small>
|
||||
</li>
|
||||
<li class="footer-element footer-link">
|
||||
<i class="material-icons md-18">bug_report</i>
|
||||
<li class="footer-element footer-link" @click="openOutside('https://github.com/EStarium/antares/issues')">
|
||||
<i class="mdi mdi-18px mdi-bug" />
|
||||
</li>
|
||||
<li class="footer-element footer-link" @click="showSettingModal('about')">
|
||||
<i class="material-icons md-18">info_outline</i>
|
||||
<i class="mdi mdi-18px mdi-information-outline" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
const { shell } = require('electron');
|
||||
|
||||
export default {
|
||||
name: 'TheFooter',
|
||||
@@ -40,47 +41,50 @@ export default {
|
||||
methods: {
|
||||
...mapActions({
|
||||
showSettingModal: 'application/showSettingModal'
|
||||
})
|
||||
}),
|
||||
openOutside (link) {
|
||||
shell.openExternal(link);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#footer{
|
||||
height: $footer-height;
|
||||
#footer {
|
||||
height: $footer-height;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $primary-color;
|
||||
padding: 0 0.2rem;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
|
||||
.footer-elements {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $primary-color;
|
||||
padding: 0 .2rem;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
box-shadow: 0 0 1px 0px #000;
|
||||
|
||||
.footer-elements{
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.footer-element {
|
||||
height: $footer-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 0.4rem;
|
||||
margin: 0;
|
||||
|
||||
.footer-element{
|
||||
height: $footer-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 .4rem;
|
||||
margin: 0;
|
||||
&.footer-link {
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
|
||||
&.footer-link{
|
||||
cursor: pointer;
|
||||
transition: background .2s;
|
||||
|
||||
&:hover{
|
||||
background: rgba($color: #fff, $alpha: .1);
|
||||
}
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
background: rgba($color: #fff, $alpha: 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -1,5 +1,9 @@
|
||||
<template>
|
||||
<div id="notifications-board">
|
||||
<div
|
||||
id="notifications-board"
|
||||
@mouseenter="clearTimeouts"
|
||||
@mouseleave="rearmTimeouts"
|
||||
>
|
||||
<transition-group name="slide-fade">
|
||||
<BaseNotification
|
||||
v-for="notification in latestNotifications"
|
||||
@@ -21,27 +25,60 @@ export default {
|
||||
components: {
|
||||
BaseNotification
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
timeouts: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
notifications: 'notifications/getNotifications'
|
||||
notifications: 'notifications/getNotifications',
|
||||
notificationsTimeout: 'settings/getNotificationsTimeout'
|
||||
}),
|
||||
latestNotifications () {
|
||||
return this.notifications.slice(0, 10);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
notifications: {
|
||||
deep: true,
|
||||
handler: function (notification) {
|
||||
if (notification.length) {
|
||||
this.timeouts[notification[0].uid] = setTimeout(() => {
|
||||
this.removeNotification(notification[0].uid);
|
||||
delete this.timeouts[notification.uid];
|
||||
}, this.notificationsTimeout * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
removeNotification: 'notifications/removeNotification'
|
||||
})
|
||||
}),
|
||||
clearTimeouts () {
|
||||
for (const uid in this.timeouts) {
|
||||
clearTimeout(this.timeouts[uid]);
|
||||
delete this.timeouts[uid];
|
||||
}
|
||||
},
|
||||
rearmTimeouts () {
|
||||
for (const notification of this.notifications) {
|
||||
this.timeouts[notification.uid] = setTimeout(() => {
|
||||
this.removeNotification(notification.uid);
|
||||
delete this.timeouts[notification.uid];
|
||||
}, this.notificationsTimeout * 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#notifications-board{
|
||||
position: absolute;
|
||||
z-index: 9;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
#notifications-board {
|
||||
position: absolute;
|
||||
z-index: 999;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
@@ -5,7 +5,7 @@
|
||||
v-if="isContext"
|
||||
:context-event="contextEvent"
|
||||
:context-connection="contextConnection"
|
||||
@closeContext="isContext = false"
|
||||
@close-context="isContext = false"
|
||||
/>
|
||||
<ul class="settingbar-elements">
|
||||
<draggable v-model="connections">
|
||||
@@ -28,7 +28,7 @@
|
||||
@click="showNewConnModal"
|
||||
@mouseover.self="tooltipPosition"
|
||||
>
|
||||
<i class="settingbar-element-icon material-icons text-light">add</i>
|
||||
<i class="settingbar-element-icon mdi mdi-24px mdi-plus text-light" />
|
||||
<span class="ex-tooltip-content">{{ $t('message.addConnection') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="settingbar-bottom-elements">
|
||||
<ul class="settingbar-elements">
|
||||
<li class="settingbar-element btn btn-link ex-tooltip" @click="showSettingModal('general')">
|
||||
<i class="settingbar-element-icon material-icons text-light">settings</i>
|
||||
<i class="settingbar-element-icon mdi mdi-24px mdi-cog text-light" :class="{' badge badge-update': hasUpdates}" />
|
||||
<span class="ex-tooltip-content">{{ $t('word.settings') }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -70,7 +70,8 @@ export default {
|
||||
getConnections: 'connections/getConnections',
|
||||
getConnectionName: 'connections/getConnectionName',
|
||||
connected: 'workspaces/getConnected',
|
||||
selectedWorkspace: 'workspaces/getSelected'
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
updateStatus: 'application/getUpdateStatus'
|
||||
}),
|
||||
connections: {
|
||||
get () {
|
||||
@@ -79,6 +80,9 @@ export default {
|
||||
set (value) {
|
||||
this.updateConnections(value);
|
||||
}
|
||||
},
|
||||
hasUpdates () {
|
||||
return ['available', 'downloading', 'downloaded'].includes(this.updateStatus);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
@@ -106,101 +110,104 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#settingbar{
|
||||
width: $settingbar-width;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
#settingbar {
|
||||
width: $settingbar-width;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: $bg-color-light;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
z-index: 9;
|
||||
|
||||
.settingbar-top-elements {
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.settingbar-bottom-elements {
|
||||
padding-top: 0.5rem;
|
||||
background: $bg-color-light;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.settingbar-elements {
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
width: $settingbar-width;
|
||||
padding: 0;
|
||||
box-shadow: 0 0 1px 0px #000;
|
||||
z-index: 9;
|
||||
margin: 0;
|
||||
|
||||
.settingbar-top-elements{
|
||||
overflow-x: hidden;
|
||||
overflow-y: overlay;
|
||||
max-height: calc((100vh - 3.5rem) - #{$excluding-size});
|
||||
.settingbar-element {
|
||||
height: $settingbar-width;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border-left: 3px solid transparent;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 3px;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
border-left-color: $body-font-color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settingbar-element-icon {
|
||||
&.badge::after {
|
||||
bottom: -10px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
background: $success-color;
|
||||
}
|
||||
|
||||
&.badge-update::after {
|
||||
bottom: initial;
|
||||
background: $primary-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.settingbar-bottom-elements{
|
||||
padding-top: .5rem;
|
||||
background: $bg-color-light;
|
||||
z-index: 1;
|
||||
}
|
||||
.ex-tooltip {// Because both overflow-x: visible and overflow-y:auto are evil!!!
|
||||
.ex-tooltip-content {
|
||||
z-index: 999;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
display: block;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
margin: 0 0 0 calc(#{$settingbar-width} - 5px);
|
||||
left: 0;
|
||||
padding: 0.2rem 0.4rem;
|
||||
font-size: 0.7rem;
|
||||
background: rgba(48, 55, 66, 0.95);
|
||||
border-radius: 0.1rem;
|
||||
color: #fff;
|
||||
max-width: 320px;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.settingbar-elements{
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
width: $settingbar-width;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.settingbar-element{
|
||||
height: $settingbar-width;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border-left: 3px solid transparent;
|
||||
opacity: .5;
|
||||
transition: opacity .2s;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
|
||||
&:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.selected{
|
||||
border-left-color: $body-font-color;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settingbar-element-icon{
|
||||
|
||||
&.badge::after{
|
||||
bottom: -10px;
|
||||
right: 0;
|
||||
position: absolute;
|
||||
background: $success-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.ex-tooltip{// Because both overflow-x: visible and overflow-y:auto are evil!!!
|
||||
.ex-tooltip-content{
|
||||
z-index: 999;
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
display:block;
|
||||
position:absolute;
|
||||
background-color:#feffe1;
|
||||
text-align: center;
|
||||
margin:.0 0 0 calc(#{$settingbar-width} - 5px);
|
||||
left: 0;
|
||||
padding: .2rem .4rem;
|
||||
font-size: .7rem;
|
||||
background: rgba(48,55,66,.95);
|
||||
border-radius: .1rem;
|
||||
color: #fff;
|
||||
max-width: 320px;
|
||||
pointer-events: none;
|
||||
text-overflow: ellipsis;
|
||||
transition: opacity .2s;
|
||||
}
|
||||
|
||||
&:hover .ex-tooltip-content{
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
&:hover .ex-tooltip-content {
|
||||
visibility: visible;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,8 +4,8 @@
|
||||
<div class="titlebar-elements">
|
||||
<img class="titlebar-logo" :src="require('@/images/logo.svg').default">
|
||||
</div>
|
||||
<div class="titlebar-elements">
|
||||
<!-- -->
|
||||
<div class="titlebar-elements titlebar-title">
|
||||
{{ windowTitle }}
|
||||
</div>
|
||||
<div class="titlebar-elements">
|
||||
<div
|
||||
@@ -13,31 +13,32 @@
|
||||
class="titlebar-element"
|
||||
@click="openDevTools"
|
||||
>
|
||||
<i class="material-icons">code</i>
|
||||
<i class="mdi mdi-24px mdi-code-tags" />
|
||||
</div>
|
||||
<div
|
||||
v-if="isDevelopment"
|
||||
class="titlebar-element"
|
||||
@click="reload"
|
||||
>
|
||||
<i class="material-icons">refresh</i>
|
||||
<i class="mdi mdi-24px mdi-refresh" />
|
||||
</div>
|
||||
<div class="titlebar-element" @click="minimizeApp">
|
||||
<i class="material-icons">remove</i>
|
||||
<i class="mdi mdi-24px mdi-minus" />
|
||||
</div>
|
||||
<div class="titlebar-element" @click="toggleFullScreen">
|
||||
<i v-if="isMaximized" class="material-icons">fullscreen_exit</i>
|
||||
<i v-else class="material-icons">fullscreen</i>
|
||||
<i v-if="isMaximized" class="mdi mdi-24px mdi-fullscreen-exit" />
|
||||
<i v-else class="mdi mdi-24px mdi-fullscreen" />
|
||||
</div>
|
||||
<div class="titlebar-element close-button" @click="closeApp">
|
||||
<i class="material-icons">close</i>
|
||||
<i class="mdi mdi-24px mdi-close" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { remote } from 'electron';
|
||||
import { remote, ipcRenderer } from 'electron';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'TheTitleBar',
|
||||
@@ -48,6 +49,22 @@ export default {
|
||||
isDevelopment: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getConnectionName: 'connections/getConnectionName',
|
||||
selectedWorkspace: 'workspaces/getSelected',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
windowTitle () {
|
||||
if (!this.selectedWorkspace) return '';
|
||||
|
||||
const connectionName = this.getConnectionName(this.selectedWorkspace);
|
||||
const workspace = this.getWorkspace(this.selectedWorkspace);
|
||||
const breadcrumbs = Object.values(workspace.breadcrumbs).filter(breadcrumb => breadcrumb);
|
||||
|
||||
return [connectionName, ...breadcrumbs].join(' • ');
|
||||
}
|
||||
},
|
||||
created () {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
},
|
||||
@@ -56,7 +73,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
closeApp () {
|
||||
this.w.close();
|
||||
ipcRenderer.send('close-app');
|
||||
},
|
||||
minimizeApp () {
|
||||
this.w.minimize();
|
||||
@@ -81,55 +98,64 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
#titlebar{
|
||||
#titlebar {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
background: $bg-color-light;
|
||||
align-items: center;
|
||||
height: $titlebar-height;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
z-index: 9999;
|
||||
|
||||
.titlebar-resizer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.titlebar-elements {
|
||||
display: flex;
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
background: $bg-color-light;
|
||||
align-items: center;
|
||||
height: $titlebar-height;
|
||||
-webkit-app-region: drag;
|
||||
user-select: none;
|
||||
box-shadow: 0 0 1px 0px #000;
|
||||
z-index: 9999;
|
||||
|
||||
.titlebar-resizer{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 4px;
|
||||
z-index: 999;
|
||||
-webkit-app-region: no-drag;
|
||||
&.titlebar-title {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
display: block;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.titlebar-elements{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.titlebar-logo{
|
||||
height: $titlebar-height;
|
||||
padding: 0 .4rem;
|
||||
}
|
||||
|
||||
.titlebar-element{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $titlebar-height;
|
||||
line-height: 0;
|
||||
padding: 0 .7rem;
|
||||
opacity: .7;
|
||||
transition: opacity .2s;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover{
|
||||
opacity: 1;
|
||||
background: rgba($color: #fff, $alpha: .2);
|
||||
}
|
||||
|
||||
&.close-button:hover{
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
.titlebar-logo {
|
||||
height: $titlebar-height;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.titlebar-element {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: $titlebar-height;
|
||||
line-height: 0;
|
||||
padding: 0 0.7rem;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: rgba($color: #fff, $alpha: 0.2);
|
||||
}
|
||||
|
||||
&.close-button:hover {
|
||||
background: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,37 +2,74 @@
|
||||
<div v-show="isSelected" class="workspace column columns col-gapless">
|
||||
<WorkspaceExploreBar :connection="connection" :is-selected="isSelected" />
|
||||
<div v-if="workspace.connected" class="workspace-tabs column columns col-gapless">
|
||||
<ul class="tab tab-block column col-12">
|
||||
<ul ref="tabWrap" class="tab tab-block column col-12">
|
||||
<li
|
||||
v-if="workspace.breadcrumbs.table"
|
||||
class="tab-item"
|
||||
:class="{'active': selectedTab === 1}"
|
||||
@click="selectTab({uid: workspace.uid, tab: 1})"
|
||||
:class="{'active': selectedTab === 'prop'}"
|
||||
@click="selectTab({uid: workspace.uid, tab: 'prop'})"
|
||||
>
|
||||
<a class="tab-link">
|
||||
<i class="material-icons md-18 mr-1">grid_on</i>
|
||||
<span :title="workspace.breadcrumbs.table">{{ workspace.breadcrumbs.table }}</span>
|
||||
<i class="mdi mdi-18px mdi-tune mr-1" />
|
||||
<span :title="workspace.breadcrumbs.table">{{ $t('word.properties').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-for="(tab, key) of queryTabs"
|
||||
v-if="workspace.breadcrumbs.table"
|
||||
class="tab-item"
|
||||
:class="{'active': selectedTab === 'data'}"
|
||||
@click="selectTab({uid: workspace.uid, tab: 'data'})"
|
||||
>
|
||||
<a class="tab-link">
|
||||
<i class="mdi mdi-18px mdi-table mr-1" />
|
||||
<span :title="workspace.breadcrumbs.table">{{ $t('word.data').toUpperCase() }}: {{ workspace.breadcrumbs.table }}</span>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
v-for="tab of queryTabs"
|
||||
:key="tab.uid"
|
||||
class="tab-item"
|
||||
:class="{'active': selectedTab === tab.uid}"
|
||||
@click="selectTab({uid: workspace.uid, tab: tab.uid})"
|
||||
@mousedown.middle="closeTab(tab.uid)"
|
||||
>
|
||||
<a><span>Query #{{ key+1 }} <span v-if="queryTabs.length > 1" class="btn btn-clear" /></span></a>
|
||||
<a>
|
||||
<span>
|
||||
Query #{{ tab.index }}
|
||||
<span
|
||||
v-if="queryTabs.length > 1"
|
||||
class="btn btn-clear"
|
||||
:title="$t('word.close')"
|
||||
@click.stop="closeTab(tab.uid)"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="tab-item">
|
||||
<a
|
||||
class="tab-add"
|
||||
:title="$t('message.openNewTab')"
|
||||
@click="addTab"
|
||||
>
|
||||
<i class="mdi mdi-24px mdi-plus" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-show="selectedTab === 'prop'" class="column col-12">
|
||||
<p class="px-2">
|
||||
In future releases
|
||||
</p>
|
||||
</div>
|
||||
<WorkspaceTableTab
|
||||
v-show="selectedTab === 1"
|
||||
v-show="selectedTab === 'data'"
|
||||
:connection="connection"
|
||||
:table="workspace.breadcrumbs.table"
|
||||
/>
|
||||
<WorkspaceQueryTab
|
||||
v-for="tab of queryTabs"
|
||||
v-show="selectedTab === tab.uid"
|
||||
:key="tab.uid"
|
||||
:tab-uid="tab.uid"
|
||||
:is-selected="selectedTab === tab.uid"
|
||||
:connection="connection"
|
||||
/>
|
||||
</div>
|
||||
@@ -68,7 +105,7 @@ export default {
|
||||
return this.selectedWorkspace === this.connection.uid;
|
||||
},
|
||||
selectedTab () {
|
||||
return this.workspace.selected_tab || this.queryTabs[0].uid;
|
||||
return this.queryTabs.find(tab => tab.uid === this.workspace.selected_tab) || ['data', 'prop'].includes(this.workspace.selected_tab) ? this.workspace.selected_tab : this.queryTabs[0].uid;
|
||||
},
|
||||
queryTabs () {
|
||||
return this.workspace.tabs.filter(tab => tab.type === 'query');
|
||||
@@ -80,102 +117,141 @@ export default {
|
||||
if (isInitiated)
|
||||
this.connectWorkspace(this.connection);
|
||||
},
|
||||
mounted () {
|
||||
if (this.$refs.tabWrap) {
|
||||
this.$refs.tabWrap.addEventListener('wheel', e => {
|
||||
if (e.deltaY > 0) this.$refs.tabWrap.scrollLeft += 50;
|
||||
else this.$refs.tabWrap.scrollLeft -= 50;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
addWorkspace: 'workspaces/addWorkspace',
|
||||
connectWorkspace: 'workspaces/connectWorkspace',
|
||||
removeConnected: 'workspaces/removeConnected',
|
||||
selectTab: 'workspaces/selectTab'
|
||||
})
|
||||
selectTab: 'workspaces/selectTab',
|
||||
newTab: 'workspaces/newTab',
|
||||
removeTab: 'workspaces/removeTab'
|
||||
}),
|
||||
addTab () {
|
||||
this.newTab(this.connection.uid);
|
||||
},
|
||||
closeTab (tUid) {
|
||||
if (this.queryTabs.length === 1) return;
|
||||
this.removeTab({ uid: this.connection.uid, tab: tUid });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.workspace {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
.workspace-tabs{
|
||||
.workspace-tabs {
|
||||
overflow: hidden;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
|
||||
.tab-block {
|
||||
background: $bg-color-light;
|
||||
margin-top: 0;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
flex-wrap: nowrap;
|
||||
overflow: auto;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
|
||||
.tab-block{
|
||||
background: $bg-color-light;
|
||||
margin-top: 0;
|
||||
|
||||
.tab-item{
|
||||
max-width: 12rem;
|
||||
width: fit-content;
|
||||
flex: initial;
|
||||
|
||||
&.active a{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> a{
|
||||
padding: .2rem .8rem;
|
||||
color: $body-font-color;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: .7;
|
||||
transition: opacity .2s;
|
||||
|
||||
&:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 2px;
|
||||
height: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-results{
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
.tab-item {
|
||||
max-width: 12rem;
|
||||
width: fit-content;
|
||||
flex: initial;
|
||||
|
||||
.table{
|
||||
width: auto;
|
||||
border-collapse: separate;
|
||||
> a {
|
||||
padding: 0.2rem 0.8rem;
|
||||
color: $body-font-color;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
.th{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: $bg-color;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-bottom-width: 2px;
|
||||
border-color: $bg-color-light;
|
||||
padding: .1rem .4rem;
|
||||
font-weight: 700;
|
||||
font-size: .7rem;
|
||||
}
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.td{
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $bg-color-light;
|
||||
padding: 0 .4rem;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
&.tab-add {
|
||||
padding: 0.2rem 0.4rem;
|
||||
margin-top: 2px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
> span {
|
||||
overflow: hidden;
|
||||
font-size: .7rem;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 0.2rem;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus{
|
||||
box-shadow:inset 0px 0px 0px 1px $body-font-color;
|
||||
background: rgba($color: #000000, $alpha: .3);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
&.active a {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-results {
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
|
||||
.table {
|
||||
width: auto;
|
||||
border-collapse: separate;
|
||||
|
||||
.th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: $bg-color;
|
||||
border: 1px solid;
|
||||
border-left: none;
|
||||
border-bottom-width: 2px;
|
||||
border-color: $bg-color-light;
|
||||
padding: 0;
|
||||
font-weight: 700;
|
||||
font-size: 0.7rem;
|
||||
z-index: 1;
|
||||
|
||||
> div {
|
||||
padding: 0.1rem 0.4rem;
|
||||
min-width: -webkit-fill-available;
|
||||
}
|
||||
}
|
||||
|
||||
.td {
|
||||
border-right: 1px solid;
|
||||
border-bottom: 1px solid;
|
||||
border-color: $bg-color-light;
|
||||
padding: 0 0.4rem;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 0.7rem;
|
||||
position: relative;
|
||||
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 1px $body-font-color;
|
||||
background: rgba($color: #000, $alpha: 0.3);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -2,10 +2,10 @@
|
||||
<div class="columns">
|
||||
<div class="column col-12 empty text-light">
|
||||
<div class="empty-icon">
|
||||
<i class="material-icons md-48">cloud_off</i>
|
||||
<i class="mdi mdi-48px mdi-power-plug-off" />
|
||||
</div>
|
||||
<p class="empty-title h5">
|
||||
{{ $t('word.disconnected') }}
|
||||
{{ isConnecting ? $t('word.connecting') : $t('word.disconnected') }}
|
||||
</p>
|
||||
<div class="empty-action">
|
||||
<button
|
||||
@@ -19,7 +19,7 @@
|
||||
</div>
|
||||
<ModalAskCredentials
|
||||
v-if="isAsking"
|
||||
@closeAsking="closeAsking"
|
||||
@close-asking="closeAsking"
|
||||
@credentials="continueTest"
|
||||
/>
|
||||
</div>
|
||||
@@ -45,7 +45,6 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification',
|
||||
connectWorkspace: 'workspaces/connectWorkspace'
|
||||
}),
|
||||
async startConnection () {
|
||||
@@ -73,11 +72,11 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.empty{
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.empty {
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
@@ -10,16 +10,21 @@
|
||||
<span class="workspace-explorebar-title">{{ connectionName }}</span>
|
||||
<span v-if="workspace.connected" class="workspace-explorebar-tools">
|
||||
<i
|
||||
class="material-icons md-18 c-hand"
|
||||
class="mdi mdi-18px mdi-database-plus c-hand mr-2"
|
||||
:title="$t('message.createNewDatabase')"
|
||||
@click="showNewDBModal"
|
||||
/>
|
||||
<i
|
||||
class="mdi mdi-18px mdi-refresh c-hand mr-2"
|
||||
:class="{'rotate':isRefreshing}"
|
||||
:title="$t('word.refresh')"
|
||||
@click="refresh"
|
||||
>refresh</i>
|
||||
/>
|
||||
<i
|
||||
class="material-icons md-18 c-hand mr-1 ml-2"
|
||||
class="mdi mdi-18px mdi-power-plug-off c-hand"
|
||||
:title="$t('word.disconnect')"
|
||||
@click="disconnectWorkspace(connection.uid)"
|
||||
>exit_to_app</i>
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<WorkspaceConnectPanel
|
||||
@@ -33,9 +38,22 @@
|
||||
:key="db.name"
|
||||
:database="db"
|
||||
:connection="connection"
|
||||
@show-database-context="openDatabaseContext"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<ModalNewDatabase
|
||||
v-if="isNewDBModal"
|
||||
@close="hideNewDBModal"
|
||||
@reload="refresh"
|
||||
/>
|
||||
<DatabaseContext
|
||||
v-if="isDatabaseContext"
|
||||
:selected-database="selectedDatabase"
|
||||
:context-event="databaseContextEvent"
|
||||
@close-context="closeDatabaseContext"
|
||||
@reload="refresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -44,12 +62,16 @@ import { mapGetters, mapActions } from 'vuex';
|
||||
import _ from 'lodash';
|
||||
import WorkspaceConnectPanel from '@/components/WorkspaceConnectPanel';
|
||||
import WorkspaceExploreBarDatabase from '@/components/WorkspaceExploreBarDatabase';
|
||||
import DatabaseContext from '@/components/WorkspaceExploreBarDatabaseContext';
|
||||
import ModalNewDatabase from '@/components/ModalNewDatabase';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBar',
|
||||
components: {
|
||||
WorkspaceConnectPanel,
|
||||
WorkspaceExploreBarDatabase
|
||||
WorkspaceExploreBarDatabase,
|
||||
DatabaseContext,
|
||||
ModalNewDatabase
|
||||
},
|
||||
props: {
|
||||
connection: Object,
|
||||
@@ -58,7 +80,14 @@ export default {
|
||||
data () {
|
||||
return {
|
||||
isRefreshing: false,
|
||||
localWidth: null
|
||||
isNewDBModal: false,
|
||||
localWidth: null,
|
||||
isDatabaseContext: false,
|
||||
isTableContext: false,
|
||||
databaseContextEvent: null,
|
||||
tableContextEvent: null,
|
||||
selectedDatabase: '',
|
||||
selectedTable: ''
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -117,76 +146,92 @@ export default {
|
||||
},
|
||||
stopResize () {
|
||||
window.removeEventListener('mousemove', this.resize);
|
||||
},
|
||||
showNewDBModal () {
|
||||
this.isNewDBModal = true;
|
||||
},
|
||||
hideNewDBModal () {
|
||||
this.isNewDBModal = false;
|
||||
},
|
||||
openDatabaseContext (payload) {
|
||||
this.isTableContext = false;
|
||||
this.selectedDatabase = payload.database;
|
||||
this.databaseContextEvent = payload.event;
|
||||
this.isDatabaseContext = true;
|
||||
},
|
||||
closeDatabaseContext () {
|
||||
this.isDatabaseContext = false;
|
||||
this.selectedDatabase = '';
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-explorebar-resizer{
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
cursor: ew-resize;
|
||||
z-index: 99;
|
||||
}
|
||||
.workspace-explorebar-resizer {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
right: -2px;
|
||||
top: 0;
|
||||
height: calc(100vh - #{$excluding-size});
|
||||
cursor: ew-resize;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
.workspace-explorebar{
|
||||
width: $explorebar-width;
|
||||
.workspace-explorebar {
|
||||
width: $explorebar-width;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
background: $bg-color-gray;
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
z-index: 8;
|
||||
flex: initial;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
|
||||
.workspace-explorebar-header {
|
||||
width: 100%;
|
||||
padding: 0.3rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
background: $bg-color-gray;
|
||||
box-shadow: 0 0 1px 0px #000;
|
||||
z-index: 8;
|
||||
flex: initial;
|
||||
position: relative;
|
||||
padding: 0;
|
||||
justify-content: space-between;
|
||||
font-size: 0.6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
|
||||
.workspace-explorebar-header{
|
||||
width: 100%;
|
||||
padding: .3rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: .6rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
|
||||
.workspace-explorebar-title{
|
||||
width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workspace-explorebar-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> i{
|
||||
opacity: .6;
|
||||
transition: opacity .2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover{
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
.workspace-explorebar-title {
|
||||
width: 80%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.workspace-explorebar-body{
|
||||
width: 100%;
|
||||
height: calc((100vh - 30px) - #{$excluding-size});
|
||||
overflow: overlay;
|
||||
padding: 0 .1rem;
|
||||
.workspace-explorebar-tools {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> i {
|
||||
opacity: 0.6;
|
||||
transition: opacity 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-explorebar-body {
|
||||
width: 100%;
|
||||
height: calc((100vh - 30px) - #{$excluding-size});
|
||||
overflow: overlay;
|
||||
padding: 0 0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@@ -4,9 +4,10 @@
|
||||
class="accordion-header database-name pb-0"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name}"
|
||||
@click="changeBreadcrumbs({schema: database.name, table:null})"
|
||||
@contextmenu.prevent="showDatabaseContext($event, database.name)"
|
||||
>
|
||||
<i class="icon material-icons md-18 mr-1">navigate_next</i>
|
||||
<i class="material-icons md-18 mr-1">view_agenda</i>
|
||||
<i class="icon mdi mdi-18px mdi-chevron-right" />
|
||||
<i class="database-icon mdi mdi-18px mdi-database mr-1" />
|
||||
<span>{{ database.name }}</span>
|
||||
</summary>
|
||||
<div class="accordion-body">
|
||||
@@ -18,9 +19,10 @@
|
||||
class="menu-item"
|
||||
:class="{'text-bold': breadcrumbs.schema === database.name && breadcrumbs.table === table.TABLE_NAME}"
|
||||
@click="changeBreadcrumbs({schema: database.name, table: table.TABLE_NAME})"
|
||||
@contextmenu.prevent="showTableContext($event, table.TABLE_NAME)"
|
||||
>
|
||||
<a class="table-name">
|
||||
<i class="material-icons md-18 mr-1">grid_on</i>
|
||||
<i class="table-icon mdi mdi-18px mdi-table mr-1" />
|
||||
<span>{{ table.TABLE_NAME }}</span>
|
||||
</a>
|
||||
</li>
|
||||
@@ -50,41 +52,52 @@ export default {
|
||||
methods: {
|
||||
...mapActions({
|
||||
changeBreadcrumbs: 'workspaces/changeBreadcrumbs'
|
||||
})
|
||||
}),
|
||||
showDatabaseContext (event, database) {
|
||||
this.$emit('show-database-context', { event, database });
|
||||
},
|
||||
showTableContext (table) {
|
||||
this.$emit('show-table-context', table);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-explorebar-database{
|
||||
.database-name,
|
||||
a.table-name{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: .1rem;
|
||||
cursor: pointer;
|
||||
font-size: .7rem;
|
||||
.workspace-explorebar-database {
|
||||
.database-name,
|
||||
a.table-name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.1rem;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
|
||||
> span{
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover{
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #FFF, $alpha: .05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
> span {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.menu-item{
|
||||
line-height: 1.2;
|
||||
&:hover {
|
||||
color: $body-font-color;
|
||||
background: rgba($color: #fff, $alpha: 0.05);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.database-tables{
|
||||
margin-left: 1.2rem;
|
||||
.database-icon,
|
||||
.table-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.database-tables {
|
||||
margin-left: 1.2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
112
src/renderer/components/WorkspaceExploreBarDatabaseContext.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<!-- <div class="context-element">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-plus text-light pr-1" /> {{ $t('word.add') }}</span>
|
||||
<i class="mdi mdi-18px mdi-chevron-right text-light pl-1" />
|
||||
</div> -->
|
||||
<div class="context-element" @click="showEditModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-pencil text-light pr-1" /> {{ $t('word.edit') }}</span>
|
||||
</div>
|
||||
<div class="context-element" @click="showDeleteModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $t('word.delete') }}</span>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
v-if="isDeleteModal"
|
||||
@confirm="deleteDatabase"
|
||||
@hide="hideDeleteModal"
|
||||
>
|
||||
<template slot="header">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-database-remove mr-1" /> {{ $t('message.deleteDatabase') }}
|
||||
</div>
|
||||
</template>
|
||||
<div slot="body">
|
||||
<div class="mb-2">
|
||||
{{ $t('message.deleteCorfirm') }} "<b>{{ selectedDatabase }}</b>"?
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
<ModalEditDatabase
|
||||
v-if="isEditModal"
|
||||
:selected-database="selectedDatabase"
|
||||
@close="hideEditModal"
|
||||
/>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import ModalEditDatabase from '@/components/ModalEditDatabase';
|
||||
import Database from '@/ipc-api/Database';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceExploreBarDatabaseContext',
|
||||
components: {
|
||||
BaseContextMenu,
|
||||
ConfirmModal,
|
||||
ModalEditDatabase
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
selectedDatabase: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isDeleteModal: false,
|
||||
isEditModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
selectedWorkspace: 'workspaces/getSelected'
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteConnection: 'connections/deleteConnection',
|
||||
showEditModal: 'application/showEditConnModal',
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
showDeleteModal () {
|
||||
this.isDeleteModal = true;
|
||||
},
|
||||
hideDeleteModal () {
|
||||
this.isDeleteModal = false;
|
||||
},
|
||||
showEditModal () {
|
||||
this.isEditModal = true;
|
||||
},
|
||||
hideEditModal () {
|
||||
this.isEditModal = false;
|
||||
this.closeContext();
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
async deleteDatabase () {
|
||||
try {
|
||||
const { status, response } = await Database.deleteDatabase({
|
||||
uid: this.selectedWorkspace,
|
||||
database: this.selectedDatabase
|
||||
});
|
||||
|
||||
if (status === 'success') {
|
||||
this.closeContext();
|
||||
this.$emit('reload');
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
@@ -1,26 +1,25 @@
|
||||
<template>
|
||||
<div class="workspace-query-tab column col-12 columns col-gapless">
|
||||
<div v-show="isSelected" class="workspace-query-tab column col-12 columns col-gapless">
|
||||
<div class="workspace-query-runner column col-12">
|
||||
<QueryEditor v-model="query" />
|
||||
<QueryEditor v-if="isSelected" :value.sync="query" />
|
||||
<div class="workspace-query-runner-footer">
|
||||
<div class="workspace-query-buttons">
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
:disabled="!query"
|
||||
@click="runQuery"
|
||||
@click="runQuery(query)"
|
||||
>
|
||||
<span>{{ $t('word.run') }}</span>
|
||||
<i class="material-icons text-success">play_arrow</i>
|
||||
<i class="mdi mdi-24px mdi-play text-success" />
|
||||
</button>
|
||||
<!-- <button class="btn btn-link btn-sm">
|
||||
<span>{{ $t('word.save') }}</span>
|
||||
<i class="material-icons ml-1">save</i>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="results.rows">
|
||||
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
|
||||
<div v-if="resultsCount !== false">
|
||||
{{ $t('word.results') }}: <b>{{ resultsCount }}</b>
|
||||
</div>
|
||||
<div v-if="affectedCount !== false">
|
||||
{{ $t('message.affectedRows') }}: <b>{{ affectedCount }}</b>
|
||||
</div>
|
||||
<div v-if="workspace.breadcrumbs.schema">
|
||||
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.schema }}</b>
|
||||
@@ -31,18 +30,26 @@
|
||||
<div class="workspace-query-results column col-12">
|
||||
<WorkspaceQueryTable
|
||||
v-if="results"
|
||||
v-show="!isQuering"
|
||||
ref="queryTable"
|
||||
:results="results"
|
||||
:fields="resultsFields"
|
||||
:tab-uid="tabUid"
|
||||
:conn-uid="connection.uid"
|
||||
mode="query"
|
||||
@update-field="updateField"
|
||||
@delete-selected="deleteSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Connection from '@/ipc-api/Connection';
|
||||
import Database from '@/ipc-api/Database';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import QueryEditor from '@/components/QueryEditor';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import tableTabs from '@/mixins/tableTabs';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTab',
|
||||
@@ -50,14 +57,20 @@ export default {
|
||||
QueryEditor,
|
||||
WorkspaceQueryTable
|
||||
},
|
||||
mixins: [tableTabs],
|
||||
props: {
|
||||
connection: Object
|
||||
connection: Object,
|
||||
tabUid: String,
|
||||
isSelected: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: '',
|
||||
lastQuery: '',
|
||||
isQuering: false,
|
||||
results: {}
|
||||
results: [],
|
||||
resultsCount: false,
|
||||
affectedCount: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -66,32 +79,126 @@ export default {
|
||||
}),
|
||||
workspace () {
|
||||
return this.getWorkspace(this.connection.uid);
|
||||
},
|
||||
resultsFields () {
|
||||
return this.results.rows && this.results.rows.length ? Object.keys(this.results.rows[0]).map(field => {
|
||||
return { name: field, key: '', type: '' }; // TODO: extract getting table name from query
|
||||
}) : [];
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
addNotification: 'notifications/addNotification',
|
||||
setTabFields: 'workspaces/setTabFields',
|
||||
setTabKeyUsage: 'workspaces/setTabKeyUsage'
|
||||
}),
|
||||
async runQuery () {
|
||||
if (!this.query) return;
|
||||
this.isQuering = true;
|
||||
this.results = {};
|
||||
getResultParams (index) {
|
||||
const resultsWithRows = this.results.filter(result => result.rows);
|
||||
let cachedTable;
|
||||
|
||||
try {
|
||||
if (resultsWithRows[index] && resultsWithRows[index].fields && resultsWithRows[index].fields.length) {
|
||||
return resultsWithRows[index].fields.map(field => {
|
||||
if (field.orgTable) cachedTable = field.orgTable;// Needed for some queries on information_schema
|
||||
return {
|
||||
table: field.orgTable || cachedTable,
|
||||
schema: field.db || 'INFORMATION_SCHEMA'
|
||||
};
|
||||
}).filter((val, i, arr) => arr.findIndex(el => el.schema === val.schema && el.table === val.table) === i);
|
||||
}
|
||||
return [];
|
||||
},
|
||||
async runQuery (query) {
|
||||
if (!query) return;
|
||||
this.isQuering = true;
|
||||
this.clearTabData();
|
||||
|
||||
try { // Query Data
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
query: this.query,
|
||||
schema: this.workspace.breadcrumbs.schema
|
||||
schema: this.schema,
|
||||
query
|
||||
};
|
||||
|
||||
const { status, response } = await Connection.rawQuery(params);
|
||||
if (status === 'success')
|
||||
this.results = response;
|
||||
const { status, response } = await Database.rawQuery(params);
|
||||
|
||||
if (status === 'success') {
|
||||
this.results = Array.isArray(response) ? response : [response];
|
||||
|
||||
let selectedFields = [];
|
||||
const fieldsArr = [];
|
||||
const keysArr = [];
|
||||
let qI = 0;// queries index
|
||||
|
||||
for (const result of this.results) { // cycle queries
|
||||
let fI = 0;// fields index
|
||||
|
||||
if (result.rows) { // if is a select
|
||||
const paramsArr = this.getResultParams(qI);
|
||||
|
||||
selectedFields = result.fields.map(field => field.orgName);
|
||||
this.resultsCount += result.rows.length;
|
||||
|
||||
for (const paramObj of paramsArr) {
|
||||
try { // Table data
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...paramObj
|
||||
};
|
||||
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
|
||||
if (status === 'success') {
|
||||
let fields = response.filter(field => selectedFields.includes(field.name));
|
||||
if (selectedFields.length) {
|
||||
fields = fields.map(field => {
|
||||
return { ...field, alias: result.fields[fI++].name };
|
||||
});
|
||||
}
|
||||
if (!fields.length) {
|
||||
fields = response.map(field => {
|
||||
return { ...field, alias: result.fields[fI++].name };
|
||||
});
|
||||
}
|
||||
|
||||
fieldsArr[qI] = fieldsArr[qI] ? [...fieldsArr[qI], ...fields] : fields;
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...paramObj
|
||||
};
|
||||
|
||||
const { status, response } = await Tables.getKeyUsage(params);
|
||||
if (status === 'success')
|
||||
keysArr[qI] = keysArr[qI] ? [...keysArr[qI], ...response] : response;
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (result.report) { // if is a query without output
|
||||
this.affectedCount += result.report.affectedRows;
|
||||
}
|
||||
|
||||
qI++;
|
||||
}
|
||||
|
||||
this.setTabFields({
|
||||
cUid: this.connection.uid,
|
||||
tUid: this.tabUid,
|
||||
fields: fieldsArr
|
||||
});
|
||||
this.setTabKeyUsage({
|
||||
cUid: this.connection.uid,
|
||||
tUid: this.tabUid,
|
||||
keyUsage: keysArr
|
||||
});
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
@@ -100,43 +207,52 @@ export default {
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
this.lastQuery = query;
|
||||
},
|
||||
reloadTable () {
|
||||
this.runQuery(this.lastQuery);
|
||||
},
|
||||
clearTabData () {
|
||||
this.results = [];
|
||||
this.resultsCount = false;
|
||||
this.affectedCount = false;
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-tabs{
|
||||
align-content: baseline;
|
||||
.workspace-tabs {
|
||||
align-content: baseline;
|
||||
|
||||
.workspace-query-runner{
|
||||
.workspace-query-runner {
|
||||
.workspace-query-runner-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.6rem 0.4rem;
|
||||
align-items: center;
|
||||
|
||||
.workspace-query-runner-footer{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .3rem .6rem .4rem;
|
||||
align-items: center;
|
||||
.workspace-query-buttons {
|
||||
display: flex;
|
||||
|
||||
.workspace-query-buttons{
|
||||
display: flex;
|
||||
|
||||
.btn{
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: .4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info{
|
||||
display: flex;
|
||||
|
||||
> div + div{
|
||||
padding-left: .6rem;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info {
|
||||
display: flex;
|
||||
|
||||
> div + div {
|
||||
padding-left: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
@@ -1,120 +1,171 @@
|
||||
<template>
|
||||
<BaseVirtualScroll
|
||||
v-if="results.rows"
|
||||
ref="resultTable"
|
||||
:items="localResults"
|
||||
:item-height="25"
|
||||
<div
|
||||
ref="tableWrapper"
|
||||
class="vscroll"
|
||||
:style="{'height': resultsSize+'px'}"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<div class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr">
|
||||
<div
|
||||
v-for="field in fields"
|
||||
:key="field.name"
|
||||
class="th"
|
||||
>
|
||||
<div class="table-column-title">
|
||||
<TableContext
|
||||
v-if="isContext"
|
||||
:context-event="contextEvent"
|
||||
:selected-rows="selectedRows"
|
||||
@delete-selected="deleteSelected"
|
||||
@close-context="isContext = false"
|
||||
/>
|
||||
<ul v-if="resultsWithRows.length > 1" class="tab tab-block result-tabs">
|
||||
<li
|
||||
v-for="(result, index) in resultsWithRows"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{'active': resultsetIndex === index}"
|
||||
@click="selectResultset(index)"
|
||||
>
|
||||
<a>{{ result.fields ? result.fields[0].orgTable : '' }} ({{ result.rows.length }})</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div ref="table" class="table table-hover">
|
||||
<div class="thead">
|
||||
<div class="tr">
|
||||
<div
|
||||
v-for="(field, index) in fields"
|
||||
:key="index"
|
||||
class="th c-hand"
|
||||
:title="field.comment ? field.comment : false"
|
||||
>
|
||||
<div ref="columnResize" class="column-resizable">
|
||||
<div class="table-column-title" @click="sort(field.name)">
|
||||
<i
|
||||
v-if="field.key"
|
||||
class="material-icons column-key c-help"
|
||||
class="mdi mdi-key column-key c-help"
|
||||
:class="`key-${field.key}`"
|
||||
:title="keyName(field.key)"
|
||||
>vpn_key</i>
|
||||
<span>{{ field.name }}</span>
|
||||
/>
|
||||
<span>{{ field.alias || field.name }}</span>
|
||||
<i
|
||||
v-if="currentSort === field.name || currentSort === `${field.table}.${field.name}`"
|
||||
class="mdi sort-icon"
|
||||
:class="currentSortDir === 'asc' ? 'mdi-sort-ascending':'mdi-sort-descending'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tbody">
|
||||
<div
|
||||
</div>
|
||||
<BaseVirtualScroll
|
||||
v-if="resultsWithRows[resultsetIndex] && resultsWithRows[resultsetIndex].rows"
|
||||
ref="resultTable"
|
||||
:items="sortedResults"
|
||||
:item-height="22"
|
||||
class="tbody"
|
||||
:visible-height="resultsSize"
|
||||
:scroll-element="scrollElement"
|
||||
>
|
||||
<template slot-scope="{ items }">
|
||||
<WorkspaceQueryTableRow
|
||||
v-for="row in items"
|
||||
:key="row._id"
|
||||
:row="row"
|
||||
:fields="fields"
|
||||
:key-usage="keyUsage"
|
||||
class="tr"
|
||||
>
|
||||
<div
|
||||
v-for="(col, cKey) in row"
|
||||
:key="cKey"
|
||||
class="td"
|
||||
:class="`type-${fieldType(cKey)}${isNull(col)}`"
|
||||
:style="{'display': cKey === '_id' ? 'none' : ''}"
|
||||
tabindex="0"
|
||||
>
|
||||
{{ col | typeFormat(fieldType(cKey)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</BaseVirtualScroll>
|
||||
:class="{'selected': selectedRows.includes(row._id)}"
|
||||
@select-row="selectRow($event, row._id)"
|
||||
@update-field="updateField($event, getPrimaryValue(row))"
|
||||
@contextmenu="contextMenu"
|
||||
/>
|
||||
</template>
|
||||
</basevirtualscroll>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { uidGen, mimeFromHex, formatBytes } from 'common/libs/utilities';
|
||||
import hexToBinary from 'common/libs/hexToBinary';
|
||||
import moment from 'moment';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
import BaseVirtualScroll from '@/components/BaseVirtualScroll';
|
||||
import WorkspaceQueryTableRow from '@/components/WorkspaceQueryTableRow';
|
||||
import TableContext from '@/components/WorkspaceQueryTableContext';
|
||||
import { mapActions, mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTable',
|
||||
components: {
|
||||
BaseVirtualScroll
|
||||
},
|
||||
filters: {
|
||||
typeFormat (val, type) {
|
||||
if (!val) return val;
|
||||
|
||||
switch (type) {
|
||||
case 'char':
|
||||
case 'varchar':
|
||||
case 'text':
|
||||
case 'mediumtext':
|
||||
return val.substring(0, 128);
|
||||
case 'date':
|
||||
return moment(val).format('YYYY-MM-DD');
|
||||
case 'datetime':
|
||||
case 'timestamp':
|
||||
return moment(val).format('YYYY-MM-DD HH:mm:ss.SSS');
|
||||
case 'blob':
|
||||
case 'mediumblob':
|
||||
case 'longblob': {
|
||||
const buff = Buffer.from(val);
|
||||
if (!buff.length) return '';
|
||||
|
||||
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
|
||||
}
|
||||
case 'bit': {
|
||||
const hex = Buffer.from(val).toString('hex');
|
||||
return hexToBinary(hex);
|
||||
}
|
||||
default:
|
||||
return val;
|
||||
}
|
||||
}
|
||||
BaseVirtualScroll,
|
||||
WorkspaceQueryTableRow,
|
||||
TableContext
|
||||
},
|
||||
props: {
|
||||
results: Object,
|
||||
fields: Array
|
||||
results: Array,
|
||||
tabUid: [String, Number],
|
||||
connUid: String,
|
||||
mode: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resultsSize: 1000,
|
||||
localResults: []
|
||||
localResults: [],
|
||||
isContext: false,
|
||||
contextEvent: null,
|
||||
selectedCell: null,
|
||||
selectedRows: [],
|
||||
currentSort: '',
|
||||
currentSortDir: 'asc',
|
||||
resultsetIndex: 0,
|
||||
scrollElement: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getWorkspaceTab: 'workspaces/getWorkspaceTab',
|
||||
getWorkspace: 'workspaces/getWorkspace'
|
||||
}),
|
||||
workspaceSchema () {
|
||||
return this.getWorkspace(this.connUid).breadcrumbs.schema;
|
||||
},
|
||||
primaryField () {
|
||||
return this.fields.filter(field => ['pri', 'uni'].includes(field.key))[0] || false;
|
||||
},
|
||||
sortedResults () {
|
||||
if (this.currentSort) {
|
||||
return [...this.localResults].sort((a, b) => {
|
||||
let modifier = 1;
|
||||
const valA = typeof a[this.currentSort] === 'string' ? a[this.currentSort].toLowerCase() : a[this.currentSort];
|
||||
const valB = typeof b[this.currentSort] === 'string' ? b[this.currentSort].toLowerCase() : b[this.currentSort];
|
||||
if (this.currentSortDir === 'desc') modifier = -1;
|
||||
if (valA < valB) return -1 * modifier;
|
||||
if (valA > valB) return 1 * modifier;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
else
|
||||
return this.localResults;
|
||||
},
|
||||
resultsWithRows () {
|
||||
return this.results.filter(result => result.rows);
|
||||
},
|
||||
tabProperties () {
|
||||
return this.getWorkspaceTab(this.tabUid);
|
||||
},
|
||||
fields () {
|
||||
return this.tabProperties && this.tabProperties.fields[this.resultsetIndex] ? this.tabProperties.fields[this.resultsetIndex] : [];
|
||||
},
|
||||
keyUsage () {
|
||||
return this.tabProperties && this.tabProperties.keyUsage[this.resultsetIndex] ? this.tabProperties.keyUsage[this.resultsetIndex] : [];
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
results () {
|
||||
this.localResults = this.results.rows ? this.results.rows.map(item => {
|
||||
return { ...item, _id: uidGen() };
|
||||
}) : [];
|
||||
this.setLocalResults();
|
||||
this.resultsetIndex = 0;
|
||||
},
|
||||
resultsetIndex () {
|
||||
this.setLocalResults();
|
||||
}
|
||||
},
|
||||
updated () {
|
||||
if (this.$refs.resultTable)
|
||||
this.resizeResults();
|
||||
if (this.$refs.table)
|
||||
this.refreshScroller();
|
||||
|
||||
if (this.$refs.tableWrapper)
|
||||
this.scrollElement = this.$refs.tableWrapper;
|
||||
},
|
||||
mounted () {
|
||||
window.addEventListener('resize', this.resizeResults);
|
||||
@@ -123,6 +174,9 @@ export default {
|
||||
window.removeEventListener('resize', this.resizeResults);
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
}),
|
||||
fieldType (cKey) {
|
||||
let type = 'unknown';
|
||||
const field = this.fields.filter(field => field.name === cKey)[0];
|
||||
@@ -131,8 +185,13 @@ export default {
|
||||
|
||||
return type;
|
||||
},
|
||||
isNull (col) {
|
||||
return col === null ? ' is-null' : '';
|
||||
fieldPrecision (cKey) {
|
||||
let length = 0;
|
||||
const field = this.fields.filter(field => field.name === cKey)[0];
|
||||
if (field)
|
||||
length = field.datePrecision;
|
||||
|
||||
return length;
|
||||
},
|
||||
keyName (key) {
|
||||
switch (key) {
|
||||
@@ -146,49 +205,174 @@ export default {
|
||||
return 'UNKNOWN ' + key;
|
||||
}
|
||||
},
|
||||
resizeResults (e) {
|
||||
getTable (index) {
|
||||
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
|
||||
return this.resultsWithRows[index].fields[0].orgTable;
|
||||
return '';
|
||||
},
|
||||
getSchema (index) {
|
||||
if (this.resultsWithRows[index] && this.resultsWithRows[index].fields && this.resultsWithRows[index].fields.length)
|
||||
return this.resultsWithRows[index].fields[0].db;
|
||||
return this.workspaceSchema;
|
||||
},
|
||||
getPrimaryValue (row) {
|
||||
const primaryFieldName = Object.keys(row).find(prop => [
|
||||
this.primaryField.alias,
|
||||
this.primaryField.name,
|
||||
`${this.primaryField.table}.${this.primaryField.alias}`,
|
||||
`${this.primaryField.table}.${this.primaryField.name}`
|
||||
].includes(prop));
|
||||
return row[primaryFieldName];
|
||||
},
|
||||
setLocalResults () {
|
||||
this.resetSort();
|
||||
this.localResults = this.resultsWithRows[this.resultsetIndex] && this.resultsWithRows[this.resultsetIndex].rows ? this.resultsWithRows[this.resultsetIndex].rows.map(item => {
|
||||
return { ...item, _id: uidGen() };
|
||||
}) : [];
|
||||
},
|
||||
resizeResults () {
|
||||
if (this.$refs.resultTable) {
|
||||
const el = this.$refs.resultTable.$el;
|
||||
const footer = document.getElementById('footer');
|
||||
const el = this.$refs.tableWrapper;
|
||||
|
||||
if (el) {
|
||||
const footer = document.getElementById('footer');
|
||||
const size = window.innerHeight - el.getBoundingClientRect().top - footer.offsetHeight;
|
||||
this.resultsSize = size;
|
||||
}
|
||||
this.$refs.resultTable.updateWindow();
|
||||
}
|
||||
},
|
||||
refreshScroller () {
|
||||
this.resizeResults();
|
||||
},
|
||||
updateField (payload, id) {
|
||||
if (!this.primaryField)
|
||||
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
|
||||
else {
|
||||
const params = {
|
||||
primary: this.primaryField.name,
|
||||
schema: this.getSchema(this.resultsetIndex),
|
||||
table: this.getTable(this.resultsetIndex),
|
||||
id,
|
||||
...payload
|
||||
};
|
||||
this.$emit('update-field', params);
|
||||
}
|
||||
},
|
||||
deleteSelected () {
|
||||
if (!this.primaryField)
|
||||
this.addNotification({ status: 'warning', message: this.$t('message.unableEditFieldWithoutPrimary') });
|
||||
else {
|
||||
const rowIDs = this.localResults.filter(row => this.selectedRows.includes(row._id)).map(row => row[this.primaryField.name]);
|
||||
const params = {
|
||||
primary: this.primaryField.name,
|
||||
schema: this.getSchema(this.resultsetIndex),
|
||||
table: this.getTable(this.resultsetIndex),
|
||||
rows: rowIDs
|
||||
};
|
||||
this.$emit('delete-selected', params);
|
||||
}
|
||||
},
|
||||
applyUpdate (params) {
|
||||
const { primary, id, field, table, content } = params;
|
||||
|
||||
this.localResults = this.localResults.map(row => {
|
||||
if (row[primary] === id)// only fieldName
|
||||
row[field] = content;
|
||||
else if (row[`${table}.${primary}`] === id)// table.fieldName
|
||||
row[`${table}.${field}`] = content;
|
||||
|
||||
return row;
|
||||
});
|
||||
},
|
||||
selectRow (event, row) {
|
||||
if (event.ctrlKey) {
|
||||
if (this.selectedRows.includes(row))
|
||||
this.selectedRows = this.selectedRows.filter(el => el !== row);
|
||||
else
|
||||
this.selectedRows.push(row);
|
||||
}
|
||||
else if (event.shiftKey) {
|
||||
if (!this.selectedRows.length)
|
||||
this.selectedRows.push(row);
|
||||
else {
|
||||
const lastID = this.selectedRows.slice(-1)[0];
|
||||
const lastIndex = this.localResults.findIndex(el => el._id === lastID);
|
||||
const clickedIndex = this.localResults.findIndex(el => el._id === row);
|
||||
if (lastIndex > clickedIndex) {
|
||||
for (let i = clickedIndex; i < lastIndex; i++)
|
||||
this.selectedRows.push(this.localResults[i]._id);
|
||||
}
|
||||
else if (lastIndex < clickedIndex) {
|
||||
for (let i = clickedIndex; i > lastIndex; i--)
|
||||
this.selectedRows.push(this.localResults[i]._id);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
this.selectedRows = [row];
|
||||
},
|
||||
contextMenu (event, cell) {
|
||||
this.selectedCell = cell;
|
||||
if (!this.selectedRows.includes(cell.id))
|
||||
this.selectedRows = [cell.id];
|
||||
this.contextEvent = event;
|
||||
this.isContext = true;
|
||||
},
|
||||
sort (field) {
|
||||
if (this.mode === 'query')
|
||||
field = `${this.getTable(this.resultsetIndex)}.${field}`;
|
||||
|
||||
if (field === this.currentSort) {
|
||||
if (this.currentSortDir === 'asc')
|
||||
this.currentSortDir = 'desc';
|
||||
else
|
||||
this.resetSort();
|
||||
}
|
||||
else {
|
||||
this.currentSortDir = 'asc';
|
||||
this.currentSort = field;
|
||||
}
|
||||
},
|
||||
resetSort () {
|
||||
this.currentSort = '';
|
||||
this.currentSortDir = 'asc';
|
||||
},
|
||||
selectResultset (index) {
|
||||
this.resultsetIndex = index;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
<style lang="scss" scoped>
|
||||
.vscroll {
|
||||
height: 1000px;
|
||||
overflow: auto;
|
||||
overflow-anchor: none;
|
||||
height: 1000px;
|
||||
overflow: auto;
|
||||
overflow-anchor: none;
|
||||
}
|
||||
|
||||
.table-column-title{
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.column-resizable {
|
||||
&:hover,
|
||||
&:active {
|
||||
resize: horizontal;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.column-key{
|
||||
transform: rotate(90deg);
|
||||
font-size: .7rem;
|
||||
line-height: 1.5;
|
||||
margin-right: .2rem;
|
||||
.table-column-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.key-pri{
|
||||
color: goldenrod;
|
||||
}
|
||||
.sort-icon {
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
|
||||
&.key-uni{
|
||||
color: deepskyblue;
|
||||
}
|
||||
|
||||
&.key-mul{
|
||||
color: palegreen;
|
||||
}
|
||||
.result-tabs {
|
||||
background: transparent !important;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
71
src/renderer/components/WorkspaceQueryTableContext.vue
Normal file
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<BaseContextMenu
|
||||
:context-event="contextEvent"
|
||||
@close-context="closeContext"
|
||||
>
|
||||
<div class="context-element" @click="showConfirmModal">
|
||||
<span class="d-flex"><i class="mdi mdi-18px mdi-delete text-light pr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}</span>
|
||||
</div>
|
||||
|
||||
<ConfirmModal
|
||||
v-if="isConfirmModal"
|
||||
@confirm="deleteRows"
|
||||
@hide="hideConfirmModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-delete mr-1" /> {{ $tc('message.deleteRows', selectedRows.length) }}
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
{{ $tc('message.confirmToDeleteRows', selectedRows.length) }}
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</BaseContextMenu>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import BaseContextMenu from '@/components/BaseContextMenu';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTableContext',
|
||||
components: {
|
||||
BaseContextMenu,
|
||||
ConfirmModal
|
||||
},
|
||||
props: {
|
||||
contextEvent: MouseEvent,
|
||||
selectedRows: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isConfirmModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
deleteConnection: 'connections/deleteConnection',
|
||||
showEditModal: 'application/showEditConnModal'
|
||||
}),
|
||||
showConfirmModal () {
|
||||
this.isConfirmModal = true;
|
||||
},
|
||||
hideConfirmModal () {
|
||||
this.isConfirmModal = false;
|
||||
},
|
||||
closeContext () {
|
||||
this.$emit('close-context');
|
||||
},
|
||||
deleteRows () {
|
||||
this.$emit('delete-selected');
|
||||
this.closeContext();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
453
src/renderer/components/WorkspaceQueryTableRow.vue
Normal file
@@ -0,0 +1,453 @@
|
||||
<template>
|
||||
<div class="tr" @click="selectRow($event, row._id)">
|
||||
<div
|
||||
v-for="(col, cKey) in row"
|
||||
v-show="cKey !== '_id'"
|
||||
:key="cKey"
|
||||
class="td p-0"
|
||||
tabindex="0"
|
||||
@contextmenu.prevent="$emit('contextmenu', $event, {id: row._id, field: cKey})"
|
||||
>
|
||||
<template v-if="cKey !== '_id'">
|
||||
<span
|
||||
v-if="!isInlineEditor[cKey]"
|
||||
class="cell-content px-2"
|
||||
:class="`${isNull(col)} type-${getFieldType(cKey)}`"
|
||||
@dblclick="editON($event, col, cKey)"
|
||||
>{{ col | typeFormat(getFieldType(cKey), getFieldPrecision(cKey)) | cutText }}</span>
|
||||
<ForeignKeySelect
|
||||
v-else-if="foreignKeys.includes(cKey)"
|
||||
class="editable-field"
|
||||
:value.sync="editingContent"
|
||||
:key-usage="getKeyUsage(cKey)"
|
||||
@blur="editOFF"
|
||||
/>
|
||||
<template v-else>
|
||||
<input
|
||||
v-if="inputProps.mask"
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
v-mask="inputProps.mask"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
<input
|
||||
v-else
|
||||
ref="editField"
|
||||
v-model="editingContent"
|
||||
:type="inputProps.type"
|
||||
autofocus
|
||||
class="editable-field px-2"
|
||||
@blur="editOFF"
|
||||
>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
<ConfirmModal
|
||||
v-if="isTextareaEditor"
|
||||
:confirm-text="$t('word.update')"
|
||||
size="medium"
|
||||
@confirm="editOFF"
|
||||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
<div>
|
||||
<textarea
|
||||
v-model="editingContent"
|
||||
class="form-input textarea-editor"
|
||||
/>
|
||||
</div>
|
||||
<div class="editor-field-info">
|
||||
<div><b>{{ $t('word.size') }}</b>: {{ editingContent.length }}</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
<ConfirmModal
|
||||
v-if="isBlobEditor"
|
||||
:confirm-text="$t('word.update')"
|
||||
@confirm="editOFF"
|
||||
@hide="hideEditorModal"
|
||||
>
|
||||
<template :slot="'header'">
|
||||
<div class="d-flex">
|
||||
<i class="mdi mdi-24px mdi-playlist-edit mr-1" /> {{ $t('word.edit') }} "{{ editingField }}"
|
||||
</div>
|
||||
</template>
|
||||
<div :slot="'body'">
|
||||
<div class="mb-2">
|
||||
<transition name="jump-down">
|
||||
<div v-if="contentInfo.size">
|
||||
<img
|
||||
v-if="isImage"
|
||||
:src="`data:${contentInfo.mime};base64, ${bufferToBase64(editingContent)}`"
|
||||
class="img-responsive p-centered bg-checkered"
|
||||
>
|
||||
<div v-else class="text-center">
|
||||
<i class="mdi mdi-36px mdi-file" />
|
||||
</div>
|
||||
<div class="editor-buttons mt-2">
|
||||
<button class="btn btn-link btn-sm" @click="downloadFile">
|
||||
<span>{{ $t('word.download') }}</span>
|
||||
<i class="mdi mdi-24px mdi-download ml-1" />
|
||||
</button>
|
||||
<button class="btn btn-link btn-sm" @click="prepareToDelete">
|
||||
<span>{{ $t('word.delete') }}</span>
|
||||
<i class="mdi mdi-24px mdi-delete-forever ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
<div class="editor-field-info">
|
||||
<div>
|
||||
<b>{{ $t('word.size') }}</b>: {{ editingContent.length | formatBytes }}<br>
|
||||
<b>{{ $t('word.mimeType') }}</b>: {{ contentInfo.mime }}
|
||||
</div>
|
||||
<div><b>{{ $t('word.type') }}</b>: {{ editingType.toUpperCase() }}</div>
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<label>{{ $t('message.uploadFile') }}</label>
|
||||
<input
|
||||
class="form-input"
|
||||
type="file"
|
||||
@change="filesChange($event)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ConfirmModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import moment from 'moment';
|
||||
import { mimeFromHex } from 'common/libs/mimeFromHex';
|
||||
import { formatBytes } from 'common/libs/formatBytes';
|
||||
import { bufferToBase64 } from 'common/libs/bufferToBase64';
|
||||
import hexToBinary from 'common/libs/hexToBinary';
|
||||
import { TEXT, LONG_TEXT, NUMBER, DATE, TIME, DATETIME, BLOB, BIT } from 'common/fieldTypes';
|
||||
import { mask } from 'vue-the-mask';
|
||||
import ConfirmModal from '@/components/BaseConfirmModal';
|
||||
import ForeignKeySelect from '@/components/ForeignKeySelect';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceQueryTableRow',
|
||||
components: {
|
||||
ConfirmModal,
|
||||
ForeignKeySelect
|
||||
},
|
||||
directives: {
|
||||
mask
|
||||
},
|
||||
filters: {
|
||||
formatBytes,
|
||||
cutText (val) {
|
||||
if (typeof val !== 'string') return val;
|
||||
return val.length > 128 ? `${val.substring(0, 128)}[...]` : val;
|
||||
},
|
||||
typeFormat (val, type, precision) {
|
||||
if (!val) return val;
|
||||
|
||||
if (DATE.includes(type))
|
||||
return moment(val).isValid() ? moment(val).format('YYYY-MM-DD') : val;
|
||||
|
||||
if (DATETIME.includes(type)) {
|
||||
let datePrecision = '';
|
||||
for (let i = 0; i < precision; i++)
|
||||
datePrecision += i === 0 ? '.S' : 'S';
|
||||
|
||||
return moment(val).isValid() ? moment(val).format(`YYYY-MM-DD HH:mm:ss${datePrecision}`) : val;
|
||||
}
|
||||
|
||||
if (BLOB.includes(type)) {
|
||||
const buff = Buffer.from(val);
|
||||
if (!buff.length) return '';
|
||||
|
||||
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||
return `${mimeFromHex(hex).mime} (${formatBytes(buff.length)})`;
|
||||
}
|
||||
|
||||
if (BIT.includes(type)) {
|
||||
const hex = Buffer.from(val).toString('hex');
|
||||
return hexToBinary(hex);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
},
|
||||
props: {
|
||||
row: Object,
|
||||
fields: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
keyUsage: Array
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isInlineEditor: {},
|
||||
isTextareaEditor: false,
|
||||
isBlobEditor: false,
|
||||
willBeDeleted: false,
|
||||
originalContent: null,
|
||||
editingContent: null,
|
||||
editingType: null,
|
||||
editingField: null,
|
||||
contentInfo: {
|
||||
ext: '',
|
||||
mime: '',
|
||||
size: null
|
||||
},
|
||||
fileToUpload: null
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
inputProps () {
|
||||
if ([...TEXT, ...LONG_TEXT].includes(this.editingType))
|
||||
return { type: 'text', mask: false };
|
||||
|
||||
if (NUMBER.includes(this.editingType))
|
||||
return { type: 'number', mask: false };
|
||||
|
||||
if (TIME.includes(this.editingType)) {
|
||||
let timeMask = '##:##:##';
|
||||
const precision = this.getFieldPrecision(this.editingField);
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
timeMask += i === 0 ? '.#' : '#';
|
||||
|
||||
return { type: 'text', mask: timeMask };
|
||||
}
|
||||
|
||||
if (DATE.includes(this.editingType))
|
||||
return { type: 'text', mask: '####-##-##' };
|
||||
|
||||
if (DATETIME.includes(this.editingType)) {
|
||||
let datetimeMask = '####-##-## ##:##:##';
|
||||
const precision = this.getFieldPrecision(this.editingField);
|
||||
|
||||
for (let i = 0; i < precision; i++)
|
||||
datetimeMask += i === 0 ? '.#' : '#';
|
||||
|
||||
return { type: 'text', mask: datetimeMask };
|
||||
}
|
||||
|
||||
if (BLOB.includes(this.editingType))
|
||||
return { type: 'file', mask: false };
|
||||
|
||||
if (BIT.includes(this.editingType))
|
||||
return { type: 'text', mask: false };
|
||||
|
||||
return { type: 'text', mask: false };
|
||||
},
|
||||
isImage () {
|
||||
return ['gif', 'jpg', 'png', 'bmp', 'ico', 'tif'].includes(this.contentInfo.ext);
|
||||
},
|
||||
foreignKeys () {
|
||||
return this.keyUsage.map(key => key.column);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
fields () {
|
||||
this.fields.forEach(field => {
|
||||
this.isInlineEditor[field.name] = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFieldType (cKey) {
|
||||
let type = 'unknown';
|
||||
const field = this.getFieldObj(cKey);
|
||||
if (field)
|
||||
type = field.type;
|
||||
|
||||
return type;
|
||||
},
|
||||
getFieldPrecision (cKey) {
|
||||
let length = 0;
|
||||
const field = this.getFieldObj(cKey);
|
||||
if (field)
|
||||
length = field.datePrecision;
|
||||
|
||||
return length;
|
||||
},
|
||||
getFieldObj (cKey) {
|
||||
return this.fields.filter(field =>
|
||||
field.name === cKey ||
|
||||
field.alias === cKey ||
|
||||
`${field.table}.${field.name}` === cKey ||
|
||||
`${field.table}.${field.alias}` === cKey ||
|
||||
`${field.table.toLowerCase()}.${field.name}` === cKey ||
|
||||
`${field.table.toLowerCase()}.${field.alias}` === cKey)[0];
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null ? ' is-null' : '';
|
||||
},
|
||||
bufferToBase64 (val) {
|
||||
return bufferToBase64(val);
|
||||
},
|
||||
editON (event, content, field) {
|
||||
const type = this.getFieldType(field);
|
||||
this.originalContent = content;
|
||||
this.editingType = type;
|
||||
this.editingField = field;
|
||||
|
||||
if (LONG_TEXT.includes(type)) {
|
||||
this.isTextareaEditor = true;
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type);
|
||||
return;
|
||||
}
|
||||
|
||||
if (BLOB.includes(type)) {
|
||||
this.isBlobEditor = true;
|
||||
this.editingContent = this.originalContent || '';
|
||||
this.fileToUpload = null;
|
||||
this.willBeDeleted = false;
|
||||
|
||||
if (this.originalContent !== null) {
|
||||
const buff = Buffer.from(this.editingContent);
|
||||
if (buff.length) {
|
||||
const hex = buff.toString('hex').substring(0, 8).toUpperCase();
|
||||
const { ext, mime } = mimeFromHex(hex);
|
||||
this.contentInfo = {
|
||||
ext,
|
||||
mime,
|
||||
size: this.editingContent.length
|
||||
};
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Inline editable fields
|
||||
this.editingContent = this.$options.filters.typeFormat(this.originalContent, type, this.getFieldPrecision(field));
|
||||
this.$nextTick(() => { // Focus on input
|
||||
event.target.blur();
|
||||
|
||||
this.$nextTick(() => document.querySelector('.editable-field').focus());
|
||||
});
|
||||
|
||||
const obj = {
|
||||
[field]: true
|
||||
};
|
||||
this.isInlineEditor = { ...this.isInlineEditor, ...obj };
|
||||
},
|
||||
editOFF () {
|
||||
this.isInlineEditor[this.editingField] = false;
|
||||
let content;
|
||||
if (!BLOB.includes(this.editingType)) {
|
||||
if (this.editingContent === this.$options.filters.typeFormat(this.originalContent, this.editingType)) return;// If not changed
|
||||
content = this.editingContent;
|
||||
}
|
||||
else { // Handle file upload
|
||||
if (this.willBeDeleted) {
|
||||
content = '';
|
||||
this.willBeDeleted = false;
|
||||
}
|
||||
else {
|
||||
if (!this.fileToUpload) return;
|
||||
content = this.fileToUpload.file.path;
|
||||
}
|
||||
}
|
||||
|
||||
this.$emit('update-field', {
|
||||
field: this.getFieldObj(this.editingField).name,
|
||||
type: this.editingType,
|
||||
content
|
||||
});
|
||||
|
||||
this.editingType = null;
|
||||
this.editingField = null;
|
||||
},
|
||||
hideEditorModal () {
|
||||
this.isTextareaEditor = false;
|
||||
this.isBlobEditor = false;
|
||||
},
|
||||
downloadFile () {
|
||||
const downloadLink = document.createElement('a');
|
||||
|
||||
downloadLink.href = `data:${this.contentInfo.mime};base64, ${bufferToBase64(this.editingContent)}`;
|
||||
downloadLink.setAttribute('download', `${this.editingField}.${this.contentInfo.ext}`);
|
||||
document.body.appendChild(downloadLink);
|
||||
|
||||
downloadLink.click();
|
||||
downloadLink.remove();
|
||||
},
|
||||
filesChange (event) {
|
||||
const { files } = event.target;
|
||||
if (!files.length) return;
|
||||
|
||||
this.fileToUpload = { name: files[0].name, file: files[0] };
|
||||
this.willBeDeleted = false;
|
||||
},
|
||||
prepareToDelete () {
|
||||
this.editingContent = '';
|
||||
this.contentInfo = {
|
||||
ext: '',
|
||||
mime: '',
|
||||
size: null
|
||||
};
|
||||
this.willBeDeleted = true;
|
||||
},
|
||||
contextMenu (event, cell) {
|
||||
this.$emit('update-field', event, cell);
|
||||
},
|
||||
selectRow (event, row) {
|
||||
this.$emit('select-row', event, row);
|
||||
},
|
||||
getKeyUsage (keyName) {
|
||||
return this.keyUsage.find(key => key.column === keyName);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.editable-field {
|
||||
margin: 0;
|
||||
border: none;
|
||||
line-height: 1;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.cell-content {
|
||||
display: block;
|
||||
min-height: 0.8rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.textarea-editor {
|
||||
height: 50vh !important;
|
||||
}
|
||||
|
||||
.editor-field-info {
|
||||
margin-top: 0.6rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.editor-buttons {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
|
||||
.btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
@@ -6,19 +6,23 @@
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
:class="{'loading':isQuering}"
|
||||
@click="getTableData"
|
||||
@click="reloadTable"
|
||||
>
|
||||
<span>{{ $t('word.refresh') }}</span>
|
||||
<i class="material-icons ml-1">refresh</i>
|
||||
<i class="mdi mdi-24px mdi-refresh ml-1" />
|
||||
</button>
|
||||
<button
|
||||
class="btn btn-link btn-sm"
|
||||
:class="{'disabled':isQuering}"
|
||||
@click="showAddModal"
|
||||
>
|
||||
<span>{{ $t('word.add') }}</span>
|
||||
<i class="mdi mdi-24px mdi-playlist-plus ml-1" />
|
||||
</button>
|
||||
<!-- <button class="btn btn-link btn-sm">
|
||||
<span>{{ $t('word.save') }}</span>
|
||||
<i class="material-icons ml-1">save</i>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="workspace-query-info">
|
||||
<div v-if="results.rows">
|
||||
{{ $t('word.results') }}: <b>{{ results.rows.length }}</b>
|
||||
<div v-if="results.length && results[0].rows">
|
||||
{{ $t('word.results') }}: <b>{{ results[0].rows.length }}</b>
|
||||
</div>
|
||||
<div v-if="workspace.breadcrumbs.database">
|
||||
{{ $t('word.schema') }}: <b>{{ workspace.breadcrumbs.database }}</b>
|
||||
@@ -29,33 +33,52 @@
|
||||
<div class="workspace-query-results column col-12">
|
||||
<WorkspaceQueryTable
|
||||
v-if="results"
|
||||
v-show="!isQuering"
|
||||
ref="queryTable"
|
||||
:results="results"
|
||||
:fields="resultsFields"
|
||||
:tab-uid="tabUid"
|
||||
:conn-uid="connection.uid"
|
||||
mode="table"
|
||||
@update-field="updateField"
|
||||
@delete-selected="deleteSelected"
|
||||
/>
|
||||
</div>
|
||||
<ModalNewTableRow
|
||||
v-if="isAddModal"
|
||||
:tab-uid="tabUid"
|
||||
@hide="hideAddModal"
|
||||
@reload="reloadTable"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Structure from '@/ipc-api/Structure';
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
import WorkspaceQueryTable from '@/components/WorkspaceQueryTable';
|
||||
import ModalNewTableRow from '@/components/ModalNewTableRow';
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import tableTabs from '@/mixins/tableTabs';
|
||||
|
||||
export default {
|
||||
name: 'WorkspaceTableTab',
|
||||
components: {
|
||||
WorkspaceQueryTable
|
||||
WorkspaceQueryTable,
|
||||
ModalNewTableRow
|
||||
},
|
||||
mixins: [tableTabs],
|
||||
props: {
|
||||
connection: Object,
|
||||
table: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
tabUid: 'data',
|
||||
isQuering: false,
|
||||
results: {},
|
||||
results: [],
|
||||
fields: [],
|
||||
lastTable: null
|
||||
keyUsage: [],
|
||||
lastTable: null,
|
||||
isAddModal: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -66,26 +89,17 @@ export default {
|
||||
return this.getWorkspace(this.connection.uid);
|
||||
},
|
||||
isSelected () {
|
||||
return this.workspace.selected_tab === 1;
|
||||
},
|
||||
resultsFields () {
|
||||
return this.fields.map(field => { // TODO: move to main process
|
||||
return {
|
||||
name: field.COLUMN_NAME,
|
||||
key: field.COLUMN_KEY.toLowerCase(),
|
||||
type: field.DATA_TYPE
|
||||
};
|
||||
});
|
||||
return this.workspace.selected_tab === 'data';
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
table: function () {
|
||||
table () {
|
||||
if (this.isSelected) {
|
||||
this.getTableData();
|
||||
this.lastTable = this.table;
|
||||
}
|
||||
},
|
||||
isSelected: function (val) {
|
||||
isSelected (val) {
|
||||
if (val && this.lastTable !== this.table) {
|
||||
this.getTableData();
|
||||
this.lastTable = this.table;
|
||||
@@ -97,23 +111,30 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
addNotification: 'notifications/addNotification'
|
||||
addNotification: 'notifications/addNotification',
|
||||
setTabFields: 'workspaces/setTabFields',
|
||||
setTabKeyUsage: 'workspaces/setTabKeyUsage'
|
||||
}),
|
||||
async getTableData () {
|
||||
if (!this.table) return;
|
||||
this.isQuering = true;
|
||||
this.results = {};
|
||||
this.results = [];
|
||||
const fieldsArr = [];
|
||||
const keysArr = [];
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: [] });
|
||||
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
schema: this.workspace.breadcrumbs.schema,
|
||||
schema: this.schema,
|
||||
table: this.workspace.breadcrumbs.table
|
||||
};
|
||||
|
||||
try {
|
||||
const { status, response } = await Structure.getTableColumns(params);
|
||||
if (status === 'success')
|
||||
this.fields = response.rows;
|
||||
try { // Columns data
|
||||
const { status, response } = await Tables.getTableColumns(params);
|
||||
if (status === 'success') {
|
||||
this.fields = response;// Needed to add new rows
|
||||
fieldsArr.push(response);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
@@ -121,11 +142,11 @@ export default {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try {
|
||||
const { status, response } = await Structure.getTableData(params);
|
||||
try { // Table data
|
||||
const { status, response } = await Tables.getTableData(params);
|
||||
|
||||
if (status === 'success')
|
||||
this.results = response;
|
||||
this.results = [response];
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
@@ -133,44 +154,71 @@ export default {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
try { // Key usage (foreign keys)
|
||||
const { status, response } = await Tables.getKeyUsage(params);
|
||||
|
||||
if (status === 'success') {
|
||||
this.keyUsage = response;// Needed to add new rows
|
||||
keysArr.push(response);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.setTabFields({ cUid: this.connection.uid, tUid: this.tabUid, fields: fieldsArr });
|
||||
this.setTabKeyUsage({ cUid: this.connection.uid, tUid: this.tabUid, keyUsage: keysArr });
|
||||
this.isQuering = false;
|
||||
},
|
||||
getTable () {
|
||||
return this.table;
|
||||
},
|
||||
reloadTable () {
|
||||
this.getTableData();
|
||||
},
|
||||
showAddModal () {
|
||||
this.isAddModal = true;
|
||||
},
|
||||
hideAddModal () {
|
||||
this.isAddModal = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.workspace-tabs{
|
||||
align-content: baseline;
|
||||
.workspace-tabs {
|
||||
align-content: baseline;
|
||||
|
||||
.workspace-query-runner{
|
||||
.workspace-query-runner {
|
||||
.workspace-query-runner-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.3rem 0.6rem 0.4rem;
|
||||
align-items: center;
|
||||
|
||||
.workspace-query-runner-footer{
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: .3rem .6rem .4rem;
|
||||
align-items: center;
|
||||
.workspace-query-buttons {
|
||||
display: flex;
|
||||
|
||||
.workspace-query-buttons{
|
||||
display: flex;
|
||||
|
||||
.btn{
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: .4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info{
|
||||
display: flex;
|
||||
|
||||
> div + div{
|
||||
padding-left: .6rem;
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
display: flex;
|
||||
align-self: center;
|
||||
color: $body-font-color;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-query-info {
|
||||
display: flex;
|
||||
|
||||
> div + div {
|
||||
padding-left: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
87
src/renderer/i18n/ar-SA.js
Normal file
@@ -0,0 +1,87 @@
|
||||
module.exports = {
|
||||
word: {
|
||||
edit: 'تعديل',
|
||||
save: 'حفظ',
|
||||
close: 'إغلاق',
|
||||
delete: 'حفظ',
|
||||
confirm: 'تأكيد',
|
||||
cancel: 'إلغاء',
|
||||
send: 'إرسال',
|
||||
connectionName: 'إسم الإتصال',
|
||||
client: 'العميل',
|
||||
hostName: 'إسم المستضيف',
|
||||
port: 'المنفذ',
|
||||
user: 'المستخدم',
|
||||
password: 'الرقم السري',
|
||||
credentials: 'بيانات الدخول',
|
||||
connect: 'إتصال',
|
||||
connected: 'متصل',
|
||||
disconnect: 'إلغاء الإتصال',
|
||||
disconnected: 'غير متصل',
|
||||
refresh: 'تحديث',
|
||||
settings: 'الإعدادات',
|
||||
general: 'عام',
|
||||
themes: 'الأنماط',
|
||||
update: 'تحديث',
|
||||
about: 'حول',
|
||||
language: 'اللغة',
|
||||
version: 'النسخة',
|
||||
donate: 'إدعم',
|
||||
run: 'شغل',
|
||||
schema: 'Schema',
|
||||
results: 'النتائج',
|
||||
size: 'الحجم',
|
||||
seconds: 'ثواني',
|
||||
type: 'نوع',
|
||||
mimeType: 'نوع الميديا',
|
||||
download: 'تحميل',
|
||||
add: 'أضف',
|
||||
data: 'بيانات',
|
||||
properties: 'خصائص',
|
||||
insert: 'أدرج'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'مرحبا بك في عميل الSQL انتاريس!',
|
||||
appFirstStep: 'خطوتك الأولى قم بإنشاء إتصال جديد بقاعدة بيانات.',
|
||||
addConnection: 'إضافة إتصال',
|
||||
createConnection: 'إنشاء إتصال',
|
||||
createNewConnection: 'إنشاء إتصال جديد',
|
||||
askCredentials: 'إطلب بيانات الدخول',
|
||||
testConnection: 'إختبر الإتصال',
|
||||
editConnection: 'عدل الإتصال',
|
||||
deleteConnection: 'إحذف الإتصال',
|
||||
deleteCorfirm: 'هل أنت متأكد من حذف الإتصال؟',
|
||||
connectionSuccessfullyMade: 'تم الإتصال بنجاح!',
|
||||
madeWithJS: 'بني بـ 💛 و جافاسكربت!',
|
||||
checkForUpdates: 'تأكد من التحديثات',
|
||||
noUpdatesAvailable: 'لا توجد تحديثات',
|
||||
checkingForUpdate: 'البحث عن تحديثات',
|
||||
checkFailure: 'فشل البحث, نرجوا المحاولة في وقت لاحق',
|
||||
updateAvailable: 'تحديث جديد متوفر',
|
||||
downloadingUpdate: 'جاري تحميل التحديث',
|
||||
updateDownloaded: 'تم تحميل التحديث',
|
||||
restartToInstall: 'قم بإعادة تشغيل انتاريس للتحديث',
|
||||
unableEditFieldWithoutPrimary: 'لا يمكن تعديل الخانة بدون وجود مفتاح رئيسي في النتائج',
|
||||
editCell: 'تعديل الخلية',
|
||||
deleteRows: 'حذف صف | حذف {count} صفوف',
|
||||
confirmToDeleteRows: 'هل أنت متأكد من حذف صف واحد؟? | هل أنت متأكد من حذف {count} صف?',
|
||||
notificationsTimeout: 'إنتهاء التنبيهات',
|
||||
uploadFile: 'رفع ملف',
|
||||
addNewRow: 'إضافة صف جديد',
|
||||
numberOfInserts: 'عدد الإدراجات'
|
||||
},
|
||||
// Date and Time
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}
|
||||
};
|
@@ -29,7 +29,19 @@ module.exports = {
|
||||
donate: 'Donate',
|
||||
run: 'Run',
|
||||
schema: 'Schema',
|
||||
results: 'Results'
|
||||
results: 'Results',
|
||||
size: 'Size',
|
||||
seconds: 'Seconds',
|
||||
type: 'Type',
|
||||
mimeType: 'Mime-Type',
|
||||
download: 'Download',
|
||||
add: 'Add',
|
||||
data: 'Data',
|
||||
properties: 'Properties',
|
||||
insert: 'Insert',
|
||||
connecting: 'Connecting',
|
||||
name: 'Name',
|
||||
collation: 'Collation'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Welcome to Antares SQL Client!',
|
||||
@@ -41,7 +53,7 @@ module.exports = {
|
||||
testConnection: 'Test connection',
|
||||
editConnection: 'Edit connection',
|
||||
deleteConnection: 'Delete connection',
|
||||
deleteConnectionCorfirm: 'Do you confirm the cancellation of',
|
||||
deleteCorfirm: 'Do you confirm the cancellation of',
|
||||
connectionSuccessfullyMade: 'Connection successfully made!',
|
||||
madeWithJS: 'Made with 💛 and JavaScript!',
|
||||
checkForUpdates: 'Check for updates',
|
||||
@@ -51,7 +63,22 @@ module.exports = {
|
||||
updateAvailable: 'Update available',
|
||||
downloadingUpdate: 'Downloading update',
|
||||
updateDownloaded: 'Update downloaded',
|
||||
restartToInstall: 'Restart Antares to install'
|
||||
restartToInstall: 'Restart Antares to install',
|
||||
unableEditFieldWithoutPrimary: 'Unable to edit a field without a primary key in resultset',
|
||||
editCell: 'Edit cell',
|
||||
deleteRows: 'Delete row | Delete {count} rows',
|
||||
confirmToDeleteRows: 'Do you confirm to delete one row? | Do you confirm to delete {count} rows?',
|
||||
notificationsTimeout: 'Notifications timeout',
|
||||
uploadFile: 'Upload file',
|
||||
addNewRow: 'Add new row',
|
||||
numberOfInserts: 'Number of inserts',
|
||||
openNewTab: 'Open a new tab',
|
||||
affectedRows: 'Affected rows',
|
||||
createNewDatabase: 'Create new Database',
|
||||
databaseName: 'Database name',
|
||||
serverDefault: 'Server default',
|
||||
deleteDatabase: 'Delete database',
|
||||
editDatabase: 'Edit database'
|
||||
},
|
||||
// Date and Time
|
||||
short: {
|
||||
|
90
src/renderer/i18n/es-ES.js
Normal file
@@ -0,0 +1,90 @@
|
||||
module.exports = {
|
||||
word: {
|
||||
edit: 'Editar',
|
||||
save: 'Guardar',
|
||||
close: 'Cerrar',
|
||||
delete: 'Eliminar',
|
||||
confirm: 'Confirmar',
|
||||
cancel: 'Cancelar',
|
||||
send: 'Enviar',
|
||||
connectionName: 'Nombre de la conexión',
|
||||
client: 'Cliente',
|
||||
hostName: 'Servidor',
|
||||
port: 'Puerto',
|
||||
user: 'Usuario',
|
||||
password: 'Contraseña',
|
||||
credentials: 'Credenciales',
|
||||
connect: 'Connectar',
|
||||
connected: 'Conectado',
|
||||
disconnect: 'Desconectar',
|
||||
disconnected: 'Desconectado',
|
||||
refresh: 'Refrescar',
|
||||
settings: 'Configuración',
|
||||
general: 'General',
|
||||
themes: 'Temas',
|
||||
update: 'Actualizar',
|
||||
about: 'Sobre',
|
||||
language: 'Idioma',
|
||||
version: 'Versión',
|
||||
donate: 'Donar',
|
||||
run: 'Ejecutar',
|
||||
schema: 'Esquema',
|
||||
results: 'Resultados',
|
||||
size: 'Tamaño',
|
||||
seconds: 'Segundos',
|
||||
type: 'Tipo',
|
||||
mimeType: 'Mime-Type',
|
||||
download: 'Descargar',
|
||||
add: 'Añadir',
|
||||
data: 'Datos',
|
||||
properties: 'Propiedades',
|
||||
insert: 'Insertar',
|
||||
connecting: 'Conectando'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Bienvenido a Antares Cliente SQL!',
|
||||
appFirstStep: 'Primer paso: Crear una conexión a una Base de Datos.',
|
||||
addConnection: 'Añadir conexión',
|
||||
createConnection: 'Crear conexión',
|
||||
createNewConnection: 'Crear nueva conexión',
|
||||
askCredentials: 'Preguntar credenciales',
|
||||
testConnection: 'Comprobar conexión',
|
||||
editConnection: 'Editar conexión',
|
||||
deleteConnection: 'Eliminar conexión',
|
||||
deleteCorfirm: 'Confirmas la cancelación de',
|
||||
connectionSuccessfullyMade: 'Conexión realizada correctamente!',
|
||||
madeWithJS: 'Hecho con 💛 y JavaScript!',
|
||||
checkForUpdates: 'Comprobar actualizaciones',
|
||||
noUpdatesAvailable: 'No hay actualizaciones',
|
||||
checkingForUpdate: 'Comprobando actualizaciones',
|
||||
checkFailure: 'Error en la comprobación, por favor pruebe más tarde',
|
||||
updateAvailable: 'Actualización disponible',
|
||||
downloadingUpdate: 'Descargando actualización',
|
||||
updateDownloaded: 'Descargada actualización',
|
||||
restartToInstall: 'Reiniciar Antares para instalar',
|
||||
unableEditFieldWithoutPrimary: 'No se puede editar una campo sin Llave Primaria en el registro',
|
||||
editCell: 'Editar celda',
|
||||
deleteRows: 'Eliminar fila | Eliminar {count} filas',
|
||||
confirmToDeleteRows: '¿Quiere realmente eliminar una fila? | ¿Quiere realmente eliminar {count} filas?',
|
||||
notificationsTimeout: 'Tiempo de espera',
|
||||
uploadFile: 'Cargar fichero',
|
||||
addNewRow: 'Añadir nueva fila',
|
||||
numberOfInserts: 'Numero de inserciones',
|
||||
openNewTab: 'Abrir nueva pestaña',
|
||||
affectedRows: 'Filas afectadas'
|
||||
},
|
||||
// Date and Time
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
weekday: 'short',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric'
|
||||
}
|
||||
};
|
@@ -6,7 +6,9 @@ Vue.use(VueI18n);
|
||||
const i18n = new VueI18n({
|
||||
messages: {
|
||||
'en-US': require('./en-US'),
|
||||
'it-IT': require('./it-IT')
|
||||
'it-IT': require('./it-IT'),
|
||||
'ar-SA': require('./ar-SA'),
|
||||
'es-ES': require('./es-ES')
|
||||
}
|
||||
});
|
||||
export default i18n;
|
||||
|
@@ -27,7 +27,19 @@ module.exports = {
|
||||
language: 'Lingua',
|
||||
version: 'Versione',
|
||||
donate: 'Dona',
|
||||
run: 'Esegui'
|
||||
run: 'Esegui',
|
||||
schema: 'Schema',
|
||||
results: 'Risultati',
|
||||
size: 'Dimensioni',
|
||||
seconds: 'Secondi',
|
||||
type: 'Tipo',
|
||||
mimeType: 'Mime-Type',
|
||||
download: 'Scarica',
|
||||
add: 'Aggiungi',
|
||||
data: 'Dati',
|
||||
properties: 'Proprietà',
|
||||
insert: 'Inserisci',
|
||||
connecting: 'Connessione in corso'
|
||||
},
|
||||
message: {
|
||||
appWelcome: 'Benvenuto in Antares SQL Client!',
|
||||
@@ -39,9 +51,27 @@ module.exports = {
|
||||
testConnection: 'Testa connessione',
|
||||
editConnection: 'Modifica connessione',
|
||||
deleteConnection: 'Elimina connessione',
|
||||
deleteConnectionCorfirm: 'Confermi l\'eliminazione di',
|
||||
deleteCorfirm: 'Confermi l\'eliminazione di',
|
||||
connectionSuccessfullyMade: 'Connessione avvenuta con successo!',
|
||||
madeWithJS: 'Fatto con 💛 e JavaScript!'
|
||||
madeWithJS: 'Fatto con 💛 e JavaScript!',
|
||||
checkForUpdates: 'Cerca aggiornamenti',
|
||||
noUpdatesAvailable: 'Nessun aggiornamento disponibile',
|
||||
checkingForUpdate: 'Controllo aggiornamenti in corso',
|
||||
checkFailure: 'Controllo fallito, riprova più tardi',
|
||||
updateAvailable: 'Aggiornamento disponibile',
|
||||
downloadingUpdate: 'Download dell\'aggiornamento',
|
||||
updateDownloaded: 'Aggiornamento scaricato',
|
||||
restartToInstall: 'Riavvia Antares per installare l\'aggiornamento',
|
||||
unableEditFieldWithoutPrimary: 'Impossibile modificare il campo senza una primary key nel resultset',
|
||||
editCell: 'Modifica cella',
|
||||
deleteRows: 'Elimina riga | Elimina {count} righe',
|
||||
confirmToDeleteRows: 'Confermi di voler cancellare una riga? | Confermi di voler cancellare {count} righe?',
|
||||
notificationsTimeout: 'Timeout Notifiche',
|
||||
uploadFile: 'Carica file',
|
||||
addNewRow: 'Aggiungi nuova riga',
|
||||
numberOfInserts: 'Numero di insert',
|
||||
openNewTab: 'Apri nuova scheda',
|
||||
affectedRows: 'Righe interessate'
|
||||
},
|
||||
// Date and Time
|
||||
short: {
|
||||
|
@@ -1,4 +1,6 @@
|
||||
export default {
|
||||
'en-US': 'English',
|
||||
'it-IT': 'Italiano'
|
||||
'it-IT': 'Italiano',
|
||||
'ar-SA': 'العربية',
|
||||
'es-ES': 'Español'
|
||||
};
|
||||
|
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 19 KiB |
@@ -1 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 210" height="210mm" width="210mm"><path d="M180.088 107.51a72.967 69.474 0 01-72.858 69.473 72.967 69.474 0 01-73.075-69.266 72.967 69.474 0 0172.639-69.68 72.967 69.474 0 0173.292 69.056" fill="#e36929"/><path d="M82.985 102.48a17.02 21.735 21.455 01-23.767 14.012 17.02 21.735 21.455 01-7.938-26.403 17.02 21.735 21.455 0123.744-14.091 17.02 21.735 21.455 018.009 26.36M157.83 118.562a17.472 11.51-52.488 01-1.695 20.917 17.472 11.51-52.488 01-19.742 6.678 17.472 11.51-52.488 011.635-20.897 17.472 11.51-52.488 0119.747-6.74" fill="#f8d163"/><path d="M72.979 98.717a9.143 10.562 0 01-9.13 10.562 9.143 10.562 0 01-9.156-10.53 9.143 10.562 0 019.102-10.594 9.143 10.562 0 019.184 10.499M155.659 130.732a6.21 6.986 0 01-6.2 6.987 6.21 6.986 0 01-6.22-6.966 6.21 6.986 0 016.182-7.007 6.21 6.986 0 016.238 6.945" fill="#fbfcf7"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 210 210">
|
||||
<defs/>
|
||||
<path fill="#e36929" d="M180.088 107.51a72.967 69.474 0 01-72.858 69.473 72.967 69.474 0 01-73.075-69.266 72.967 69.474 0 0172.639-69.68 72.967 69.474 0 0173.292 69.056"/>
|
||||
<path fill="#f8d163" d="M82.985 102.48a17.02 21.735 21.455 01-23.767 14.012 17.02 21.735 21.455 01-7.938-26.403 17.02 21.735 21.455 0123.744-14.091 17.02 21.735 21.455 018.009 26.36m74.797 16.204a17.472 11.51-52.488 01-1.695 20.917 17.472 11.51-52.488 01-19.742 6.678 17.472 11.51-52.488 011.635-20.897 17.472 11.51-52.488 0119.747-6.74"/>
|
||||
<path fill="#fbfcf7" d="M72.979 98.717a9.143 10.562 0 01-9.13 10.562 9.143 10.562 0 01-9.156-10.53 9.143 10.562 0 019.102-10.594 9.143 10.562 0 019.184 10.499m82.68 32.078a6.21 6.986 0 01-6.2 6.987 6.21 6.986 0 01-6.22-6.966 6.21 6.986 0 016.182-7.007 6.21 6.986 0 016.238 6.945"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 892 B After Width: | Height: | Size: 880 B |
@@ -3,11 +3,11 @@ import { ipcRenderer } from 'electron';
|
||||
|
||||
export default class {
|
||||
static makeTest (params) {
|
||||
return ipcRenderer.invoke('testConnection', params);
|
||||
return ipcRenderer.invoke('test-connection', params);
|
||||
}
|
||||
|
||||
static checkConnection (params) {
|
||||
return ipcRenderer.invoke('checkConnection', params);
|
||||
return ipcRenderer.invoke('check-connection', params);
|
||||
}
|
||||
|
||||
static connect (params) {
|
||||
@@ -17,12 +17,4 @@ export default class {
|
||||
static disconnect (uid) {
|
||||
return ipcRenderer.invoke('disconnect', uid);
|
||||
}
|
||||
|
||||
static refresh (uid) {
|
||||
return ipcRenderer.invoke('refresh', uid);
|
||||
}
|
||||
|
||||
static rawQuery (params) {
|
||||
return ipcRenderer.invoke('rawQuery', params);
|
||||
}
|
||||
}
|
||||
|
40
src/renderer/ipc-api/Database.js
Normal file
@@ -0,0 +1,40 @@
|
||||
'use strict';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export default class {
|
||||
static createDatabase (params) {
|
||||
return ipcRenderer.invoke('create-database', params);
|
||||
}
|
||||
|
||||
static updateDatabase (params) {
|
||||
return ipcRenderer.invoke('update-database', params);
|
||||
}
|
||||
|
||||
static getDatabaseCollation (params) {
|
||||
return ipcRenderer.invoke('get-database-collation', params);
|
||||
}
|
||||
|
||||
static deleteDatabase (params) {
|
||||
return ipcRenderer.invoke('delete-database', params);
|
||||
}
|
||||
|
||||
static getStructure (uid) {
|
||||
return ipcRenderer.invoke('get-structure', uid);
|
||||
}
|
||||
|
||||
static getCollations (uid) {
|
||||
return ipcRenderer.invoke('get-collations', uid);
|
||||
}
|
||||
|
||||
static getVariables (uid) {
|
||||
return ipcRenderer.invoke('get-variables', uid);
|
||||
}
|
||||
|
||||
static useSchema (params) {
|
||||
return ipcRenderer.invoke('use-schema', params);
|
||||
}
|
||||
|
||||
static rawQuery (params) {
|
||||
return ipcRenderer.invoke('raw-query', params);
|
||||
}
|
||||
}
|
@@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export default class {
|
||||
static getTableColumns (params) {
|
||||
return ipcRenderer.invoke('getTableColumns', params);
|
||||
}
|
||||
|
||||
static getTableData (params) {
|
||||
return ipcRenderer.invoke('getTableData', params);
|
||||
}
|
||||
}
|
32
src/renderer/ipc-api/Tables.js
Normal file
@@ -0,0 +1,32 @@
|
||||
'use strict';
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export default class {
|
||||
static getTableColumns (params) {
|
||||
return ipcRenderer.invoke('get-table-columns', params);
|
||||
}
|
||||
|
||||
static getTableData (params) {
|
||||
return ipcRenderer.invoke('get-table-data', params);
|
||||
}
|
||||
|
||||
static getKeyUsage (params) {
|
||||
return ipcRenderer.invoke('get-key-usage', params);
|
||||
}
|
||||
|
||||
static updateTableCell (params) {
|
||||
return ipcRenderer.invoke('update-table-cell', params);
|
||||
}
|
||||
|
||||
static deleteTableRows (params) {
|
||||
return ipcRenderer.invoke('delete-table-rows', params);
|
||||
}
|
||||
|
||||
static insertTableRows (params) {
|
||||
return ipcRenderer.invoke('insert-table-rows', params);
|
||||
}
|
||||
|
||||
static getForeignList (params) {
|
||||
return ipcRenderer.invoke('get-foreign-list', params);
|
||||
}
|
||||
}
|
@@ -2,7 +2,7 @@
|
||||
|
||||
import Vue from 'vue';
|
||||
|
||||
import 'material-design-icons/iconfont/material-icons.css';
|
||||
import '@mdi/font/css/materialdesignicons.css';
|
||||
import '@/scss/main.scss';
|
||||
|
||||
import App from '@/App.vue';
|
||||
|
58
src/renderer/mixins/tableTabs.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import Tables from '@/ipc-api/Tables';
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
schema () {
|
||||
return this.workspace.breadcrumbs.schema;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async updateField (payload) {
|
||||
this.isQuering = true;
|
||||
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...payload
|
||||
};
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.updateTableCell(params);
|
||||
if (status === 'success') {
|
||||
if (response.reload)// Needed for blob fields
|
||||
this.reloadTable();
|
||||
else
|
||||
this.$refs.queryTable.applyUpdate(payload);
|
||||
}
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
},
|
||||
async deleteSelected (payload) {
|
||||
this.isQuering = true;
|
||||
|
||||
const params = {
|
||||
uid: this.connection.uid,
|
||||
...payload
|
||||
};
|
||||
|
||||
try {
|
||||
const { status, response } = await Tables.deleteTableRows(params);
|
||||
|
||||
if (status === 'success')
|
||||
this.reloadTable();
|
||||
else
|
||||
this.addNotification({ status: 'error', message: response });
|
||||
}
|
||||
catch (err) {
|
||||
this.addNotification({ status: 'error', message: err.stack });
|
||||
}
|
||||
|
||||
this.isQuering = false;
|
||||
}
|
||||
}
|
||||
};
|
@@ -1,44 +1,48 @@
|
||||
@mixin type-colors($types) {
|
||||
@each $type, $color in $types {
|
||||
.type-#{$type} {
|
||||
color: $color;
|
||||
@each $type, $color in $types {
|
||||
.type-#{$type} {
|
||||
color: $color;
|
||||
|
||||
@if $type == 'number'{
|
||||
text-align: right;
|
||||
}
|
||||
@if $type == "number" {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include type-colors((
|
||||
"char": seagreen,
|
||||
"varchar": seagreen,
|
||||
"text": seagreen,
|
||||
"mediumtext": seagreen,
|
||||
@include type-colors(
|
||||
(
|
||||
"char": seagreen,
|
||||
"varchar": seagreen,
|
||||
"text": seagreen,
|
||||
"mediumtext": seagreen,
|
||||
"longtext": seagreen,
|
||||
"int": cornflowerblue,
|
||||
"tinyint": cornflowerblue,
|
||||
"smallint": cornflowerblue,
|
||||
"mediumint": cornflowerblue,
|
||||
"float": cornflowerblue,
|
||||
"double": cornflowerblue,
|
||||
"decimal": cornflowerblue,
|
||||
"bigint": cornflowerblue,
|
||||
"datetime": coral,
|
||||
"date": coral,
|
||||
"time": coral,
|
||||
"timestamp": coral,
|
||||
"bit": lightskyblue,
|
||||
"blob": darkorchid,
|
||||
"mediumblob": darkorchid,
|
||||
"longblob": darkorchid,
|
||||
"enum": gold,
|
||||
"set": gold,
|
||||
"unknown": gray,
|
||||
)
|
||||
);
|
||||
|
||||
"int": cornflowerblue,
|
||||
"tinyint": cornflowerblue,
|
||||
"smallint": cornflowerblue,
|
||||
"mediumint": cornflowerblue,
|
||||
|
||||
"datetime": coral,
|
||||
"date": coral,
|
||||
"time": coral,
|
||||
"timestamp": coral,
|
||||
.is-null {
|
||||
color: gray;
|
||||
|
||||
"bit": lightskyblue,
|
||||
|
||||
"blob": darkorchid,
|
||||
"mediumblob": darkorchid,
|
||||
"longblob": darkorchid,
|
||||
|
||||
"unknown": gray,
|
||||
));
|
||||
|
||||
.is-null{
|
||||
color: gray;
|
||||
|
||||
&::after{
|
||||
content: 'NULL';
|
||||
}
|
||||
}
|
||||
&::after {
|
||||
content: "NULL";
|
||||
}
|
||||
}
|
||||
|
@@ -1,26 +1,26 @@
|
||||
.dbi{
|
||||
display: inline-block;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background-size: cover;
|
||||
.dbi {
|
||||
display: inline-block;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background-size: cover;
|
||||
|
||||
&.dbi-mysql{
|
||||
background-image: url('../images/svg/mysql.svg');
|
||||
}
|
||||
&.dbi-mysql {
|
||||
background-image: url("../images/svg/mysql.svg");
|
||||
}
|
||||
|
||||
&.dbi-maria{
|
||||
background-image: url('../images/svg/mariadb.svg');
|
||||
}
|
||||
&.dbi-maria {
|
||||
background-image: url("../images/svg/mariadb.svg");
|
||||
}
|
||||
|
||||
&.dbi-mssql{
|
||||
background-image: url('../images/svg/mssql.svg');
|
||||
}
|
||||
&.dbi-mssql {
|
||||
background-image: url("../images/svg/mssql.svg");
|
||||
}
|
||||
|
||||
&.dbi-pg{
|
||||
background-image: url('../images/svg/pg.svg');
|
||||
}
|
||||
&.dbi-pg {
|
||||
background-image: url("../images/svg/pg.svg");
|
||||
}
|
||||
|
||||
&.dbi-oracledb{
|
||||
background-image: url('../images/svg/oracledb.svg');
|
||||
}
|
||||
}
|
||||
&.dbi-oracledb {
|
||||
background-image: url("../images/svg/oracledb.svg");
|
||||
}
|
||||
}
|
||||
|
@@ -1,65 +1,71 @@
|
||||
.table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
display: table;
|
||||
|
||||
&.table-striped {
|
||||
.tbody {
|
||||
.tr:nth-of-type(odd) {
|
||||
background: $bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&,
|
||||
&.table-striped {
|
||||
.tbody {
|
||||
.tr {
|
||||
&.active {
|
||||
background: $bg-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.table-hover {
|
||||
.tbody {
|
||||
.tr {
|
||||
&:hover {
|
||||
background: $bg-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scollable tables
|
||||
&.table-scroll {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding-bottom: .75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
|
||||
.thead{
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
.tbody{
|
||||
.tbody {
|
||||
display: table-row-group;
|
||||
}
|
||||
}
|
||||
|
||||
.tr{
|
||||
.tr {
|
||||
display: table-row;
|
||||
}
|
||||
}
|
||||
|
||||
.td,
|
||||
.th {
|
||||
border-bottom: $border-width solid $border-color;
|
||||
padding: $unit-3 $unit-2;
|
||||
display: table-cell;
|
||||
}
|
||||
.th {
|
||||
border-bottom-width: $border-width-lg;
|
||||
}
|
||||
}
|
||||
// Scollable tables
|
||||
&.table-scroll {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
.td,
|
||||
.th {
|
||||
border-bottom: $border-width solid $border-color;
|
||||
padding: $unit-3 $unit-2;
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.th {
|
||||
border-bottom-width: $border-width-lg;
|
||||
}
|
||||
|
||||
&,
|
||||
&.table-striped {
|
||||
.tbody {
|
||||
.tr {
|
||||
&.selected {
|
||||
background: #333 !important;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: $bg-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.table-hover {
|
||||
.tbody {
|
||||
.tr {
|
||||
&:hover {
|
||||
background: $bg-color-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.table-striped {
|
||||
.tbody {
|
||||
.tr:nth-of-type(odd) {
|
||||
background: $bg-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,16 +1,9 @@
|
||||
.mdi {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.material-icons{// TODO: rewrite with rem
|
||||
/* Rules for sizing the icon. */
|
||||
&.md-18 { font-size: 18px; }
|
||||
&.md-24 { font-size: 24px; }
|
||||
&.md-36 { font-size: 36px; }
|
||||
&.md-48 { font-size: 48px; }
|
||||
|
||||
/* Rules for using icons as black on a light background. */
|
||||
&.md-dark { color: rgba(0, 0, 0, 0.54); }
|
||||
&.md-dark.md-inactive { color: rgba(0, 0, 0, 0.26); }
|
||||
|
||||
/* Rules for using icons as white on a dark background. */
|
||||
&.md-light { color: rgba(255, 255, 255, 1); }
|
||||
&.md-light.md-inactive { color: rgba(255, 255, 255, 0.3); }
|
||||
}
|
||||
&::before {
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
|
18
src/renderer/scss/_table-keys.scss
Normal file
@@ -0,0 +1,18 @@
|
||||
.column-key {
|
||||
transform: rotate(90deg);
|
||||
font-size: 0.7rem;
|
||||
line-height: 1.5;
|
||||
margin-right: 0.2rem;
|
||||
|
||||
&.key-pri {
|
||||
color: goldenrod;
|
||||
}
|
||||
|
||||
&.key-uni {
|
||||
color: deepskyblue;
|
||||
}
|
||||
|
||||
&.key-mul {
|
||||
color: palegreen;
|
||||
}
|
||||
}
|
@@ -1,10 +1,41 @@
|
||||
.slide-fade-enter-active {
|
||||
transition: all .3s ease;
|
||||
}
|
||||
.slide-fade-leave-active {
|
||||
transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0);
|
||||
}
|
||||
.slide-fade-enter, .slide-fade-leave-to {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.slide-fade-enter,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateX(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.jump-down-enter-active {
|
||||
animation: jump-down-in 0.2s;
|
||||
}
|
||||
|
||||
.jump-down-leave-active {
|
||||
animation: jump-down-in 0.2s reverse;
|
||||
}
|
||||
|
||||
@keyframes jump-down-in {
|
||||
0% {
|
||||
transform: scale(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
/*Colors*/
|
||||
/* Colors */
|
||||
$body-bg: #1d1d1d;
|
||||
$body-font-color: #fff;
|
||||
$bg-color: #1d1d1d;
|
||||
@@ -7,10 +7,11 @@ $bg-color-gray: #272727;
|
||||
$primary-color: #e36929;
|
||||
$success-color: #32b643;
|
||||
$error-color: #de3b28;
|
||||
$warning-color: #e0a40c;
|
||||
|
||||
/*Sizes*/
|
||||
/* Sizes */
|
||||
$titlebar-height: 1.5rem;
|
||||
$settingbar-width: 3rem;
|
||||
$explorebar-width: 14rem;
|
||||
$footer-height: 1.5rem;
|
||||
$excluding-size: $footer-height + $titlebar-height;
|
||||
$excluding-size: $footer-height + $titlebar-height;
|
||||
|
@@ -1,150 +1,175 @@
|
||||
|
||||
@import "~spectre.css/src/variables";
|
||||
@import "variables";
|
||||
@import "transitions";
|
||||
@import "data-types";
|
||||
@import "table-keys";
|
||||
@import "fake-tables";
|
||||
@import "mdi-additions";
|
||||
@import "db-icons";
|
||||
@import "~spectre.css/src/spectre";
|
||||
@import "~spectre.css/src/spectre-exp";
|
||||
|
||||
body{
|
||||
user-select: none;
|
||||
body {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/*Additions*/
|
||||
/* Additions */
|
||||
@include margin-variant(3, $unit-3);
|
||||
@include margin-variant(4, $unit-4);
|
||||
@include padding-variant(3, $unit-3);
|
||||
@include padding-variant(4, $unit-4);
|
||||
|
||||
.btn.btn-gray{
|
||||
color: #fff;
|
||||
background: $bg-color-gray;
|
||||
.btn.btn-gray {
|
||||
color: #fff;
|
||||
background: $bg-color-gray;
|
||||
|
||||
&:hover{
|
||||
background: $bg-color;
|
||||
}
|
||||
&:hover {
|
||||
background: $bg-color;
|
||||
}
|
||||
}
|
||||
|
||||
.p-vcentered{
|
||||
display: flex!important;
|
||||
align-items: center;
|
||||
.p-vcentered {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.c-help{
|
||||
cursor: help;
|
||||
.c-help {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.bg-checkered {
|
||||
background-image:
|
||||
linear-gradient(to right, rgba(192, 192, 192, 0.75), rgba(192, 192, 192, 0.75)),
|
||||
linear-gradient(to right, black 50%, white 50%),
|
||||
linear-gradient(to bottom, black 50%, white 50%);
|
||||
background-blend-mode: normal, difference, normal;
|
||||
background-size: 2em 2em;
|
||||
}
|
||||
|
||||
// Scrollbars
|
||||
::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: $bg-color-light;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba($color: #FFF, $alpha: .5);
|
||||
|
||||
&:hover {
|
||||
background: rgba($color: #FFF, $alpha: 1);
|
||||
}
|
||||
::-webkit-scrollbar-track {
|
||||
background: $bg-color-light;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba($color: #fff, $alpha: 0.5);
|
||||
|
||||
&:hover {
|
||||
background: rgba($color: #fff, $alpha: 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Animations
|
||||
@keyframes rotation {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.rotate {
|
||||
animation: rotation .8s infinite linear;
|
||||
animation: rotation 0.8s infinite linear;
|
||||
}
|
||||
|
||||
/*Override*/
|
||||
.modal{
|
||||
.modal-overlay,
|
||||
&.active .modal-overlay{
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.modal-sm .modal-container,
|
||||
.modal-container{
|
||||
box-shadow: 0 0 1px 0px #000;
|
||||
padding: 0;
|
||||
background: $bg-color;
|
||||
|
||||
.modal-header{
|
||||
padding: .4rem .8rem;
|
||||
text-transform: uppercase;
|
||||
background: $bg-color-gray;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
/* Override */
|
||||
.modal {
|
||||
.modal-overlay,
|
||||
&.active .modal-overlay {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.modal-container,
|
||||
.modal-sm .modal-container {
|
||||
box-shadow: 0 0 1px 0 #000;
|
||||
padding: 0;
|
||||
background: $bg-color;
|
||||
|
||||
.modal-header {
|
||||
padding: 0.4rem 0.8rem;
|
||||
text-transform: uppercase;
|
||||
background: $bg-color-gray;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab{
|
||||
border-color: #272727;
|
||||
.tab {
|
||||
border-color: #272727;
|
||||
|
||||
.tab-item {
|
||||
.btn-clear {
|
||||
margin-top: -0.1rem;
|
||||
font-size: 0.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel{
|
||||
border: none;
|
||||
.panel {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.badge{
|
||||
&[data-badge],
|
||||
&:not([data-badge]){
|
||||
&::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
.badge {
|
||||
&[data-badge],
|
||||
&:not([data-badge]) {
|
||||
&::after {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.form-select{
|
||||
cursor: pointer;
|
||||
.form-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-select,
|
||||
.form-select:not([multiple]):not([size]),
|
||||
.form-input,
|
||||
.form-checkbox .form-icon,
|
||||
.form-radio .form-icon{
|
||||
border-color: $bg-color-light;
|
||||
background: $bg-color-gray;
|
||||
.form-select:not([multiple]):not([size]),
|
||||
.form-checkbox .form-icon,
|
||||
.form-radio .form-icon {
|
||||
border-color: $bg-color-light;
|
||||
background-color: $bg-color-gray;
|
||||
}
|
||||
|
||||
.form-select:not([multiple]):not([size]):focus{
|
||||
border-color: $primary-color;
|
||||
.form-input:not(:placeholder-shown):invalid:focus {
|
||||
background: $bg-color-gray;
|
||||
}
|
||||
|
||||
.menu{
|
||||
font-size: .7rem;
|
||||
.menu-item {
|
||||
+ .menu-item{
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
.form-select:not([multiple]):not([size]):focus {
|
||||
border-color: $primary-color;
|
||||
}
|
||||
|
||||
}
|
||||
.input-group .input-group-addon {
|
||||
border-color: #3f3f3f;
|
||||
}
|
||||
|
||||
.menu {
|
||||
font-size: 0.7rem;
|
||||
|
||||
.menu-item {
|
||||
+ .menu-item {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.accordion-body {
|
||||
max-height: 500rem!important;
|
||||
max-height: 500rem !important;
|
||||
}
|
||||
|
||||
.btn.loading {
|
||||
> .material-icons,
|
||||
> span{
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
> .mdi,
|
||||
> span {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ export default {
|
||||
namespaced: true,
|
||||
strict: true,
|
||||
state: {
|
||||
app_name: 'Antares - Database Client',
|
||||
app_name: 'Antares - SQL Client',
|
||||
app_version: process.env.PACKAGE_VERSION || 0,
|
||||
is_loading: false,
|
||||
is_new_modal: false,
|
||||
@@ -24,7 +24,7 @@ export default {
|
||||
isSettingModal: state => state.is_setting_modal,
|
||||
selectedSettingTab: state => state.selected_setting_tab,
|
||||
getUpdateStatus: state => state.update_status,
|
||||
getDownloadProgress: state => state.download_progress
|
||||
getDownloadProgress: state => Number(state.download_progress.toFixed(1))
|
||||
},
|
||||
mutations: {
|
||||
SET_LOADING_STATUS (state, payload) {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
'use strict';
|
||||
import { uidGen } from 'common/libs/utilities';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
@@ -20,7 +20,7 @@ export default {
|
||||
},
|
||||
actions: {
|
||||
addNotification ({ commit }, payload) {
|
||||
payload.uid = uidGen();
|
||||
payload.uid = uidGen('N');
|
||||
commit('ADD_NOTIFICATION', payload);
|
||||
},
|
||||
removeNotification ({ commit }, uid) {
|
||||
|
@@ -6,17 +6,22 @@ export default {
|
||||
strict: true,
|
||||
state: {
|
||||
locale: 'en-US',
|
||||
explorebar_size: null
|
||||
explorebar_size: null,
|
||||
notifications_timeout: 5
|
||||
},
|
||||
getters: {
|
||||
getLocale: state => state.locale,
|
||||
getExplorebarSize: state => state.explorebar_size
|
||||
getExplorebarSize: state => state.explorebar_size,
|
||||
getNotificationsTimeout: state => state.notifications_timeout
|
||||
},
|
||||
mutations: {
|
||||
SET_LOCALE (state, locale) {
|
||||
state.locale = locale;
|
||||
i18n.locale = locale;
|
||||
},
|
||||
SET_NOTIFICATIONS_TIMEOUT (state, timeout) {
|
||||
state.notifications_timeout = timeout;
|
||||
},
|
||||
SET_EXPLOREBAR_SIZE (state, size) {
|
||||
state.explorebar_size = size;
|
||||
}
|
||||
@@ -25,6 +30,9 @@ export default {
|
||||
changeLocale ({ commit }, locale) {
|
||||
commit('SET_LOCALE', locale);
|
||||
},
|
||||
updateNotificationsTimeout ({ commit }, timeout) {
|
||||
commit('SET_NOTIFICATIONS_TIMEOUT', timeout);
|
||||
},
|
||||
changeExplorebarSize ({ commit }, size) {
|
||||
commit('SET_EXPLOREBAR_SIZE', size);
|
||||
}
|
||||
|
@@ -1,18 +1,9 @@
|
||||
'use strict';
|
||||
import Connection from '@/ipc-api/Connection';
|
||||
import { uidGen } from 'common/libs/utilities';
|
||||
|
||||
function remapStructure (structure) {
|
||||
const databases = structure.map(table => table.TABLE_SCHEMA)
|
||||
.filter((value, index, self) => self.indexOf(value) === index);
|
||||
|
||||
return databases.map(db => {
|
||||
return {
|
||||
name: db,
|
||||
tables: structure.filter(table => table.TABLE_SCHEMA === db)
|
||||
};
|
||||
});
|
||||
}
|
||||
import Database from '@/ipc-api/Database';
|
||||
import { uidGen } from 'common/libs/uidGen';
|
||||
const tabIndex = [];
|
||||
let lastSchema = '';
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
@@ -28,8 +19,17 @@ export default {
|
||||
return null;
|
||||
},
|
||||
getWorkspace: state => uid => {
|
||||
const workspace = state.workspaces.filter(workspace => workspace.uid === uid);
|
||||
return workspace.length ? workspace[0] : {};
|
||||
return state.workspaces.find(workspace => workspace.uid === uid);
|
||||
},
|
||||
getDatabaseVariable: state => (uid, name) => {
|
||||
return state.workspaces.find(workspace => workspace.uid === uid).variables.find(variable => variable.name === name);
|
||||
},
|
||||
getWorkspaceTab: (state, getters) => tUid => {
|
||||
if (!getters.getSelected) return;
|
||||
const workspace = state.workspaces.find(workspace => workspace.uid === getters.getSelected);
|
||||
if ('tabs' in workspace)
|
||||
return workspace.tabs.find(tab => tab.uid === tUid);
|
||||
return {};
|
||||
},
|
||||
getConnected: state => {
|
||||
return state.workspaces
|
||||
@@ -50,6 +50,12 @@ export default {
|
||||
REFRESH_STRUCTURE (state, { uid, structure }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, structure } : workspace);
|
||||
},
|
||||
REFRESH_COLLATIONS (state, { uid, collations }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, collations } : workspace);
|
||||
},
|
||||
REFRESH_VARIABLES (state, { uid, variables }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, variables } : workspace);
|
||||
},
|
||||
ADD_WORKSPACE (state, workspace) {
|
||||
state.workspaces.push(workspace);
|
||||
},
|
||||
@@ -57,10 +63,15 @@ export default {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, breadcrumbs } : workspace);
|
||||
},
|
||||
NEW_TAB (state, uid) {
|
||||
tabIndex[uid] = tabIndex[uid] ? ++tabIndex[uid] : 1;
|
||||
|
||||
const newTab = {
|
||||
uid: uidGen(),
|
||||
uid: uidGen('T'),
|
||||
index: tabIndex[uid],
|
||||
selected: false,
|
||||
type: 'query'
|
||||
type: 'query',
|
||||
fields: [],
|
||||
keyUsage: []
|
||||
};
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === uid) {
|
||||
@@ -73,12 +84,58 @@ export default {
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
REMOVE_TAB (state, { uid, tab: tUid }) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === uid) {
|
||||
return {
|
||||
...workspace,
|
||||
tabs: workspace.tabs.filter(tab => tab.uid !== tUid)
|
||||
};
|
||||
}
|
||||
else
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
SELECT_TAB (state, { uid, tab }) {
|
||||
state.workspaces = state.workspaces.map(workspace => workspace.uid === uid ? { ...workspace, selected_tab: tab } : workspace);
|
||||
},
|
||||
SET_TAB_FIELDS (state, { cUid, tUid, fields }) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === cUid) {
|
||||
return {
|
||||
...workspace,
|
||||
tabs: workspace.tabs.map(tab => {
|
||||
if (tab.uid === tUid)
|
||||
return { ...tab, fields };
|
||||
else
|
||||
return tab;
|
||||
})
|
||||
};
|
||||
}
|
||||
else
|
||||
return workspace;
|
||||
});
|
||||
},
|
||||
SET_TAB_KEY_USAGE (state, { cUid, tUid, keyUsage }) {
|
||||
state.workspaces = state.workspaces.map(workspace => {
|
||||
if (workspace.uid === cUid) {
|
||||
return {
|
||||
...workspace,
|
||||
tabs: workspace.tabs.map(tab => {
|
||||
if (tab.uid === tUid)
|
||||
return { ...tab, keyUsage };
|
||||
else
|
||||
return tab;
|
||||
})
|
||||
};
|
||||
}
|
||||
else
|
||||
return workspace;
|
||||
});
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
selectWorkspace ({ commit }, uid) {
|
||||
selectWorkspace ({ commit, dispatch }, uid) {
|
||||
commit('SELECT_WORKSPACE', uid);
|
||||
},
|
||||
async connectWorkspace ({ dispatch, commit }, connection) {
|
||||
@@ -86,8 +143,11 @@ export default {
|
||||
const { status, response } = await Connection.connect(connection);
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else
|
||||
commit('ADD_CONNECTED', { uid: connection.uid, structure: remapStructure(response) });
|
||||
else {
|
||||
commit('ADD_CONNECTED', { uid: connection.uid, structure: response });
|
||||
dispatch('refreshCollations', connection.uid);
|
||||
dispatch('refreshVariables', connection.uid);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
@@ -95,11 +155,35 @@ export default {
|
||||
},
|
||||
async refreshStructure ({ dispatch, commit }, uid) {
|
||||
try {
|
||||
const { status, response } = await Connection.refresh(uid);
|
||||
const { status, response } = await Database.getStructure(uid);
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else
|
||||
commit('REFRESH_STRUCTURE', { uid, structure: remapStructure(response) });
|
||||
commit('REFRESH_STRUCTURE', { uid, structure: response });
|
||||
}
|
||||
catch (err) {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
}
|
||||
},
|
||||
async refreshCollations ({ dispatch, commit }, uid) {
|
||||
try {
|
||||
const { status, response } = await Database.getCollations(uid);
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else
|
||||
commit('REFRESH_COLLATIONS', { uid, collations: response });
|
||||
}
|
||||
catch (err) {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
}
|
||||
},
|
||||
async refreshVariables ({ dispatch, commit }, uid) {
|
||||
try {
|
||||
const { status, response } = await Database.getVariables(uid);
|
||||
if (status === 'error')
|
||||
dispatch('notifications/addNotification', { status, message: response }, { root: true });
|
||||
else
|
||||
commit('REFRESH_VARIABLES', { uid, variables: response });
|
||||
}
|
||||
catch (err) {
|
||||
dispatch('notifications/addNotification', { status: 'error', message: err.stack }, { root: true });
|
||||
@@ -115,24 +199,51 @@ export default {
|
||||
uid,
|
||||
connected: false,
|
||||
selected_tab: 0,
|
||||
tabs: [{ uid: 1, type: 'table' }],
|
||||
tabs: [{
|
||||
uid: 'data',
|
||||
type: 'table',
|
||||
fields: [],
|
||||
keyUsage: []
|
||||
},
|
||||
{
|
||||
uid: 'prop',
|
||||
type: 'table',
|
||||
fields: [],
|
||||
keyUsage: []
|
||||
}],
|
||||
structure: {},
|
||||
variables: [],
|
||||
collations: [],
|
||||
breadcrumbs: {}
|
||||
};
|
||||
|
||||
commit('ADD_WORKSPACE', workspace);
|
||||
|
||||
if (getters.getWorkspace(uid).tabs.length < 2)
|
||||
if (getters.getWorkspace(uid).tabs.length < 3)
|
||||
dispatch('newTab', uid);
|
||||
},
|
||||
changeBreadcrumbs ({ commit, getters }, payload) {
|
||||
if (lastSchema !== payload.schema) {
|
||||
Database.useSchema({ uid: getters.getSelected, schema: payload.schema });
|
||||
lastSchema = payload.schema;
|
||||
}
|
||||
|
||||
commit('CHANGE_BREADCRUMBS', { uid: getters.getSelected, breadcrumbs: payload });
|
||||
},
|
||||
newTab ({ commit }, uid) {
|
||||
commit('NEW_TAB', uid);
|
||||
},
|
||||
removeTab ({ commit }, payload) {
|
||||
commit('REMOVE_TAB', payload);
|
||||
},
|
||||
selectTab ({ commit }, payload) {
|
||||
commit('SELECT_TAB', payload);
|
||||
},
|
||||
setTabFields ({ commit }, payload) {
|
||||
commit('SET_TAB_FIELDS', payload);
|
||||
},
|
||||
setTabKeyUsage ({ commit }, payload) {
|
||||
commit('SET_TAB_KEY_USAGE', payload);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@@ -1,23 +1,23 @@
|
||||
import { ipcRenderer } from 'electron';
|
||||
|
||||
export default store => {
|
||||
ipcRenderer.on('checkingForUpdate', () => {
|
||||
ipcRenderer.on('checking-for-update', () => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'checking');
|
||||
});
|
||||
ipcRenderer.on('updateAvailable', () => {
|
||||
ipcRenderer.on('update-available', () => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'available');
|
||||
});
|
||||
ipcRenderer.on('updateNotAvailable', () => {
|
||||
ipcRenderer.on('update-not-available', () => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'noupdate');
|
||||
});
|
||||
ipcRenderer.on('checkFailed', () => {
|
||||
ipcRenderer.on('check-failed', () => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'nocheck');
|
||||
});
|
||||
ipcRenderer.on('downloadProgress', (event, data) => {
|
||||
ipcRenderer.on('download-progress', (event, data) => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'downloading');
|
||||
store.commit('application/CHANGE_PROGRESS_PERCENTAGE', data.percent);
|
||||
});
|
||||
ipcRenderer.on('updateDownloaded', () => {
|
||||
ipcRenderer.on('update-downloaded', () => {
|
||||
store.commit('application/CHANGE_UPDATE_STATUS', 'downloaded');
|
||||
});
|
||||
};
|
||||
|
12
src/renderer/suggestions/sql.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import { functions } from '@/suggestions/sql/sql-functions';
|
||||
import { keywords } from '@/suggestions/sql/sql-keywords';
|
||||
import { operators } from '@/suggestions/sql/sql-operators';
|
||||
import { variables } from '@/suggestions/sql/sql-variables';
|
||||
|
||||
export const completionItemProvider = (monaco) => {
|
||||
return {
|
||||
provideCompletionItems () {
|
||||
return { suggestions: [...functions(monaco), ...keywords(monaco), ...operators(monaco), ...variables(monaco)] };
|
||||
}
|
||||
};
|
||||
};
|