Compare commits
360 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12fa0749b1 | ||
|
|
75bf849823 | ||
|
|
3ee825defe | ||
|
|
77df3a8446 | ||
|
|
559e04200c | ||
|
|
4568780526 | ||
|
|
fa9108bc08 | ||
|
|
df16383d49 | ||
|
|
6f7961e1b4 | ||
|
|
2741aa6f33 | ||
|
|
6ceac83db9 | ||
|
|
a46625ebe7 | ||
|
|
5ef0b32549 | ||
|
|
f49fa0ea96 | ||
|
|
108ae454c1 | ||
|
|
43b6110c28 | ||
|
|
5a805fba80 | ||
|
|
58cdab94c1 | ||
|
|
b3d81666be | ||
|
|
fdf180d340 | ||
|
|
f2ff5aa2af | ||
|
|
0c1b91ab2f | ||
|
|
5e2b34a856 | ||
|
|
24786c9069 | ||
|
|
c28d31b019 | ||
|
|
6009ebb447 | ||
|
|
b5504b91ce | ||
|
|
828cad6439 | ||
|
|
8fa3c2ae58 | ||
|
|
aa5c907095 | ||
|
|
3a05b27400 | ||
|
|
108d96a753 | ||
|
|
f55a8caa92 | ||
|
|
87f9f9eb01 | ||
|
|
d6408bdd85 | ||
|
|
e14696b59e | ||
|
|
eee67763b5 | ||
|
|
637d8d26dd | ||
|
|
b30b2181e4 | ||
|
|
378b9fb580 | ||
|
|
244ba9eb08 | ||
|
|
53e5194295 | ||
|
|
04274ef19a | ||
|
|
3893a66f4e | ||
|
|
1b6b7c71e9 | ||
|
|
3f6427ff0e | ||
|
|
a2464d839f | ||
|
|
316e603c3c | ||
|
|
88466eca5e | ||
|
|
5123e39a60 | ||
|
|
4c8401f32f | ||
|
|
d949629ee4 | ||
|
|
7a18e415c8 | ||
|
|
878689b3f7 | ||
|
|
42f040975d | ||
|
|
78e9ca2120 | ||
|
|
96af391f20 | ||
|
|
f58b62eb0c | ||
|
|
b17040d3ef | ||
|
|
bc6154b9ad | ||
|
|
3aea8c951b | ||
|
|
1e982a1196 | ||
|
|
6ecbde7fd3 | ||
|
|
5ee881432a | ||
|
|
735e4ec7f6 | ||
|
|
07d31dbfe9 | ||
|
|
ac1f7de62c | ||
|
|
96877de532 | ||
|
|
b60fc28e47 | ||
|
|
bec3d9c737 | ||
|
|
8aac7af481 | ||
|
|
6982204e68 | ||
|
|
41e0ae7332 | ||
|
|
ebb5af4f10 | ||
|
|
ae26358b25 | ||
|
|
d9ee702b8e | ||
|
|
446045fa55 | ||
|
|
1a9d1b308b | ||
|
|
014ecf145e | ||
|
|
0044d82b6f | ||
|
|
998e8d66f7 | ||
|
|
db3dbdf993 | ||
|
|
4e13a16e33 | ||
|
|
9c0103fd05 | ||
|
|
e4b117ffb9 | ||
|
|
6320f818cb | ||
|
|
3c456ef135 | ||
|
|
c713c713b7 | ||
|
|
babf0074c0 | ||
|
|
e71e6700c1 | ||
|
|
84e66b8167 | ||
|
|
9e84cf269e | ||
|
|
e897b4913b | ||
|
|
0646f58ca0 | ||
|
|
c674bf11e3 | ||
|
|
2d8a91675e | ||
|
|
45b1021559 | ||
|
|
7216e996d1 | ||
|
|
6eae9a0f2d | ||
|
|
7486b32bd1 | ||
|
|
2c564767f8 | ||
|
|
289a727cbe | ||
|
|
5f2b8ba5a9 | ||
|
|
f0a4212e2b | ||
|
|
c8deff32c1 | ||
|
|
d56604a7d6 | ||
|
|
48e311bff8 | ||
|
|
518b22b489 | ||
|
|
a20dd7f849 | ||
|
|
310a939109 | ||
|
|
bb9ba08902 | ||
|
|
c7c727ff78 | ||
|
|
8669a6a9e5 | ||
|
|
c1cc5bb95e | ||
|
|
9c55e76a41 | ||
|
|
70a9edf57e | ||
|
|
b2c2344951 | ||
|
|
cbec91e78a | ||
|
|
816b0e6218 | ||
|
|
4ed93bbea7 | ||
|
|
3e3a70430f | ||
|
|
bce3854792 | ||
|
|
307cac06e6 | ||
|
|
c4b0bdc870 | ||
|
|
69e0b2129b | ||
|
|
4ebb3715d6 | ||
|
|
858e32c9fd | ||
|
|
42cce95ed0 | ||
|
|
61ffcc82d6 | ||
|
|
4716bcf258 | ||
|
|
23d2421584 | ||
|
|
4730afc68a | ||
|
|
6b902d5f00 | ||
|
|
a484c1e022 | ||
|
|
1ed5bf2fae | ||
|
|
da8dc71f23 | ||
|
|
ebac3d8f6c | ||
|
|
0336168bdc | ||
|
|
e38f482933 | ||
|
|
bcd98fe1ad | ||
|
|
ec3ec6f57a | ||
|
|
ae9e2bc03c | ||
|
|
fe11d446f3 | ||
|
|
e6bef139f1 | ||
|
|
d7e0e52da5 | ||
|
|
09cec13a9c | ||
|
|
0cae368350 | ||
|
|
88c62a1dfb | ||
|
|
ead861b610 | ||
|
|
949e6b626e | ||
|
|
6dc81f7be6 | ||
|
|
bb39dcbc2f | ||
|
|
131efb55df | ||
|
|
b33974c26c | ||
|
|
e9c97beb5a | ||
|
|
cabc4f8acd | ||
|
|
679a785d70 | ||
|
|
a91add40cc | ||
|
|
53b2d8372f | ||
|
|
f3e8448851 | ||
|
|
4213e9df5c | ||
|
|
9f32323a80 | ||
|
|
2ed5160f65 | ||
|
|
fe8ab3f3e8 | ||
|
|
848112979b | ||
|
|
2830df2adc | ||
|
|
5017b55944 | ||
|
|
8d0bc6affe | ||
|
|
d07506266c | ||
|
|
cea1d40797 | ||
|
|
0f2dc9f11e | ||
|
|
23250259eb | ||
|
|
fb930028de | ||
|
|
1ff4adf95c | ||
|
|
78cdb3809c | ||
|
|
3a6628cab9 | ||
|
|
418809d27d | ||
|
|
f9edeafd40 | ||
|
|
a37ed93306 | ||
|
|
cf4b83f7d4 | ||
|
|
2abd42c9c3 | ||
|
|
1251c542cb | ||
|
|
ac89259924 | ||
|
|
179ff8b1e1 | ||
|
|
99a10225a3 | ||
|
|
c96deb5766 | ||
|
|
700970e1cc | ||
|
|
e2be61e2cf | ||
|
|
9c2c8f3692 | ||
|
|
414a116f94 | ||
|
|
3e503f85a9 | ||
|
|
88257bfcf6 | ||
|
|
bdcc494138 | ||
|
|
d750541c80 | ||
|
|
75f743ff9e | ||
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b | ||
|
|
9b3dda6cff | ||
|
|
d94604ebfb | ||
|
|
16868ef430 | ||
|
|
b162c7043e | ||
|
|
8e856063b8 | ||
|
|
8684b4cef9 | ||
|
|
bcaebd4840 | ||
|
|
4619461af8 | ||
|
|
9fff1d699a | ||
|
|
5ab19c3fae | ||
|
|
cc483f4720 | ||
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a | ||
|
|
1f2327a724 | ||
|
|
6d512422cf | ||
|
|
8ce9a01372 | ||
|
|
acd56a85cb | ||
|
|
a45e218e3f | ||
|
|
a2bc495259 | ||
|
|
7f4b167dc2 | ||
|
|
5ded99e89f | ||
|
|
a991b02a20 | ||
|
|
9b6aa3d6c7 | ||
|
|
92022f9083 | ||
|
|
15636fed5f | ||
|
|
9ed53e0d25 | ||
|
|
35baaf2722 | ||
|
|
453098b410 | ||
|
|
a469de3674 | ||
|
|
24411ac18f | ||
|
|
a7ef152140 | ||
|
|
97c0c6191b | ||
|
|
9a91dd19bf | ||
|
|
472794e203 | ||
|
|
58d4a7bacd | ||
|
|
d0c624a3cc | ||
|
|
9e29a12ebb | ||
|
|
fdbac941d5 | ||
|
|
628e9cee62 | ||
|
|
803622f18f | ||
|
|
edd45b317a | ||
|
|
6ce5d2b201 | ||
|
|
5c9a5560b7 | ||
|
|
13a8b33815 | ||
|
|
5ba371e339 | ||
|
|
5db8376f95 | ||
|
|
be56079e4e | ||
|
|
a7c8c29624 | ||
|
|
9ce1c27cf5 | ||
|
|
b2d21cc859 | ||
|
|
1945467dc7 | ||
|
|
73c0936927 | ||
|
|
8867092601 | ||
|
|
c7039e144a | ||
|
|
b30eeb6788 | ||
|
|
c2864b4308 | ||
|
|
ba75685a23 | ||
|
|
9314e27d0e | ||
|
|
d4d68bb682 | ||
|
|
05b642bc86 | ||
|
|
62386d3fc0 | ||
|
|
2acfc75da2 | ||
|
|
98cc09741c | ||
|
|
abc852624a | ||
|
|
7791f477cb | ||
|
|
eb950c1b9c | ||
|
|
e60a28ca23 | ||
|
|
4b67fb268b | ||
|
|
8f445ba570 | ||
|
|
0bf5300c9d | ||
|
|
a8e12d64a0 | ||
|
|
aa63aa94ee | ||
|
|
8decda51a4 | ||
|
|
d52f822c15 | ||
|
|
507e978386 | ||
|
|
a457f967c0 | ||
|
|
854d2adfca | ||
|
|
6118a12672 | ||
|
|
7bd8388fd6 | ||
|
|
664c43bce1 | ||
|
|
95d6d6131e | ||
|
|
46757bd5ef | ||
|
|
43cfc8d815 | ||
|
|
6cb0d2d95d | ||
|
|
8e5e2787c7 | ||
|
|
4014aa5730 | ||
|
|
869da1da74 | ||
|
|
3b34f81d56 | ||
|
|
720f23745f | ||
|
|
797430e9f8 | ||
|
|
f6559c1015 | ||
|
|
2974bc6d34 | ||
|
|
f3beeb9f1c | ||
|
|
36f0809546 | ||
|
|
896b8af1a2 | ||
|
|
7e26446573 | ||
|
|
9a7dca3482 | ||
|
|
f6c7c9283e | ||
|
|
840f4fa450 | ||
|
|
d68616b543 | ||
|
|
9e41dab43e | ||
|
|
5006f6935a | ||
|
|
9d38f2a047 | ||
|
|
a787d9afb2 | ||
|
|
82592ed69d | ||
|
|
3bcf0b9ea6 | ||
|
|
cd866fcf50 | ||
|
|
8eb33633e2 | ||
|
|
5c17638a90 | ||
|
|
7550c1e8bd | ||
|
|
82b80e4760 | ||
|
|
6cf22cef09 | ||
|
|
a8f49d9187 | ||
|
|
d45212733d | ||
|
|
f640727d96 | ||
|
|
242b65c446 | ||
|
|
e041e58e7f | ||
|
|
083386383e | ||
|
|
3aa9c144e3 | ||
|
|
ba44a6dfc6 | ||
|
|
8652edc139 | ||
|
|
6886b7903f | ||
|
|
1be139d327 | ||
|
|
86825475f0 | ||
|
|
eb1f71608d | ||
|
|
58401edc35 | ||
|
|
6dede6ee4d | ||
|
|
42199f6ebc | ||
|
|
badcefc4f7 | ||
|
|
b55dec55ee | ||
|
|
8e73e526ec | ||
|
|
fd6d7b8b3a | ||
|
|
6612f4444b | ||
|
|
3168c77c17 | ||
|
|
33a9b261bf | ||
|
|
11721590c1 | ||
|
|
78e803b95d | ||
|
|
8d083a93c4 | ||
|
|
9035cae19f | ||
|
|
e98fa8793b | ||
|
|
ef461be3e5 | ||
|
|
4c2da33d01 | ||
|
|
c3ae115546 | ||
|
|
bbcb553fdc | ||
|
|
08e4f5333d | ||
|
|
dc3979584c | ||
|
|
34764a6e33 |
29
.eslintrc.cjs
Normal file
@@ -0,0 +1,29 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
es2022: true
|
||||
},
|
||||
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-case-declarations': 'off',
|
||||
'max-len': [2, 100, 4, { ignoreUrls: true }],
|
||||
'vue/multi-word-component-names': 'off',
|
||||
'vue/no-mutating-props': 'warn',
|
||||
'vue/no-reserved-component-names': 'warn',
|
||||
'vue/no-v-model-argument': 'off',
|
||||
'vue/require-default-prop': 'off',
|
||||
'vue/custom-event-name-casing': ['error', 'camelCase'],
|
||||
'vue/attribute-hyphenation': ['error', 'never']
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/**/*.spec.{j,t}s?(x)'],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
28
.eslintrc.js
@@ -1,28 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true
|
||||
},
|
||||
extends: [
|
||||
'plugin:vue/essential',
|
||||
'@vue/standard'
|
||||
],
|
||||
parserOptions: {
|
||||
parser: 'babel-eslint'
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
14
.github/workflows/config.grenrc.cjs
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
module.exports = {
|
||||
dataSource: 'milestones',
|
||||
ignoreIssuesWith: ['wontfix', 'duplicate'],
|
||||
milestoneMatch: 'v{{tag_name}}',
|
||||
template: {
|
||||
issue: '- {{name}} [{{text}}]({{url}})',
|
||||
changelogTitle: '',
|
||||
release: '{{body}}'
|
||||
},
|
||||
groupBy: {
|
||||
Enhancements: ['enhancement', 'internal'],
|
||||
'Bug fixes': ['bug']
|
||||
}
|
||||
}
|
||||
61
.github/workflows/main.yml
vendored
@@ -1,40 +1,45 @@
|
||||
name: Deploy to GitHub Pages and create release
|
||||
name: Create release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy to GitHub Pages and create release
|
||||
name: Create release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: npm install and build
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.x
|
||||
|
||||
- name: Create archive
|
||||
run: |
|
||||
cd dist
|
||||
zip -9 -r dist.zip . -x "js/*.map"
|
||||
- name: Update npm
|
||||
run: npm install -g npm@10
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/dist.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: npm install and build
|
||||
run: |
|
||||
npm install
|
||||
npm run build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: build # The branch the action should deploy to.
|
||||
FOLDER: dist/ # The folder the action should deploy.
|
||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
||||
- name: Create archives
|
||||
run: |
|
||||
cd dist
|
||||
zip -9 -r ../dist.zip . -x "*.map"
|
||||
zip -9 -r ../dist_map.zip .
|
||||
|
||||
- name: Create Release Notes
|
||||
run: |
|
||||
npm install github-release-notes@0.16.0 -g
|
||||
gren changelog --generate --config="/.github/workflows/config.grenrc.cjs"
|
||||
env:
|
||||
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create release
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: 'dist.zip,dist_map.zip'
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyFile: 'CHANGELOG.md'
|
||||
|
||||
38
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
name: Run tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 18.x
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo add-apt-repository -y ppa:mozillateam/ppa
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium-browser firefox-esr
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@10
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint -- --no-fix
|
||||
|
||||
- name: Run karma tests
|
||||
run: npm run test
|
||||
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"trailingComma": "none",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
24
Dockerfile.test
Normal file
@@ -0,0 +1,24 @@
|
||||
# An easy way to run tests locally without Nodejs installed:
|
||||
#
|
||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||
#
|
||||
|
||||
FROM node:12.22-bullseye
|
||||
|
||||
RUN set -ex; \
|
||||
apt update; \
|
||||
apt install -y chromium firefox-esr; \
|
||||
npm install -g npm@7
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY lib lib
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -ex; \
|
||||
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
|
||||
|
||||
RUN npm run lint -- --no-fix && npm run test
|
||||
42
README.md
@@ -4,22 +4,46 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
**Note: This is a work-in-progress project.**
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
|
||||
of SQLite databases, CSV, JSON or NDJSON files.
|
||||
|
||||
With sqliteviz you can:
|
||||
|
||||
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
||||
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
|
||||
- export result set to CSV file
|
||||
- manage inquiries and run them against different databases
|
||||
- import/export inquiries from/to a JSON file
|
||||
- export a modified SQLite database
|
||||
- use it offline from your OS application menu like any other desktop app
|
||||
|
||||
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
|
||||
|
||||
## Quickstart
|
||||
|
||||
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
|
||||
|
||||
## Wiki
|
||||
|
||||
For user documentation, check out sqliteviz [documentation][7].
|
||||
|
||||
## Motivation
|
||||
|
||||
Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases.
|
||||
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
||||
It is built on top of [react-chart-editor][3] and [sql.js][4] in [Vue.js][5].
|
||||
|
||||
## Features
|
||||
## Components
|
||||
|
||||
1. Ad-hoc and prepared visualisation of a SQLite database using flexibility
|
||||
of SQL and richness of Plotly's `react-chart-editor`
|
||||
2. Visualisation pallete, SQL and chart definitions, kept in local
|
||||
storage with import/export
|
||||
3. Integration-side provided pallete
|
||||
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
||||
|
||||
[1]: https://github.com/plotly/falcon
|
||||
[2]: https://github.com/getredash/redash
|
||||
[3]: https://github.com/plotly/react-chart-editor
|
||||
[4]: https://github.com/sql-js/sql.js
|
||||
[5]: https://github.com/vuejs/vue
|
||||
[6]: https://sqliteviz.com/app/
|
||||
[7]: https://sqliteviz.com/docs
|
||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||
[9]: https://www.papaparse.com/
|
||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||
[11]: https://github.com/plotly/plotly.js
|
||||
[12]: https://github.com/nicolaskruchten/pivottable
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
'@vue/cli-plugin-babel/preset'
|
||||
]
|
||||
presets: ['@vue/cli-plugin-babel/preset']
|
||||
}
|
||||
|
||||
98
index.html
Normal file
@@ -0,0 +1,98 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
|
||||
<link rel="icon" href="favicon.png" />
|
||||
<link rel="manifest" href="manifest.webmanifest" />
|
||||
<title>sqliteviz</title>
|
||||
<style>
|
||||
#sqliteviz-loading-wrapper {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-text {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #506784;
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
fill: none;
|
||||
stroke-width: 5px;
|
||||
stroke-linecap: round;
|
||||
stroke: #119dff;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle.bg {
|
||||
stroke: #c8d4e3;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle.front {
|
||||
stroke-dasharray: 402px;
|
||||
animation: sqliteviz-loading 2s linear 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes sqliteviz-loading {
|
||||
0% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 251px;
|
||||
stroke-dashoffset: -251px;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: -502px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- head extention slot start -->
|
||||
<!-- head extention slot end -->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>
|
||||
We're sorry but this app doesn't work properly without JavaScript
|
||||
enabled. Please enable it to continue.
|
||||
</strong>
|
||||
</noscript>
|
||||
<div id="app">
|
||||
<div id="sqliteviz-loading-wrapper">
|
||||
<div id="sqliteviz-loading-text">LOADING</div>
|
||||
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||
<circle class="bg" cx="85" cy="85" r="80" />
|
||||
<circle class="front" cx="85" cy="85" r="80" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- extention slot start -->
|
||||
<!-- extention slot end -->
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
10
jsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"include": ["src/**/*", "tests/**/*"],
|
||||
"exclude": ["node_modules", "dist"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"paths": {
|
||||
"@*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
122
karma.conf.cjs
Normal file
@@ -0,0 +1,122 @@
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
vite: {
|
||||
config: {
|
||||
resolve: {
|
||||
alias: {
|
||||
vue: 'vue/dist/vue.esm-bundler.js'
|
||||
}
|
||||
},
|
||||
server: {
|
||||
preTransformRequests: false
|
||||
}
|
||||
},
|
||||
coverage: {
|
||||
enable: true,
|
||||
include: 'src/*',
|
||||
exclude: ['node_modules', 'src/components/svg/*'],
|
||||
extension: ['.js', '.vue'],
|
||||
requireEnv: false
|
||||
}
|
||||
},
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ['mocha', 'sinon-chai', 'vite'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
{
|
||||
pattern: 'test.setup.js',
|
||||
type: 'module',
|
||||
watched: false,
|
||||
served: false
|
||||
},
|
||||
{
|
||||
pattern: 'tests/**/*.spec.js',
|
||||
type: 'module',
|
||||
watched: false,
|
||||
served: false
|
||||
},
|
||||
{
|
||||
pattern: 'src/assets/styles/*.css',
|
||||
type: 'css',
|
||||
watched: false,
|
||||
served: false
|
||||
}
|
||||
],
|
||||
|
||||
plugins: [
|
||||
'karma-vite',
|
||||
'karma-mocha',
|
||||
'karma-sinon-chai',
|
||||
'karma-firefox-launcher',
|
||||
'karma-chrome-launcher',
|
||||
'karma-spec-reporter',
|
||||
'karma-coverage'
|
||||
],
|
||||
|
||||
// list of files / patterns to exclude
|
||||
exclude: [],
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ['spec', 'coverage'],
|
||||
|
||||
coverageReporter: {
|
||||
dir: 'coverage',
|
||||
reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
|
||||
},
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
|
||||
// config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
customLaunchers: {
|
||||
FirefoxHeadlessTouch: {
|
||||
base: 'FirefoxHeadless',
|
||||
prefs: {
|
||||
'dom.w3c_touch_events.enabled': 1,
|
||||
'dom.events.asyncClipboard.clipboardItem': true
|
||||
}
|
||||
}
|
||||
},
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: 2,
|
||||
|
||||
client: {
|
||||
captureConsole: true,
|
||||
mocha: {
|
||||
timeout: 7000
|
||||
}
|
||||
},
|
||||
browserConsoleLogOptions: {
|
||||
terminal: true,
|
||||
level: ''
|
||||
}
|
||||
})
|
||||
// Fix the timezone
|
||||
process.env.TZ = 'Europe/Amsterdam'
|
||||
}
|
||||
173
karma.conf.js
@@ -1,173 +0,0 @@
|
||||
// Karma configuration
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const VueLoaderPlugin = require("vue-loader/lib/plugin");
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir);
|
||||
}
|
||||
|
||||
module.exports = function(config) {
|
||||
config.set({
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: "",
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ["mocha", "sinon-chai"],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
"tests/unit/*.spec.js",
|
||||
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false
|
||||
},
|
||||
{ pattern: 'node_modules/sql.js/dist/worker.sql-wasm.js',
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false
|
||||
}
|
||||
],
|
||||
|
||||
// list of files / patterns to exclude
|
||||
exclude: [],
|
||||
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
"tests/unit/*.spec.js": ["webpack"]
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["spec", "coverage"],
|
||||
|
||||
coverageReporter: {
|
||||
dir: "coverage",
|
||||
reporters: [{ type: "lcov", subdir: "." }, { type: "text-summary" }]
|
||||
},
|
||||
|
||||
// !!DONOT delete this reporter, or vue-cli-addon-ui-karma doesnot work
|
||||
jsonResultReporter: {
|
||||
outputFile: "report/karma-result.json",
|
||||
isSynchronous: true
|
||||
},
|
||||
|
||||
junitReporter: {
|
||||
outputDir: "report", // results will be saved as $outputDir/$browserName.xml
|
||||
outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile
|
||||
suite: "", // suite will become the package name attribute in xml testsuite element
|
||||
useBrowserName: true, // add browser name to report and classes names
|
||||
nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element
|
||||
classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element
|
||||
properties: {} // key value pair of properties to add to the <properties> section of the report
|
||||
},
|
||||
|
||||
// web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
// level of logging
|
||||
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["ChromiumHeadless"],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
singleRun: true,
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
|
||||
client: {
|
||||
captureConsole: true
|
||||
},
|
||||
browserConsoleLogOptions: {
|
||||
terminal: true,
|
||||
level: ""
|
||||
},
|
||||
webpack: {
|
||||
mode: "development",
|
||||
entry: "./src/main.js",
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.esm.js",
|
||||
"@": resolve("src")
|
||||
}
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader"
|
||||
},
|
||||
{
|
||||
loader: "istanbul-instrumenter-loader",
|
||||
options: {
|
||||
esModules: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: "url-loader"
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader",
|
||||
options: {
|
||||
loaders: {
|
||||
js: "babel-loader"
|
||||
},
|
||||
postLoaders: {
|
||||
js: "istanbul-instrumenter-loader?esModules=true"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["vue-style-loader", "css-loader"]
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ["vue-style-loader", "css-loader", "sass-loader"]
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: "url-loader",
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: resolve("fonts/[name].[hash:7].[ext]")
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [new VueLoaderPlugin()],
|
||||
node: {
|
||||
fs: 'empty'
|
||||
}
|
||||
},
|
||||
proxies: {
|
||||
"/js/": "/base/node_modules/sql.js/dist/"
|
||||
}
|
||||
});
|
||||
};
|
||||
2
lib/sql-js/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
benchmark
|
||||
dist
|
||||
9
lib/sql-js/Dockerfile
Normal file
@@ -0,0 +1,9 @@
|
||||
FROM emscripten/emsdk:3.0.1
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY configure.py .
|
||||
RUN python3.8 configure.py
|
||||
|
||||
COPY build.py .
|
||||
RUN python3.8 build.py
|
||||
108
lib/sql-js/README.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# SQLite WebAssembly build
|
||||
|
||||
This directory contains Docker-based build script, `make.sh`, that builds
|
||||
a custom version of [sql.js][1]. It allows sqliteviz to have more recent
|
||||
version of SQLite build with a number of useful extensions.
|
||||
|
||||
`Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py`
|
||||
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
|
||||
|
||||
## Extension
|
||||
|
||||
SQLite [amalgamation][2] extensions included:
|
||||
|
||||
1. [FTS5][4] -- virtual table module that provides full-text search
|
||||
functionality
|
||||
2. [FTS3/FTS4][15] -- older virtual table modules for full-text search
|
||||
3. [JSON1][16] -- scalar, aggregate and table-valued functions for managing JSON data
|
||||
|
||||
SQLite [contribution extensions][17]:
|
||||
|
||||
1. [extension-functions][18] -- mathematical and string extension functions for SQL queries.
|
||||
|
||||
Math: `acos`, `asin`, `atan`, `atn2`, `atan2`, `acosh`, `asinh`, `atanh`, `difference`,
|
||||
`degrees`, `radians`, `cos`, `sin`, `tan`, `cot`, `cosh`, `sinh`, `tanh`, `coth`,
|
||||
`exp`, `log`, `log10`, `power`, `sign`, `sqrt`, `square`, `ceil`, `floor`, `pi`.
|
||||
|
||||
String: `replicate`, `charindex`, `leftstr`, `rightstr`, `ltrim`, `rtrim`, `trim`,
|
||||
`replace`, `reverse`, `proper`, `padl`, `padr`, `padc`, `strfilter`.
|
||||
|
||||
Aggregate: `stdev`, `variance`, `mode`, `median`, `lower_quartile`, `upper_quartile`.
|
||||
|
||||
SQLite [miscellaneous extensions][3] included:
|
||||
|
||||
1. `generate_series` table-valued [series function][6] ([series.c][7])
|
||||
2. `transitive_closure` virtual table for
|
||||
[Querying Tree Structures in SQLite][11] ([closure.c][8])
|
||||
3. `uuid`, `uuid_str` and `uuid_blob` RFC-4122 UUID functions ([uuid.c][9])
|
||||
4. `regexp` (hence `REGEXP` operator) and `regexpi` functions ([regexp.c][10])
|
||||
5. `percentile` function ([percentile.c][13])
|
||||
6. `decimal`, `decimal_cmp`, `decimal_add`, `decimal_sub` and `decimal_mul` functions
|
||||
([decimal.c][14])
|
||||
|
||||
SQLite 3rd party extensions included:
|
||||
|
||||
1. [pivot_vtab][5] -- a pivot virtual table
|
||||
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||
(which is part of [squib][20])
|
||||
3. [sqlitelua][22] -- a virtual table `luafunctions` which allows to define custom scalar,
|
||||
aggregate and table-valued functions in Lua
|
||||
|
||||
To ease the step to have working clone locally, the build is committed into
|
||||
the repository.
|
||||
|
||||
Examples of queries involving these extensions can be found in the test suite in
|
||||
[sqliteExtensions.spec.js][19].
|
||||
|
||||
## Build method
|
||||
|
||||
Basically it's extended amalgamation and `SQLITE_EXTRA_INIT` concisely
|
||||
described in [this message from SQLite Forum][12]:
|
||||
|
||||
> Simply append it to the end of the amalgamation file. The real problem is
|
||||
> how you get the init function called. The easiest way (to me at any rate) is
|
||||
> to append a function (after the extensions you want to add are all appended)
|
||||
> that adds the init function for each extension to the auto extension list
|
||||
> for new connections, and set the pre-processor symbol SQLITE_EXTRA_INIT to
|
||||
> the name of this function. [...]
|
||||
>
|
||||
> An example `SQLITE_EXTRA_INIT` function looks like this:
|
||||
>
|
||||
> ```
|
||||
> int core_init(const char* dummy)
|
||||
> {
|
||||
> int nErr = 0;
|
||||
>
|
||||
> nErr += sqlite3_auto_extension((void*)sqlite3_autobusy_init);
|
||||
> nErr += sqlite3_auto_extension((void*)sqlite3_ipaddress_init);
|
||||
>
|
||||
> return nErr ? SQLITE_ERROR : SQLITE_OK;
|
||||
> }
|
||||
> ```
|
||||
>
|
||||
> so you would then define `SQLITE_EXTRA_INIT=core_init` when compiling the
|
||||
> amalgamation code and the extensions would thereafter be automatically
|
||||
> initialized on each connection.
|
||||
|
||||
[1]: https://github.com/sql-js/sql.js
|
||||
[2]: https://sqlite.org/amalgamation.html
|
||||
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
|
||||
[4]: https://sqlite.org/fts5.html
|
||||
[5]: https://github.com/jakethaw/pivot_vtab
|
||||
[6]: https://sqlite.org/series.html
|
||||
[7]: https://sqlite.org/src/file/ext/misc/series.c
|
||||
[8]: https://sqlite.org/src/file/ext/misc/closure.c
|
||||
[9]: https://sqlite.org/src/file/ext/misc/uuid.c
|
||||
[10]: https://sqlite.org/src/file/ext/misc/regexp.c
|
||||
[11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/
|
||||
[12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw
|
||||
[13]: https://sqlite.org/src/file/ext/misc/percentile.c
|
||||
[14]: https://sqlite.org/src/file/ext/misc/decimal.c
|
||||
[15]: https://sqlite.org/fts3.html
|
||||
[16]: https://sqlite.org/json1.html
|
||||
[17]: https://sqlite.org/contrib/
|
||||
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
|
||||
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
|
||||
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
||||
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c
|
||||
[22]: https://github.com/kev82/sqlitelua
|
||||
4
lib/sql-js/benchmark/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/lib/build-*
|
||||
/lib/dist
|
||||
/build-*-result.json
|
||||
/sample.csv
|
||||
17
lib/sql-js/benchmark/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM node:20.14-bookworm
|
||||
|
||||
RUN set -ex; \
|
||||
apt-get update; \
|
||||
apt-get install -y firefox-esr; \
|
||||
apt-get install -y chromium
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY package.json ./
|
||||
COPY lib/dist lib/dist
|
||||
COPY lib/package.json lib/package.json
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD npm run benchmark
|
||||
25
lib/sql-js/benchmark/README.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# SQLite WebAssembly build micro-benchmark
|
||||
|
||||
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
|
||||
builds performance on read and write SQL queries, run from `make.sh` script. If
|
||||
the script has permission to `nice` processes and [Procpath][1] is installed,
|
||||
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
|
||||
processes running inside the benchmark containers. It can also serve as a smoke
|
||||
test (e.g. for memory leaks).
|
||||
|
||||
The benchmark operates on a set of SQLite WebAssembly builds expected in
|
||||
`lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
|
||||
benchmark in Firefox and Chromium using Karma in the container.
|
||||
|
||||
After successful run, the benchmark produces the following per each build:
|
||||
|
||||
- `build-$NAME-result.json`
|
||||
- `build-$NAME.sqlite` (if Procpath is installed)
|
||||
- `build-$NAME.svg` (if Procpath is installed)
|
||||
|
||||
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
|
||||
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
|
||||
run, then Firefox run per container).
|
||||
|
||||
[1]: https://pypi.org/project/Procpath/
|
||||
57
lib/sql-js/benchmark/karma.conf.js
Normal file
@@ -0,0 +1,57 @@
|
||||
module.exports = function (config) {
|
||||
const timeout = 15 * 60 * 1000
|
||||
config.set({
|
||||
frameworks: ['mocha'],
|
||||
|
||||
files: [
|
||||
'suite.js',
|
||||
{
|
||||
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
served: true,
|
||||
included: false
|
||||
},
|
||||
{ pattern: 'sample.csv', served: true, included: false }
|
||||
],
|
||||
|
||||
reporters: ['progress', 'json-to-file'],
|
||||
|
||||
singleRun: true,
|
||||
|
||||
customLaunchers: {
|
||||
ChromiumHeadlessNoSandbox: {
|
||||
base: 'ChromiumHeadless',
|
||||
flags: ['--no-sandbox']
|
||||
}
|
||||
},
|
||||
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
|
||||
concurrency: 1,
|
||||
|
||||
browserDisconnectTimeout: timeout,
|
||||
browserNoActivityTimeout: timeout,
|
||||
captureTimeout: timeout,
|
||||
browserSocketTimeout: timeout,
|
||||
pingTimeout: timeout,
|
||||
client: {
|
||||
captureConsole: true,
|
||||
mocha: { timeout: timeout }
|
||||
},
|
||||
|
||||
logLevel: config.LOG_INFO,
|
||||
browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
|
||||
|
||||
preprocessors: { 'suite.js': ['webpack'] },
|
||||
webpack: {
|
||||
mode: 'development',
|
||||
module: {
|
||||
noParse: [__dirname + '/node_modules/benchmark/benchmark.js']
|
||||
},
|
||||
node: { fs: 'empty' }
|
||||
},
|
||||
|
||||
proxies: {
|
||||
'/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
|
||||
},
|
||||
|
||||
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
|
||||
})
|
||||
}
|
||||
5
lib/sql-js/benchmark/lib/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sql.js",
|
||||
"main": "./dist/sql-wasm.js",
|
||||
"private": true
|
||||
}
|
||||
47
lib/sql-js/benchmark/make.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
cleanup () {
|
||||
rm -rf lib/dist "$renice_flag_file"
|
||||
docker rm -f sqljs-benchmark-run 2> /dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [ ! -f sample.csv ]; then
|
||||
wget --header="accept-encoding: gzip" -q -O- \
|
||||
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
||||
| gunzip -c > sample.csv
|
||||
fi
|
||||
|
||||
PLAYBOOK=procpath/karma_docker.procpath
|
||||
|
||||
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
|
||||
test_ni=$(nice -n -5 nice)
|
||||
if [ $test_ni == -5 ]; then
|
||||
renice_flag_file=$(mktemp)
|
||||
fi
|
||||
{
|
||||
while [ -f $renice_flag_file ]; do
|
||||
procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
|
||||
done
|
||||
} &
|
||||
|
||||
shopt -s nullglob
|
||||
for d in lib/build-* ; do
|
||||
rm -rf lib/dist
|
||||
cp -r $d lib/dist
|
||||
sample_name=$(basename $d)
|
||||
|
||||
docker build -t sqliteviz/sqljs-benchmark .
|
||||
docker rm sqljs-benchmark-run 2> /dev/null || true
|
||||
docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
|
||||
{
|
||||
rm -f ${sample_name}.sqlite
|
||||
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
|
||||
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
|
||||
-o plot_file=${sample_name}.svg track:plot
|
||||
} &
|
||||
|
||||
docker attach sqljs-benchmark-run
|
||||
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
|
||||
docker rm sqljs-benchmark-run
|
||||
done
|
||||
23
lib/sql-js/benchmark/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "sqlite-webassembly-microbenchmark",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.14.8",
|
||||
"babel-loader": "^8.2.2",
|
||||
"benchmark": "^2.1.4",
|
||||
"lodash": "^4.17.4",
|
||||
"papaparse": "^5.3.1",
|
||||
"mocha": "^9.0.3",
|
||||
"karma": "^6.3.4",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-firefox-launcher": "^2.1.1",
|
||||
"karma-json-to-file-reporter": "^1.0.1",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"webpack": "^4.46.0",
|
||||
"sql.js": "file:./lib"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "karma start karma.conf.js"
|
||||
}
|
||||
}
|
||||
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
@@ -0,0 +1,28 @@
|
||||
# This command may run when "sqljs-benchmark-run" does not yet exist or run
|
||||
[renice:watch]
|
||||
interval: 2
|
||||
repeat: 30
|
||||
environment:
|
||||
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
|
||||
query:
|
||||
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
|
||||
command:
|
||||
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
|
||||
|
||||
# Expected input arguments: database_file
|
||||
[track:record]
|
||||
interval: 1
|
||||
stop_without_result: 1
|
||||
environment:
|
||||
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
|
||||
query:
|
||||
$..children[?(@.stat.pid == $ROOT_PID)]
|
||||
pid_list: $ROOT_PID
|
||||
|
||||
# Expected input arguments: database_file, plot_file
|
||||
[track:plot]
|
||||
moving_average_window: 5
|
||||
title: Chromium vs Firefox (№1 RSS, №2 CPU)
|
||||
custom_query_file:
|
||||
procpath/top2_rss.sql
|
||||
procpath/top2_cpu.sql
|
||||
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
@@ -0,0 +1,29 @@
|
||||
WITH diff_all AS (
|
||||
SELECT
|
||||
record_id,
|
||||
ts,
|
||||
stat_pid,
|
||||
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
|
||||
PARTITION BY stat_pid
|
||||
ORDER BY record_id
|
||||
) tick_diff,
|
||||
ts - LAG(ts) OVER (
|
||||
PARTITION BY stat_pid
|
||||
ORDER BY record_id
|
||||
) ts_diff
|
||||
FROM record
|
||||
), diff AS (
|
||||
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
|
||||
), one_time_pid_condition AS (
|
||||
SELECT stat_pid
|
||||
FROM record
|
||||
GROUP BY 1
|
||||
ORDER BY SUM(stat_utime + stat_stime) DESC
|
||||
LIMIT 2
|
||||
)
|
||||
SELECT
|
||||
ts,
|
||||
stat_pid pid,
|
||||
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
|
||||
FROM diff
|
||||
JOIN one_time_pid_condition USING(stat_pid)
|
||||
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
WITH one_time_pid_condition AS (
|
||||
SELECT stat_pid
|
||||
FROM record
|
||||
GROUP BY 1
|
||||
ORDER BY SUM(stat_rss) DESC
|
||||
LIMIT 2
|
||||
)
|
||||
SELECT
|
||||
ts,
|
||||
stat_pid pid,
|
||||
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
|
||||
FROM record
|
||||
JOIN one_time_pid_condition USING(stat_pid)
|
||||
218
lib/sql-js/benchmark/result-analysis.ipynb
Normal file
@@ -0,0 +1,218 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"import json\n",
|
||||
"from pathlib import Path\n",
|
||||
"\n",
|
||||
"import pandas\n",
|
||||
"from IPython.display import display, IFrame, Markdown\n",
|
||||
"from plotly import express"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"outputHidden": false,
|
||||
"inputHidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"render_format = 'svg'\n",
|
||||
"benchmark_path = Path()\n",
|
||||
"df_dict = {}\n",
|
||||
"stat_dict = {}\n",
|
||||
"for p in benchmark_path.glob('build-*-result.json'):\n",
|
||||
" build_name = p.stem.split('-', 2)[1]\n",
|
||||
" for browser_data in json.loads(p.read_text()):\n",
|
||||
" browser_name = f'{browser_data[\"browser\"][\"name\"]} {browser_data[\"browser\"][\"major\"]}'\n",
|
||||
" browser_name = browser_name.lower().replace('chrome headless', 'chromium')\n",
|
||||
" for result in (r for i, r in browser_data['result'].items() if i.isdigit()):\n",
|
||||
" key = build_name, browser_name, result['name']\n",
|
||||
" df_dict[key] = result['stats']['sample']\n",
|
||||
" stat_dict[key] = result['stats']\n",
|
||||
"\n",
|
||||
"min_sample_size = min(len(v) for v in df_dict.values())\n",
|
||||
"df_dict = {k: v[:min_sample_size] for k, v in df_dict.items()}\n",
|
||||
" \n",
|
||||
"wide_df = pandas.DataFrame(df_dict).reset_index()\n",
|
||||
"df = pandas.melt(\n",
|
||||
" wide_df, \n",
|
||||
" id_vars='index', \n",
|
||||
" var_name=['build', 'browser', 'test'], \n",
|
||||
" value_name='duration',\n",
|
||||
")\n",
|
||||
"df = df.rename(columns={'index': 'run'})\n",
|
||||
"df.sort_values(['build', 'run'], inplace=True)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"source": [
|
||||
"# sql.js build comparison\n",
|
||||
"\n",
|
||||
"<style>\n",
|
||||
"@page {\n",
|
||||
" size: 215mm 297mm;\n",
|
||||
" margin: 0;\n",
|
||||
"}\n",
|
||||
"</style>"
|
||||
],
|
||||
"metadata": {}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"!du -b lib | head -n 2"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"outputHidden": false,
|
||||
"inputHidden": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"stat_df = pandas.DataFrame(stat_dict)\n",
|
||||
"stat_df = stat_df.loc[['mean', 'rme']].transpose()\n",
|
||||
"stat_df.index = stat_df.index.set_names(['build', 'browser', 'test'])\n",
|
||||
"stat_df = stat_df.reset_index().sort_values(['test', 'browser'], ascending=False)\n",
|
||||
"for index, row in stat_df.iterrows():\n",
|
||||
" print('\\t'.join([\n",
|
||||
" row['build'],\n",
|
||||
" row['browser'],\n",
|
||||
" row['test'],\n",
|
||||
" f'{row[\"mean\"]:.2f} s ± {row[\"rme\"]:.1f}%'\n",
|
||||
" ]))"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"fig = express.box(df, x='browser', y='duration', points='all', color='build', facet_row='test')\n",
|
||||
"fig.update_yaxes(matches=None)\n",
|
||||
"fig.show(render_format)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"fig = express.line(\n",
|
||||
" df, x='run', y='duration', color='build', facet_col='browser', facet_row='test'\n",
|
||||
")\n",
|
||||
"fig.update_yaxes(matches=None)\n",
|
||||
"fig.show(render_format)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"plot_df = df.groupby(['browser', 'build', 'test'])['duration'].mean().reset_index()\n",
|
||||
"plot_df['pct'] = (\n",
|
||||
" plot_df\n",
|
||||
" .groupby(['browser', 'test'])['duration']\n",
|
||||
" .pct_change()\n",
|
||||
" .fillna('')\n",
|
||||
" .map(lambda v: '{:.2%}'.format(v) if v else v)\n",
|
||||
")\n",
|
||||
"fig = express.bar(\n",
|
||||
" plot_df, \n",
|
||||
" x='browser', \n",
|
||||
" y='duration', \n",
|
||||
" color='build', \n",
|
||||
" text='pct', \n",
|
||||
" barmode='overlay', \n",
|
||||
" facet_row='test',\n",
|
||||
")\n",
|
||||
"fig.update_yaxes(matches=None)\n",
|
||||
"fig.show(render_format)"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"source": [
|
||||
"for p in sorted(benchmark_path.glob('build-*.svg')):\n",
|
||||
" display(Markdown(p.stem))\n",
|
||||
" display(IFrame(p, 640, 480))"
|
||||
],
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"metadata": {
|
||||
"collapsed": false,
|
||||
"outputHidden": false,
|
||||
"inputHidden": true,
|
||||
"outputExpanded": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"name": "stats",
|
||||
"language": "python",
|
||||
"display_name": "Stats (Python 3.10)"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.10.14",
|
||||
"mimetype": "text/x-python",
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 3
|
||||
},
|
||||
"pygments_lexer": "ipython3",
|
||||
"nbconvert_exporter": "python",
|
||||
"file_extension": ".py"
|
||||
},
|
||||
"widgets": {
|
||||
"application/vnd.jupyter.widget-state+json": {
|
||||
"state": {},
|
||||
"version_major": 2,
|
||||
"version_minor": 0
|
||||
}
|
||||
},
|
||||
"kernel_info": {
|
||||
"name": "stats"
|
||||
},
|
||||
"nteract": {
|
||||
"version": "0.14.5"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
137
lib/sql-js/benchmark/suite.js
Normal file
@@ -0,0 +1,137 @@
|
||||
import benchmark from 'benchmark'
|
||||
import initSqlJs from 'sql.js'
|
||||
import lodash from 'lodash'
|
||||
import Papa from 'papaparse'
|
||||
import useragent from 'ua-parser-js'
|
||||
|
||||
describe('SQLite build benchmark', function () {
|
||||
let parsedCsv
|
||||
let sqlModule
|
||||
let selectDb
|
||||
|
||||
before(async function () {
|
||||
parsedCsv = await parseCsv('http://localhost:9876/base/sample.csv')
|
||||
sqlModule = await initSqlJs()
|
||||
|
||||
selectDb = new sqlModule.Database()
|
||||
importToTable(selectDb, parsedCsv)
|
||||
})
|
||||
|
||||
function benchmarkImport() {
|
||||
const db = new sqlModule.Database()
|
||||
try {
|
||||
importToTable(db, parsedCsv)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
function benchmarkSelect() {
|
||||
const result = selectDb.exec(`
|
||||
SELECT county, AVG(avg_depth) avg_depth_c
|
||||
FROM (
|
||||
SELECT s.county, s.town, COUNT(*) cnt, AVG(s.DrilledDepth) avg_depth
|
||||
FROM csv_import s
|
||||
JOIN csv_import USING(hole)
|
||||
WHERE s.town IS NOT NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 4 DESC
|
||||
)
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC
|
||||
`)
|
||||
console.assert(result.values.length == 56, 'Unexpected size of result set')
|
||||
}
|
||||
|
||||
it('run', async function () {
|
||||
const suite = createSuite()
|
||||
suite.add('import', { initCount: 3, minSamples: 50, fn: benchmarkImport })
|
||||
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
|
||||
await run(suite)
|
||||
})
|
||||
})
|
||||
|
||||
function importToTable(db, parsedCsv, chunkSize = 1024) {
|
||||
const columnListString = parsedCsv.meta.fields.join(', ')
|
||||
db.exec(`CREATE TABLE csv_import(${columnListString})`)
|
||||
|
||||
const params = parsedCsv.meta.fields.map(name => '?').join(', ')
|
||||
const insertStmt = db.prepare(`INSERT INTO csv_import VALUES(${params})`)
|
||||
chunkArray(parsedCsv.data, chunkSize).map(function (chunk) {
|
||||
db.exec('BEGIN')
|
||||
chunk.map(row => insertStmt.run(Object.values(row)))
|
||||
db.exec('COMMIT')
|
||||
})
|
||||
}
|
||||
|
||||
class PromiseWrapper {
|
||||
constructor() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.reject = reject
|
||||
this.resolve = resolve
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseCsv(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Papa.parse(url, {
|
||||
header: true,
|
||||
download: true,
|
||||
skipEmptyLines: 'greedy',
|
||||
complete: results => resolve(results),
|
||||
error: (error, file) => reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function chunkArray(arr, size) {
|
||||
return arr.reduce(function (result, value, index) {
|
||||
const chunkIndex = Math.floor(index / size)
|
||||
|
||||
if (!(chunkIndex in result)) {
|
||||
result[chunkIndex] = []
|
||||
}
|
||||
result[chunkIndex].push(value)
|
||||
|
||||
return result
|
||||
}, [])
|
||||
}
|
||||
|
||||
function createSuite() {
|
||||
// Combined workaround from:
|
||||
// - https://github.com/bestiejs/benchmark.js/issues/106
|
||||
// - https://github.com/bestiejs/benchmark.js/issues/237
|
||||
|
||||
// Benchmark could not pick up lodash otherwise
|
||||
const bm = benchmark.runInContext({ _: lodash })
|
||||
// Avoid `ReferenceError: Benchmark is not defined` error because Benchmark is assumed
|
||||
// to be in window
|
||||
window.Benchmark = bm
|
||||
|
||||
return new bm.Suite()
|
||||
}
|
||||
|
||||
function run(suite) {
|
||||
const suiteResult = new PromiseWrapper()
|
||||
suite
|
||||
.on('cycle', function (event) {
|
||||
console.info(String(event.target))
|
||||
})
|
||||
.on('complete', function () {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
browser: useragent(navigator.userAgent).browser,
|
||||
result: this.filter('successful')
|
||||
})
|
||||
)
|
||||
suiteResult.resolve()
|
||||
})
|
||||
.on('error', function (event) {
|
||||
console.error('Benchmark failed', String(event.target))
|
||||
suiteResult.reject()
|
||||
})
|
||||
.run({ async: true })
|
||||
|
||||
return suiteResult.promise
|
||||
}
|
||||
99
lib/sql-js/build.py
Normal file
@@ -0,0 +1,99 @@
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# See the setting descriptions on these pages:
|
||||
# - https://emscripten.org/docs/optimizing/Optimizing-Code.html
|
||||
# - https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
|
||||
cflags = (
|
||||
# SQLite configuration
|
||||
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
||||
'-DSQLITE_DISABLE_LFS',
|
||||
'-DSQLITE_DQS=0',
|
||||
'-DSQLITE_ENABLE_FTS3',
|
||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||
'-DSQLITE_ENABLE_FTS5',
|
||||
'-DSQLITE_ENABLE_NORMALIZE',
|
||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||
'-DSQLITE_OMIT_DEPRECATED',
|
||||
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||
'-DSQLITE_OMIT_SHARED_CACHE',
|
||||
'-DSQLITE_THREADSAFE=0',
|
||||
# Compile-time optimisation
|
||||
'-Os', # reduces the code size about in half comparing to -O2
|
||||
'-flto',
|
||||
'-Isrc', '-Isrc/lua',
|
||||
)
|
||||
emflags = (
|
||||
# Base
|
||||
'--memory-init-file', '0',
|
||||
'-s', 'ALLOW_TABLE_GROWTH=1',
|
||||
# WASM
|
||||
'-s', 'WASM=1',
|
||||
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
||||
'-s', 'ENVIRONMENT=web,worker',
|
||||
# Link-time optimisation
|
||||
'-Os',
|
||||
'-flto',
|
||||
# sql.js
|
||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||
'-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
|
||||
'--pre-js', 'src/sqljs/api.js',
|
||||
)
|
||||
|
||||
|
||||
def build(src: Path, dst: Path):
|
||||
out = Path('out')
|
||||
out.mkdir()
|
||||
|
||||
logging.info('Building LLVM bitcode for sqlite3.c')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'sqlite3.c',
|
||||
'-o', out / 'sqlite3.o',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for extension-functions.c')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'extension-functions.c',
|
||||
'-o', out / 'extension-functions.o',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for SQLite Lua extension')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-shared',
|
||||
*(src / 'lua').glob('*.c'),
|
||||
*(src / 'sqlitelua').glob('*.c'),
|
||||
'-o', out / 'sqlitelua.o',
|
||||
])
|
||||
|
||||
logging.info('Building WASM from bitcode')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*emflags,
|
||||
out / 'sqlite3.o',
|
||||
out / 'extension-functions.o',
|
||||
out / 'sqlitelua.o',
|
||||
'-o', out / 'sql-wasm.js',
|
||||
])
|
||||
|
||||
logging.info('Post-processing build and copying to dist')
|
||||
(out / 'sql-wasm.wasm').rename(dst / 'sql-wasm.wasm')
|
||||
with (dst / 'sql-wasm.js').open('w') as f:
|
||||
f.write((src / 'sqljs' / 'shell-pre.js').read_text())
|
||||
f.write((out / 'sql-wasm.js').read_text())
|
||||
f.write((src / 'sqljs' / 'shell-post.js').read_text())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||
|
||||
src = Path('src')
|
||||
dst = Path('dist')
|
||||
dst.mkdir()
|
||||
build(src, dst)
|
||||
150
lib/sql-js/configure.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
|
||||
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
|
||||
|
||||
# Extension-functions
|
||||
# ===================
|
||||
# It breaks amalgamation if appended as other extension because it redefines
|
||||
# several functions, so build it separately. Note that sql.js registers these
|
||||
# extension functions by calling ``registerExtensionFunctions`` itself.
|
||||
contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions.c?get=25'
|
||||
|
||||
extension_urls = (
|
||||
# Miscellaneous extensions
|
||||
# ========================
|
||||
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
|
||||
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
|
||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
|
||||
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
|
||||
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
|
||||
# Third-party extension
|
||||
# =====================
|
||||
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||
# Third-party extension with own dependencies
|
||||
# ===========================================
|
||||
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
|
||||
)
|
||||
|
||||
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
|
||||
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
|
||||
|
||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||
|
||||
|
||||
def _generate_extra_init_c_function(init_function_names):
|
||||
auto_ext_calls = '\n'.join([
|
||||
'nErr += sqlite3_auto_extension((void*){});'.format(init_fn)
|
||||
for init_fn in init_function_names
|
||||
])
|
||||
return '''
|
||||
int extra_init(const char* dummy)
|
||||
{
|
||||
int nErr = 0;
|
||||
%s
|
||||
return nErr ? SQLITE_ERROR : SQLITE_OK;
|
||||
}
|
||||
''' % auto_ext_calls
|
||||
|
||||
|
||||
def _get_amalgamation(tgt: Path):
|
||||
logging.info('Downloading and extracting SQLite amalgamation %s', amalgamation_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(amalgamation_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
for zpath in archive_root_dir.iterdir():
|
||||
with zpath.open() as fr, (tgt / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_lua(tgt: Path):
|
||||
# Library definitions from lua/Makefile
|
||||
lib_str = '''
|
||||
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||
ltm.o lundump.o lvm.o lzio.o
|
||||
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
|
||||
LUA_O= lua.o
|
||||
'''
|
||||
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
|
||||
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
|
||||
|
||||
logging.info('Downloading and extracting Lua %s', lua_url)
|
||||
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
|
||||
(tgt / 'lua').mkdir()
|
||||
for tarinfo in archive:
|
||||
tarpath = Path(tarinfo.name)
|
||||
if tarpath.match('src/*') and tarpath.stem in lib_names:
|
||||
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
|
||||
|
||||
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
(tgt / 'sqlitelua').mkdir()
|
||||
for zpath in (archive_root_dir / 'src').iterdir():
|
||||
if zpath.name != 'main.c':
|
||||
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_contrib_functions(tgt: Path):
|
||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||
|
||||
|
||||
def _get_extensions(tgt: Path):
|
||||
init_functions = []
|
||||
sqlite3_c = tgt / 'sqlite3.c'
|
||||
with sqlite3_c.open('ab') as f:
|
||||
for url, init_fn in extension_urls:
|
||||
logging.info('Downloading and appending to amalgamation %s', url)
|
||||
with request.urlopen(url) as resp:
|
||||
f.write(b'\n')
|
||||
shutil.copyfileobj(resp, f)
|
||||
init_functions.append(init_fn)
|
||||
|
||||
logging.info('Appending SQLITE_EXTRA_INIT to amalgamation')
|
||||
f.write(_generate_extra_init_c_function(init_functions).encode())
|
||||
|
||||
|
||||
def _get_sqljs(tgt: Path):
|
||||
logging.info('Downloading and extracting sql.js %s', sqljs_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqljs_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
(tgt / 'sqljs').mkdir()
|
||||
for zpath in (archive_root_dir / 'src').iterdir():
|
||||
with zpath.open() as fr, (tgt / 'sqljs' / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def configure(tgt: Path):
|
||||
_get_amalgamation(tgt)
|
||||
_get_contrib_functions(tgt)
|
||||
_get_lua(tgt)
|
||||
_get_extensions(tgt)
|
||||
_get_sqljs(tgt)
|
||||
|
||||
subprocess.check_call(['emcc', '--version'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if sys.version_info < (3, 8):
|
||||
print('Python 3.8 or higher is expected', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
|
||||
|
||||
src = Path('src')
|
||||
src.mkdir()
|
||||
configure(src)
|
||||
94
lib/sql-js/dist/sql-wasm.js
vendored
Normal file
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Executable file
9
lib/sql-js/make.sh
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
docker build -t sqliteviz/sqljs .
|
||||
|
||||
rm -r dist || true
|
||||
|
||||
CONTAINER=$(docker create sqliteviz/sqljs)
|
||||
docker cp $CONTAINER:/tmp/build/dist .
|
||||
docker rm $CONTAINER
|
||||
5
lib/sql-js/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sql.js",
|
||||
"main": "./dist/sql-wasm.js",
|
||||
"private": true
|
||||
}
|
||||
46556
package-lock.json
generated
95
package.json
@@ -1,52 +1,79 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.8.0",
|
||||
"version": "0.26.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"lint": "vue-cli-service lint"
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview",
|
||||
"test": "karma start karma.conf.cjs",
|
||||
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||
"format": "prettier . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"buffer": "^6.0.3",
|
||||
"codemirror": "^5.65.18",
|
||||
"codemirror-editor-vue3": "^2.8.0",
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"dataurl-to-blob": "^0.0.1",
|
||||
"html2canvas": "^1.1.4",
|
||||
"jquery": "^3.6.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"plotly.js": "^1.57.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"sql.js": "^1.3.0",
|
||||
"sqlite-parser": "^1.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-js-modal": "^2.0.0-rc.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vuejs-paginate": "^2.1.0",
|
||||
"vuera": "^0.2.7",
|
||||
"vuex": "^3.4.0"
|
||||
"papaparse": "^5.4.1",
|
||||
"pivottable": "^2.23.0",
|
||||
"plotly.js": "^2.35.2",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.14.0",
|
||||
"react-chart-editor": "^0.46.1",
|
||||
"react-dom": "^16.14.0",
|
||||
"sql.js": "file:./lib/sql-js",
|
||||
"tiny-emitter": "^2.1.0",
|
||||
"veaury": "^2.5.1",
|
||||
"vue": "^3.5.11",
|
||||
"vue-final-modal": "^4.5.5",
|
||||
"vue-multiselect": "^3.0.0-beta.3",
|
||||
"vue-router": "^4.4.5",
|
||||
"vuejs-paginate-next": "^1.0.2",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vue/cli-plugin-babel": "^4.4.0",
|
||||
"@vue/cli-plugin-eslint": "^4.4.0",
|
||||
"@vue/cli-plugin-router": "^4.4.0",
|
||||
"@vue/cli-plugin-vuex": "^4.4.0",
|
||||
"@vue/cli-service": "^4.4.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"@babel/core": "^7.25.7",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vue/eslint-config-standard": "^8.0.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"chai": "^4.1.2",
|
||||
"eslint": "^6.7.2",
|
||||
"chai-as-promised": "^8.0.1",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"karma": "^3.1.4",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"eslint-plugin-vue": "^9.28.0",
|
||||
"flush-promises": "^1.0.2",
|
||||
"karma": "^6.4.4",
|
||||
"karma-coverage": "^2.2.1",
|
||||
"karma-coverage-istanbul-reporter": "^3.0.3",
|
||||
"karma-firefox-launcher": "^2.1.3",
|
||||
"karma-mocha": "^1.3.0",
|
||||
"karma-spec-reporter": "^0.0.36",
|
||||
"karma-vite": "^1.0.5",
|
||||
"mocha": "^5.2.0",
|
||||
"prettier": "3.5.3",
|
||||
"process": "^0.11.10",
|
||||
"url-loader": "^4.1.1",
|
||||
"vite": "^5.4.14",
|
||||
"vite-plugin-istanbul": "^5.0.0",
|
||||
"vite-plugin-node-polyfills": "^0.23.0",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5"
|
||||
},
|
||||
"overrides": {
|
||||
"karma-vite": {
|
||||
"vite-plugin-istanbul": "$vite-plugin-istanbul"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/Logo192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/Logo512x512.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -1,17 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
1
public/inquiries.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
30
public/manifest.webmanifest
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "sqliteviz",
|
||||
"short_name": "sqliteviz",
|
||||
"start_url": "index.html"
|
||||
}
|
||||
58
src/App.vue
@@ -1,58 +1,85 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<router-view/>
|
||||
<router-view />
|
||||
<modals-container />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import { ModalsContainer } from 'vue-final-modal'
|
||||
|
||||
export default {
|
||||
components: { ModalsContainer },
|
||||
computed: {
|
||||
inquiries() {
|
||||
return this.$store.state.inquiries
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inquiries: {
|
||||
deep: true,
|
||||
handler() {
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-Regular.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-Regular.woff2');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-SemiBold.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-Bold.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-Bold.woff2');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-Italic.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-Italic.woff2');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-SemiBoldItalic.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
src: url("~@/assets/fonts/OpenSans-BoldItalic.woff2");
|
||||
font-family: 'Open Sans';
|
||||
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#app,
|
||||
.dialog,
|
||||
input,
|
||||
label,
|
||||
button,
|
||||
.plotly_editor * {
|
||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
||||
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -60,4 +87,7 @@ button,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
.CodeMirror-hints {
|
||||
z-index: 999 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
src/assets/images/Screenshot_editor.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
3
src/assets/images/arrow-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.51581 7.54801C4.95181 7.10201 5.55881 7.06701 6.09181 7.54801L9.99981 11.295L13.9078 7.54801C14.4408 7.06701 15.0488 7.10201 15.4818 7.54801C15.9178 7.99301 15.8898 8.74501 15.4818 9.16301C15.0758 9.58101 10.7868 13.665 10.7868 13.665C10.5698 13.888 10.2848 14 9.99981 14C9.71481 14 9.42981 13.888 9.21081 13.665C9.21081 13.665 4.92381 9.58101 4.51581 9.16301C4.10781 8.74501 4.07981 7.99301 4.51581 7.54801V7.54801Z" fill="#119DFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
3
src/assets/images/arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.51581 7.54801C4.95181 7.10201 5.55881 7.06701 6.09181 7.54801L9.99981 11.295L13.9078 7.54801C14.4408 7.06701 15.0488 7.10201 15.4818 7.54801C15.9178 7.99301 15.8898 8.74501 15.4818 9.16301C15.0758 9.58101 10.7868 13.665 10.7868 13.665C10.5698 13.888 10.2848 14 9.99981 14C9.71481 14 9.42981 13.888 9.21081 13.665C9.21081 13.665 4.92381 9.58101 4.51581 9.16301C4.10781 8.74501 4.07981 7.99301 4.51581 7.54801V7.54801Z" fill="#C8D4E3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
17
src/assets/images/checkbox_checked_disabled.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#DFE8F3" stroke="#C8D4E3"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.25L6.75 14.25L2.625 10.125L3.6825 9.0675L6.75 12.1275L14.6925 4.1925L15.75 5.25Z" fill="#A2B1C6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.1925" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
3
src/assets/images/delete-tag-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0436 10.3943C9.7153 10.7226 9.1833 10.7226 8.8557 10.3943L7 8.27329L5.1443 10.3936C4.816 10.7219 4.284 10.7219 3.9564 10.3936C3.6281 10.0653 3.6281 9.53329 3.9564 9.20569L5.887 7.00069L3.9557 4.79429C3.6274 4.46599 3.6274 3.93469 3.9557 3.60639C4.284 3.27809 4.8153 3.27809 5.1436 3.60639L7 5.72809L8.8557 3.60639C9.184 3.27809 9.7153 3.27809 10.0436 3.60639C10.3719 3.93469 10.3719 4.46669 10.0436 4.79429L8.113 7.00069L10.0436 9.20569C10.3719 9.53399 10.3719 10.066 10.0436 10.3943V10.3943Z" fill="#DE350B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 627 B |
3
src/assets/images/delete-tag.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0436 10.3945C9.7153 10.7228 9.1833 10.7228 8.8557 10.3945L7 8.27348L5.1443 10.3938C4.816 10.7221 4.284 10.7221 3.9564 10.3938C3.6281 10.0655 3.6281 9.53348 3.9564 9.20588L5.887 7.00088L3.9557 4.79448C3.6274 4.46618 3.6274 3.93488 3.9557 3.60658C4.284 3.27828 4.8153 3.27828 5.1436 3.60658L7 5.72828L8.8557 3.60658C9.184 3.27828 9.7153 3.27828 10.0436 3.60658C10.3719 3.93488 10.3719 4.46688 10.0436 4.79448L8.113 7.00088L10.0436 9.20588C10.3719 9.53418 10.3719 10.0662 10.0436 10.3945V10.3945Z" fill="#506784"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 628 B |
5
src/assets/images/error.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99984 16.6667C6.32484 16.6667 3.33317 13.675 3.33317 10C3.33317 6.32502 6.32484 3.33335 9.99984 3.33335C13.6748 3.33335 16.6665 6.32502 16.6665 10C16.6665 13.675 13.6748 16.6667 9.99984 16.6667ZM9.99984 1.66669C8.90549 1.66669 7.82186 1.88224 6.81081 2.30102C5.79976 2.71981 4.8811 3.33364 4.10728 4.10746C2.54448 5.67027 1.6665 7.78988 1.6665 10C1.6665 12.2102 2.54448 14.3298 4.10728 15.8926C4.8811 16.6664 5.79976 17.2802 6.81081 17.699C7.82186 18.1178 8.90549 18.3334 9.99984 18.3334C12.21 18.3334 14.3296 17.4554 15.8924 15.8926C17.4552 14.3298 18.3332 12.2102 18.3332 10C18.3332 8.90567 18.1176 7.82204 17.6988 6.81099C17.28 5.79995 16.6662 4.88129 15.8924 4.10746C15.1186 3.33364 14.1999 2.71981 13.1889 2.30102C12.1778 1.88224 11.0942 1.66669 9.99984 1.66669Z" fill="#EF553B"/>
|
||||
<rect x="6.36768" y="7.54855" width="1.67" height="8.60363" transform="rotate(-45 6.36768 7.54855)" fill="#EF553B"/>
|
||||
<rect x="12.4517" y="6.36771" width="1.67" height="8.60363" transform="rotate(45 12.4517 6.36771)" fill="#EF553B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
3
src/assets/images/logo_simple.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 862 B |
11
src/assets/images/sort.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M2.99932 -3.63032e-05C2.75092 -3.63032e-05 2.54932 0.201563 2.54932 0.449964L2.54932 11.55C2.54932 11.7984 2.75092 12 2.99932 12C3.24772 12 3.44932 11.7984 3.44932 11.55L3.44932 0.449964C3.44932 0.201563 3.24772 -3.63032e-05 2.99932 -3.63032e-05Z" fill="#506784"/>
|
||||
<path d="M2.99915 1.80492e-05C2.8839 1.80492e-05 2.76865 0.0438534 2.68109 0.132073L0.581055 2.232C0.405273 2.40789 0.405273 2.69287 0.581055 2.86865C0.756946 3.04443 1.04193 3.04443 1.21771 2.86865L2.99969 1.08667L4.78168 2.86865C4.95746 3.04443 5.24255 3.04443 5.41833 2.86865C5.59412 2.69287 5.59412 2.40789 5.41833 2.232L3.3183 0.132073C3.22953 0.0438534 3.11428 1.80492e-05 2.99915 1.80492e-05V1.80492e-05Z" fill="#506784"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="6" height="12" fill="white" transform="matrix(1 0 0 -1 0 12)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
5
src/assets/images/success.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4998 16.6667C6.82484 16.6667 3.83317 13.675 3.83317 10C3.83317 6.32502 6.82484 3.33335 10.4998 3.33335C14.1748 3.33335 17.1665 6.32502 17.1665 10C17.1665 13.675 14.1748 16.6667 10.4998 16.6667ZM10.4998 1.66669C9.40549 1.66669 8.32186 1.88224 7.31081 2.30102C6.29976 2.71981 5.3811 3.33364 4.60728 4.10746C3.04448 5.67027 2.1665 7.78988 2.1665 10C2.1665 12.2102 3.04448 14.3298 4.60728 15.8926C5.3811 16.6664 6.29976 17.2802 7.31081 17.699C8.32186 18.1178 9.40549 18.3334 10.4998 18.3334C12.71 18.3334 14.8296 17.4554 16.3924 15.8926C17.9552 14.3298 18.8332 12.2102 18.8332 10C18.8332 8.90567 18.6176 7.82204 18.1988 6.81099C17.78 5.79995 17.1662 4.88129 16.3924 4.10746C15.6186 3.33364 14.6999 2.71981 13.6889 2.30102C12.6778 1.88224 11.5942 1.66669 10.4998 1.66669Z" fill="#00CC96"/>
|
||||
<rect x="6.09717" y="10.2771" width="1.67" height="4.345" transform="rotate(-45 6.09717 10.2771)" fill="#00CC96"/>
|
||||
<rect x="13.752" y="6.97589" width="1.67" height="7.74359" transform="rotate(45 13.752 6.97589)" fill="#00CC96"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -27,14 +27,6 @@ button.primary:hover {
|
||||
text-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
background: var(--color-bg-light-2);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-light-2);
|
||||
text-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -59,5 +51,11 @@ button.toolbar:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
|
||||
|
||||
button.primary:disabled,
|
||||
button.secondary:disabled {
|
||||
background: var(--color-bg-light-2);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-light-2);
|
||||
text-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
.dialog {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.dialog .vfm__content {
|
||||
border-radius: var(--border-radius-big);
|
||||
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dialog-header {
|
||||
@@ -16,7 +23,7 @@
|
||||
}
|
||||
|
||||
.dialog-body {
|
||||
min-height: 60px;
|
||||
min-height: 56px;
|
||||
background-color: var(--color-bg-light);
|
||||
padding: 24px;
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
@@ -35,6 +42,6 @@
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.vm--overlay {
|
||||
.vfm__overlay.vfm--overlay {
|
||||
background-color: rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
136
src/assets/styles/multiselect.css
Normal file
@@ -0,0 +1,136 @@
|
||||
.sqliteviz-select,
|
||||
.sqliteviz-select .multiselect__tags {
|
||||
min-height: 36px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select {
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 6px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tags {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 4px 32px 0 6px;
|
||||
}
|
||||
|
||||
.sqliteviz-select,
|
||||
.sqliteviz-select .multiselect__input,
|
||||
.sqliteviz-select .multiselect__single,
|
||||
.sqliteviz-select .multiselect__placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__single,
|
||||
.sqliteviz-select .multiselect__placeholder,
|
||||
.sqliteviz-select .multiselect__input {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__input {
|
||||
width: 0 !important;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__input {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__placeholder,
|
||||
.sqliteviz-select .multiselect__input::placeholder {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option.multiselect__option--highlight {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag {
|
||||
background-color: var(--color-bg-light-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-text-active);
|
||||
font-size: 11.05px;
|
||||
margin: 2px;
|
||||
}
|
||||
.sqliteviz-select .multiselect__tag-icon:after {
|
||||
content: url('@/assets/images/delete-tag.svg');
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag-icon:focus:after,
|
||||
.sqliteviz-select .multiselect__tag-icon:hover:after {
|
||||
content: url('@/assets/images/delete-tag-hover.svg');
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag-icon:focus,
|
||||
.sqliteviz-select .multiselect__tag-icon:hover {
|
||||
background-color: var(--color-bg-danger);
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option {
|
||||
min-height: 29px;
|
||||
padding: 8px 12px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option:after {
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__content-wrapper {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
top: calc(100% - 1px);
|
||||
max-height: 292px !important;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--above .multiselect__content-wrapper {
|
||||
top: unset;
|
||||
bottom: calc(100% - 1px);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select:before {
|
||||
content: url('@/assets/images/arrow.svg');
|
||||
border: none;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__select {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sqliteviz-select:hover .multiselect__tags {
|
||||
border-color: var(--color-border-dark);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select:hover:before {
|
||||
content: url('@/assets/images/arrow-hover.svg');
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__tags {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option .no-results {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--disabled {
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--disabled .multiselect__select {
|
||||
background: unset;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
.rounded-bg {
|
||||
padding: 40px 5px 5px;
|
||||
padding: 35px 5px 5px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.straight .rounded-bg {
|
||||
border-radius: 0;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
@@ -18,7 +24,20 @@
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
@supports (-moz-appearance:none) {
|
||||
.straight .header-container {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.straight {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.straight .rounded-bg {
|
||||
/* 27 - height of table footer */
|
||||
height: calc(100% - 27px);
|
||||
}
|
||||
|
||||
@supports (-moz-appearance: none) {
|
||||
.header-container {
|
||||
top: 0;
|
||||
padding-left: 6px;
|
||||
@@ -32,41 +51,47 @@
|
||||
}
|
||||
.table-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
table {
|
||||
table.sqliteviz-table {
|
||||
min-width: 100%;
|
||||
margin-top: -40px;
|
||||
margin-top: -35px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th, .fixed-header {
|
||||
.sqliteviz-table thead th,
|
||||
.fixed-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-bg-dark);
|
||||
color: var(--color-text-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
tbody td {
|
||||
.sqliteviz-table tbody td {
|
||||
font-size: 13px;
|
||||
background-color:white;
|
||||
background-color: white;
|
||||
color: var(--color-text-base);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
td, th, .fixed-header {
|
||||
padding: 12px 24px;
|
||||
.sqliteviz-table td,
|
||||
.sqliteviz-table th,
|
||||
.fixed-header {
|
||||
padding: 8px 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr td:last-child,
|
||||
thead tr th:last-child,
|
||||
.sqliteviz-table tbody tr td:last-child,
|
||||
.sqliteviz-table thead tr th:last-child,
|
||||
.header-container div .fixed-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
td > div.cell-data {
|
||||
.sqliteviz-table td > div.cell-data {
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
@@ -83,3 +108,9 @@ td > div.cell-data {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-table tbody td[data-isNull='true'],
|
||||
.sqliteviz-table tbody td[data-isBlob='true'] {
|
||||
color: var(--color-text-light-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
.icon-tooltip {
|
||||
visibility: hidden;
|
||||
background-color: rgba(80, 103, 132, 0.85);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
line-height: 19px;;
|
||||
line-height: 19px;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
height: 19px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -1,29 +1,33 @@
|
||||
:root {
|
||||
--color-white: #ffffff;
|
||||
--color-gray-light: #F3F6FA;
|
||||
--color-gray-light-2: #DFE8F3;
|
||||
--color-gray-light-3: #C8D4E3;
|
||||
--color-gray-light-4:#EBF0F8;
|
||||
--color-gray-light-5:#f8f8f9;
|
||||
--color-gray-medium: #A2B1C6;
|
||||
--color-gray-light: #f3f6fa;
|
||||
--color-gray-light-2: #dfe8f3;
|
||||
--color-gray-light-3: #c8d4e3;
|
||||
--color-gray-light-4: #ebf0f8;
|
||||
--color-gray-light-5: #f8f8f9;
|
||||
--color-gray-medium: #a2b1c6;
|
||||
--color-gray-dark: #506784;
|
||||
--color-blue-medium: #119DFF;
|
||||
--color-blue-dark: #0D76BF;
|
||||
--color-blue-dark-2: #2A3F5F;
|
||||
--color-red: #EF553B;
|
||||
--color-yellow: #FBEFCB;
|
||||
|
||||
|
||||
--color-blue-medium: #119dff;
|
||||
--color-blue-dark: #0d76bf;
|
||||
--color-blue-dark-2: #2a3f5f;
|
||||
--color-red: #ef553b;
|
||||
--color-red-2: #de350b;
|
||||
--color-red-light: #ffbdad;
|
||||
--color-yellow: #fbefcb;
|
||||
|
||||
--color-bg-light: var(--color-gray-light);
|
||||
--color-bg-light-2: var(--color-gray-light-2);
|
||||
--color-bg-light-3: var(--color-gray-light-5);
|
||||
--color-bg-light-4: var(--color-gray-light-4);
|
||||
--color-bg-dark: var(--color-gray-dark);
|
||||
--color-bg-warning: var(--color-yellow);
|
||||
--color-bg-danger: var(--color-red-light);
|
||||
--color-danger: var(--color-red-2);
|
||||
--color-accent: var(--color-blue-medium);
|
||||
--color-accent-shade: var(--color-blue-dark);
|
||||
--color-border-light: var(--color-gray-light-2);
|
||||
--color-border: var(--color-gray-light-3);
|
||||
--color-border-dark: var(--color-gray-medium);
|
||||
--color-text-light: var(--color-white);
|
||||
--color-text-light-2: var(--color-gray-medium);
|
||||
--color-text-base: var(--color-gray-dark);
|
||||
@@ -42,6 +46,3 @@
|
||||
.plotly-editor--theme-provider {
|
||||
--sidebar-width: 112px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
<template>
|
||||
<div v-show="visible" class="chart-container">
|
||||
<div class="warning chart-warning" v-show="!sqlResult && visible">
|
||||
There is no data to build a chart. Run your sql query and make sure the result is not empty.
|
||||
</div>
|
||||
<PlotlyEditor
|
||||
:data="state.data"
|
||||
:layout="state.layout"
|
||||
:frames="state.frames"
|
||||
:config="{ editable: true, displaylogo: false }"
|
||||
:dataSources="dataSources"
|
||||
:dataSourceOptions="dataSourceOptions"
|
||||
:plotly="plotly"
|
||||
@onUpdate="update"
|
||||
:useResizeHandler="true"
|
||||
:debug="true"
|
||||
:advancedTraceTypeSelector="true"
|
||||
class="chart"
|
||||
:style="{ height: !sqlResult ? 'calc(100% - 40px)' : '100%' }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import plotly from 'plotly.js/dist/plotly'
|
||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||
|
||||
import PlotlyEditor from 'react-chart-editor'
|
||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||
|
||||
export default {
|
||||
name: 'Chart',
|
||||
props: ['sqlResult', 'initChart', 'visible'],
|
||||
components: {
|
||||
PlotlyEditor
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
plotly: plotly,
|
||||
state: this.initChart || {
|
||||
data: [],
|
||||
layout: {},
|
||||
frames: []
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
dataSources () {
|
||||
if (!this.sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const matrix = this.sqlResult.values
|
||||
const [row] = matrix
|
||||
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
||||
this.sqlResult.columns.forEach((column, index) => {
|
||||
dataSorces[column] = transposedMatrix[index]
|
||||
})
|
||||
return dataSorces
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return Object.keys(this.dataSources).map(name => ({
|
||||
value: name,
|
||||
label: name
|
||||
}))
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
dataSources () {
|
||||
// we need to update state.data in order to update the graph
|
||||
// https://github.com/plotly/react-chart-editor/issues/948
|
||||
dereference(this.state.data, this.dataSources)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
update (data, layout, frames) {
|
||||
this.state = { data, layout, frames }
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
// we don't need to save the data, only settings
|
||||
// so we modify state.data using dereference
|
||||
const stateCopy = JSON.parse(JSON.stringify(this.state))
|
||||
const emptySources = {}
|
||||
for (const key in this.dataSources) {
|
||||
emptySources[key] = []
|
||||
}
|
||||
dereference(stateCopy.data, emptySources)
|
||||
return stateCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chart-container {
|
||||
height: calc(100% - 89px);
|
||||
}
|
||||
|
||||
.chart-warning {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
|
||||
.chart {
|
||||
border-top: 1px solid var(--color-border);
|
||||
min-height: 242px;
|
||||
}
|
||||
|
||||
>>> .editor_controls .sidebar__item:before {
|
||||
width: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,26 +1,41 @@
|
||||
<template>
|
||||
<div class="checkbox-container" @click.stop="onClick">
|
||||
<div
|
||||
:class="[
|
||||
'checkbox-container',
|
||||
{ checked: checked },
|
||||
{ disabled: disabled }
|
||||
]"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<div v-show="!checked" class="unchecked" />
|
||||
<img
|
||||
v-show="checked && theme === 'accent'"
|
||||
:src="require('@/assets/images/checkbox_checked.svg')"
|
||||
v-show="checked && !disabled && theme === 'light'"
|
||||
class="checked-light"
|
||||
src="~@/assets/images/checkbox_checked_light.svg"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && theme === 'light'"
|
||||
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
||||
v-show="checked && !disabled && theme !== 'light'"
|
||||
class="checked"
|
||||
src="~@/assets/images/checkbox_checked.svg"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && disabled"
|
||||
class="checked-disabled"
|
||||
src="~@/assets/images/checkbox_checked_disabled.svg"
|
||||
/>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'checkBox',
|
||||
name: 'CheckBox',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'accent',
|
||||
validator: (value) => {
|
||||
validator: value => {
|
||||
return ['accent', 'light'].includes(value)
|
||||
}
|
||||
},
|
||||
@@ -28,17 +43,30 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
emits: ['click'],
|
||||
data() {
|
||||
return {
|
||||
checked: this.init
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
onClick() {
|
||||
if (!this.disabled) {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +74,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.checkbox-container {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unchecked {
|
||||
@@ -62,6 +90,26 @@ export default {
|
||||
}
|
||||
|
||||
img {
|
||||
display: block;
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
margin-left: 6px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.checked .label {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.disabled.checkbox-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled .label {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.disabled .unchecked,
|
||||
.disabled .unchecked:hover {
|
||||
background-color: var(--color-bg-light-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
130
src/components/CsvJsonImport/DelimiterSelector/ascii.js
Normal file
@@ -0,0 +1,130 @@
|
||||
export default {
|
||||
0: { name: 'null character' },
|
||||
1: { name: 'start of header' },
|
||||
2: { name: 'start of text' },
|
||||
3: { name: 'end of text' },
|
||||
4: { name: 'end of transmission' },
|
||||
5: { name: 'enquiry' },
|
||||
6: { name: 'acknowledge' },
|
||||
7: { name: 'bell (ring)' },
|
||||
8: { name: 'backspace' },
|
||||
9: { name: 'horizontal tab' },
|
||||
10: { name: 'line feed' },
|
||||
11: { name: 'vertical tab' },
|
||||
12: { name: 'form feed' },
|
||||
13: { name: 'carriage return' },
|
||||
14: { name: 'shift out' },
|
||||
15: { name: 'shift in' },
|
||||
16: { name: 'data link escape' },
|
||||
17: { name: 'device control 1' },
|
||||
18: { name: 'device control 2' },
|
||||
19: { name: 'device control 3' },
|
||||
20: { name: 'device control 4' },
|
||||
21: { name: 'negative acknowledge' },
|
||||
22: { name: 'synchronize' },
|
||||
23: { name: 'end transmission block' },
|
||||
24: { name: 'cancel' },
|
||||
25: { name: 'end of medium' },
|
||||
26: { name: 'substitute' },
|
||||
27: { name: 'escape' },
|
||||
28: { name: 'file separator' },
|
||||
29: { name: 'group separator' },
|
||||
30: { name: 'record separator' },
|
||||
31: { name: 'unit separator' },
|
||||
32: { name: 'space' },
|
||||
33: { name: 'exclamation mark' },
|
||||
34: { name: 'quotation mark' },
|
||||
35: { name: 'number sign' },
|
||||
36: { name: 'dollar sign' },
|
||||
37: { name: 'percent sign' },
|
||||
38: { name: 'ampersand' },
|
||||
39: { name: 'apostrophe' },
|
||||
40: { name: 'left parenthesis' },
|
||||
41: { name: 'right parenthesis' },
|
||||
42: { name: 'asterisk' },
|
||||
43: { name: 'plus sign' },
|
||||
44: { name: 'comma' },
|
||||
45: { name: 'hyphen' },
|
||||
46: { name: 'period' },
|
||||
47: { name: 'slash' },
|
||||
48: { name: 'digit 0' },
|
||||
49: { name: 'digit 1' },
|
||||
50: { name: 'digit 2' },
|
||||
51: { name: 'digit 3' },
|
||||
52: { name: 'digit 4' },
|
||||
53: { name: 'digit 5' },
|
||||
54: { name: 'digit 6' },
|
||||
55: { name: 'digit 7' },
|
||||
56: { name: 'digit 8' },
|
||||
57: { name: 'digit 9' },
|
||||
58: { name: 'colon' },
|
||||
59: { name: 'semicolon' },
|
||||
60: { name: 'less-than' },
|
||||
61: { name: 'equals-to' },
|
||||
62: { name: 'greater-than' },
|
||||
63: { name: 'question mark' },
|
||||
64: { name: 'at sign' },
|
||||
65: { name: 'uppercase A' },
|
||||
66: { name: 'uppercase B' },
|
||||
67: { name: 'uppercase C' },
|
||||
68: { name: 'uppercase D' },
|
||||
69: { name: 'uppercase E' },
|
||||
70: { name: 'uppercase F' },
|
||||
71: { name: 'uppercase G' },
|
||||
72: { name: 'uppercase H' },
|
||||
73: { name: 'uppercase I' },
|
||||
74: { name: 'uppercase J' },
|
||||
75: { name: 'uppercase K' },
|
||||
76: { name: 'uppercase L' },
|
||||
77: { name: 'uppercase M' },
|
||||
78: { name: 'uppercase N' },
|
||||
79: { name: 'uppercase O' },
|
||||
80: { name: 'uppercase P' },
|
||||
81: { name: 'uppercase Q' },
|
||||
82: { name: 'uppercase R' },
|
||||
83: { name: 'uppercase S' },
|
||||
84: { name: 'uppercase T' },
|
||||
85: { name: 'uppercase U' },
|
||||
86: { name: 'uppercase V' },
|
||||
87: { name: 'uppercase W' },
|
||||
88: { name: 'uppercase X' },
|
||||
89: { name: 'uppercase Y' },
|
||||
90: { name: 'uppercase Z' },
|
||||
91: { name: 'left square bracket' },
|
||||
92: { name: 'backslash' },
|
||||
93: { name: 'right square bracket' },
|
||||
94: { name: 'caret' },
|
||||
95: { name: 'underscore' },
|
||||
96: { name: 'grave accent' },
|
||||
97: { name: 'lowercase a' },
|
||||
98: { name: 'lowercase b' },
|
||||
99: { name: 'lowercase c' },
|
||||
100: { name: 'lowercase d' },
|
||||
101: { name: 'lowercase e' },
|
||||
102: { name: 'lowercase f' },
|
||||
103: { name: 'lowercase g' },
|
||||
104: { name: 'lowercase h' },
|
||||
105: { name: 'lowercase i' },
|
||||
106: { name: 'lowercase j' },
|
||||
107: { name: 'lowercase k' },
|
||||
108: { name: 'lowercase l' },
|
||||
109: { name: 'lowercase m' },
|
||||
110: { name: 'lowercase n' },
|
||||
111: { name: 'lowercase o' },
|
||||
112: { name: 'lowercase p' },
|
||||
113: { name: 'lowercase q' },
|
||||
114: { name: 'lowercase r' },
|
||||
115: { name: 'lowercase s' },
|
||||
116: { name: 'lowercase t' },
|
||||
117: { name: 'lowercase u' },
|
||||
118: { name: 'lowercase v' },
|
||||
119: { name: 'lowercase w' },
|
||||
120: { name: 'lowercase x' },
|
||||
121: { name: 'lowercase y' },
|
||||
122: { name: 'lowercase z' },
|
||||
123: { name: 'left curly brace' },
|
||||
124: { name: 'vertical bar' },
|
||||
125: { name: 'right curly brace' },
|
||||
126: { name: 'tilde' },
|
||||
127: { name: 'delete (rubout)' }
|
||||
}
|
||||
210
src/components/CsvJsonImport/DelimiterSelector/index.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<div :class="{ disabled: disabled }">
|
||||
<div class="text-field-label">Delimiter</div>
|
||||
<div
|
||||
class="delimiter-selector-container"
|
||||
:style="{ width: width }"
|
||||
@click="onContainerClick"
|
||||
>
|
||||
<div class="value">
|
||||
<input
|
||||
ref="delimiterInput"
|
||||
v-model="inputValue"
|
||||
:class="{ filled: filled }"
|
||||
type="text"
|
||||
maxlength="1"
|
||||
:disabled="disabled"
|
||||
@click.stop
|
||||
/>
|
||||
<div class="name">{{ getSymbolName(modelValue) }}</div>
|
||||
</div>
|
||||
<div class="controls" @click.stop>
|
||||
<clear-icon :disabled="disabled" @click="clear" />
|
||||
<drop-down-chevron
|
||||
:disabled="disabled"
|
||||
@click="!disabled && (showOptions = !showOptions)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="showOptions" class="options" :style="{ width: width }">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
class="option"
|
||||
@click="chooseOption(option)"
|
||||
>
|
||||
<pre>{{ option }}</pre>
|
||||
<div>{{ getSymbolName(option) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ascii from './ascii'
|
||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||
import ClearIcon from '@/components/svg/clear'
|
||||
|
||||
export default {
|
||||
name: 'DelimiterSelector',
|
||||
components: { DropDownChevron, ClearIcon },
|
||||
props: {
|
||||
modelValue: String,
|
||||
width: String,
|
||||
disabled: Boolean
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
showOptions: false,
|
||||
options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'],
|
||||
filled: false,
|
||||
inputValue: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inputValue() {
|
||||
if (this.inputValue) {
|
||||
this.filled = true
|
||||
if (this.inputValue !== this.modelValue) {
|
||||
this.$emit('update:modelValue', this.inputValue)
|
||||
}
|
||||
} else {
|
||||
this.filled = false
|
||||
}
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.inputValue = this.modelValue
|
||||
},
|
||||
methods: {
|
||||
getSymbolName(str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
return ascii[str.charCodeAt(0).toString()].name
|
||||
},
|
||||
chooseOption(option) {
|
||||
this.inputValue = option
|
||||
this.showOptions = false
|
||||
},
|
||||
onContainerClick() {
|
||||
this.$refs.delimiterInput.focus()
|
||||
},
|
||||
|
||||
clear() {
|
||||
if (!this.disabled) {
|
||||
this.inputValue = ''
|
||||
this.$refs.delimiterInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.delimiter-selector-container {
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value .name {
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.options {
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-width: 0 1px 1px 1px;
|
||||
color: var(--color-text-base);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option pre {
|
||||
background-color: var(--color-bg-warning);
|
||||
line-height: 20px;
|
||||
margin-right: 6px;
|
||||
tab-size: 1;
|
||||
font-family: monospace;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
background: var(--color-white);
|
||||
border: none;
|
||||
color: var(--color-text-base);
|
||||
height: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input.filled {
|
||||
background: var(--color-bg-warning);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.text-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-base);
|
||||
padding-left: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.disabled .text-field-label {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.disabled .delimiter-selector-container {
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
</style>
|
||||
518
src/components/CsvJsonImport/index.vue
Normal file
@@ -0,0 +1,518 @@
|
||||
<template>
|
||||
<modal
|
||||
:modalId="dialogName"
|
||||
class="dialog"
|
||||
contentClass="import-modal"
|
||||
scrollable
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
{{ typeName }} import
|
||||
<close-icon :disabled="disableDialog" @click="cancelImport" />
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<text-field
|
||||
id="csv-json-table-name"
|
||||
v-model="tableName"
|
||||
label="Table name"
|
||||
width="484px"
|
||||
:disabled="disableDialog"
|
||||
:errorMsg="tableNameError"
|
||||
/>
|
||||
<div v-if="!isJson && !isNdJson" class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
:disabled="disableDialog"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
id="quote-char"
|
||||
v-model="quoteChar"
|
||||
label="Quote char"
|
||||
hint="The character used to quote fields."
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
id="escape-char"
|
||||
v-model="escapeChar"
|
||||
label="Escape char"
|
||||
hint='
|
||||
The character used to escape the quote character within a field
|
||||
(e.g. "column with ""quotes"" in text").
|
||||
'
|
||||
maxHintWidth="242px"
|
||||
width="93px"
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
@input="preview"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
v-if="!isJson && !isNdJson"
|
||||
:init="header"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
@click="changeHeaderDisplaying"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData && previewData.rowCount > 0"
|
||||
:data-set="previewData"
|
||||
:preview="true"
|
||||
class="preview-table"
|
||||
/>
|
||||
<div v-else class="no-data">No data</div>
|
||||
<logs class="import-errors" :messages="importMessages" />
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
id="import-cancel"
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelImport"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCompleted"
|
||||
id="import-start"
|
||||
class="primary"
|
||||
:disabled="disableDialog || disableImport"
|
||||
@click="loadToDb(file)"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCompleted"
|
||||
id="import-finish"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import csv from '@/lib/csv'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import DelimiterSelector from './DelimiterSelector'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import Logs from '@/components/Logs'
|
||||
import time from '@/lib/utils/time'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
name: 'CsvJsonImport',
|
||||
components: {
|
||||
CloseIcon,
|
||||
TextField,
|
||||
DelimiterSelector,
|
||||
CheckBox,
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
props: {
|
||||
file: File,
|
||||
db: Object,
|
||||
dialogName: String
|
||||
},
|
||||
emits: ['cancel', 'finish'],
|
||||
data() {
|
||||
return {
|
||||
disableDialog: false,
|
||||
disableImport: false,
|
||||
tableName: '',
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
importCompleted: false,
|
||||
importMessages: [],
|
||||
previewData: null,
|
||||
addedTable: null,
|
||||
tableNameError: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isJson() {
|
||||
return fIo.isJSON(this.file)
|
||||
},
|
||||
isNdJson() {
|
||||
return fIo.isNDJSON(this.file)
|
||||
},
|
||||
typeName() {
|
||||
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
isJson() {
|
||||
if (this.isJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
isNdJson() {
|
||||
if (this.isNdJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
tableName: time.debounce(function () {
|
||||
this.tableNameError = ''
|
||||
if (!this.tableName) {
|
||||
return
|
||||
}
|
||||
this.db.validateTableName(this.tableName).catch(err => {
|
||||
this.tableNameError = err.message + '. Try another table name.'
|
||||
})
|
||||
}, 400)
|
||||
},
|
||||
methods: {
|
||||
changeHeaderDisplaying(e) {
|
||||
this.header = e
|
||||
this.preview()
|
||||
},
|
||||
cancelImport() {
|
||||
if (!this.disableDialog) {
|
||||
if (this.addedTable) {
|
||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||
this.db.refreshSchema()
|
||||
}
|
||||
this.$modal.hide(this.dialogName)
|
||||
this.$emit('cancel')
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.header = !this.isJson && !this.isNdJson
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
|
||||
this.tableName = ''
|
||||
this.disableDialog = false
|
||||
this.disableImport = false
|
||||
this.importCompleted = false
|
||||
this.importMessages = []
|
||||
this.previewData = null
|
||||
this.addedTable = null
|
||||
this.tableNameError = ''
|
||||
},
|
||||
open() {
|
||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||
this.$modal.show(this.dialogName)
|
||||
},
|
||||
async preview() {
|
||||
this.disableImport = false
|
||||
if (!this.file) {
|
||||
return
|
||||
}
|
||||
this.importCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(this.file)
|
||||
: await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.previewData.rowCount = parseResult.rowCount
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importMessages = parseResult.messages || []
|
||||
|
||||
if (this.previewData.rowCount === 0) {
|
||||
this.disableImport = true
|
||||
this.importMessages.push({
|
||||
type: 'info',
|
||||
message: 'No rows to import.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
this.importMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
this.importMessages = [
|
||||
{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
async getJsonParseResult(file) {
|
||||
const jsonContent = await fIo.getFileContent(file)
|
||||
const isEmpty = !jsonContent.trim()
|
||||
return {
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: { doc: !isEmpty ? [jsonContent] : [] }
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: [],
|
||||
rowCount: +!isEmpty
|
||||
}
|
||||
},
|
||||
async loadToDb(file) {
|
||||
if (!this.tableName) {
|
||||
this.tableNameError = "Table name can't be empty"
|
||||
return
|
||||
}
|
||||
|
||||
this.disableDialog = true
|
||||
const config = {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
let parsingMsg = {}
|
||||
this.importMessages.push({
|
||||
message: `Parsing ${this.typeName}...`,
|
||||
type: 'info'
|
||||
})
|
||||
// Get *reactive* link to parsing message for later updates
|
||||
parsingMsg = this.importMessages[this.importMessages.length - 1]
|
||||
const parsingLoadingIndicator = setTimeout(() => {
|
||||
parsingMsg.type = 'loading'
|
||||
}, 1000)
|
||||
|
||||
let importMsg = {}
|
||||
let importLoadingIndicator = null
|
||||
|
||||
const updateProgress = progress => {
|
||||
importMsg.progress = progress
|
||||
}
|
||||
const progressCounterId = this.db.createProgressCounter(updateProgress)
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(file)
|
||||
: await csv.parse(this.file, config)
|
||||
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.rowCount
|
||||
let period = time.getPeriod(start, end)
|
||||
parsingMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importMessages = this.importMessages.concat(
|
||||
parseResult.messages
|
||||
)
|
||||
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about parsing success
|
||||
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for parsing is not needed anymore
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importMessages.push({
|
||||
message: `Importing ${this.typeName} into a SQLite database...`,
|
||||
type: 'info'
|
||||
})
|
||||
importMsg = this.importMessages[this.importMessages.length - 1]
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
importMsg.type = 'loading'
|
||||
}, 1000)
|
||||
|
||||
// Add table
|
||||
start = new Date()
|
||||
await this.db.addTableFromCsv(
|
||||
this.tableName,
|
||||
parseResult.data,
|
||||
progressCounterId
|
||||
)
|
||||
end = new Date()
|
||||
|
||||
this.addedTable = this.tableName
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message =
|
||||
`Importing ${this.typeName} ` +
|
||||
`into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCompleted = true
|
||||
} else {
|
||||
parsingMsg.message = 'Parsing ended with errors.'
|
||||
parsingMsg.type = 'info'
|
||||
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
if (parsingMsg.type === 'loading') {
|
||||
parsingMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.db.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
async finish() {
|
||||
this.$modal.hide(this.dialogName)
|
||||
const stmt = this.getQueryExample()
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCompleted = false
|
||||
this.$emit('finish')
|
||||
events.send('inquiry.create', null, { auto: true })
|
||||
},
|
||||
getQueryExample() {
|
||||
return this.isNdJson
|
||||
? this.getNdJsonQueryExample()
|
||||
: this.isJson
|
||||
? this.getJsonQueryExample()
|
||||
: [
|
||||
'/*',
|
||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
`SELECT * FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
},
|
||||
getNdJsonQueryExample() {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
`SELECT doc->>'${firstKey}'`,
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
},
|
||||
getJsonQueryExample() {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your JSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`,
|
||||
`JOIN json_each(doc, '$.${firstKey}')`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.import-modal {
|
||||
width: 80%;
|
||||
max-width: 1152px;
|
||||
margin: auto;
|
||||
left: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.dialog-body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#csv-json-table-name {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
}
|
||||
.preview-table {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.import-errors {
|
||||
height: 136px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.no-data {
|
||||
margin-top: 32px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
height: 147px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-base);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,200 +0,0 @@
|
||||
<template>
|
||||
<div class="db-upload-container">
|
||||
<label for="assetsFieldHandle">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@dragleave.prevent="state=''"
|
||||
@drop.prevent="drop"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="assetsFieldHandle"
|
||||
@change="loadDb"
|
||||
ref="file"
|
||||
accept=".db,.sqlite,.sqlite3"
|
||||
/>
|
||||
<div class="text">
|
||||
Drop the database file here or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="illustrated" id="img-container">
|
||||
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
|
||||
<img
|
||||
id="left-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/leftArm.svg')"
|
||||
/>
|
||||
<img
|
||||
id="file-img"
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'drop'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
|
||||
<img id="body-img" :src="require('@/assets/images/body.svg')" />
|
||||
<img
|
||||
id="right-arm-img"
|
||||
:class="{'swing': state === 'dragover'}"
|
||||
:src="require('@/assets/images/rightArm.svg')"
|
||||
/>
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DbUpload',
|
||||
props: {
|
||||
illustrated: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve()
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.illustrated) {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadDb () {
|
||||
this.state = 'drop'
|
||||
return Promise.all([this.$db.loadDb(this.$refs.file.files[0]), this.animationPromise])
|
||||
.then(([schema]) => {
|
||||
this.$store.commit('saveSchema', schema)
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
})
|
||||
},
|
||||
drop (event) {
|
||||
this.$refs.file.files = event.dataTransfer.files
|
||||
this.loadDb()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius-big);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: var(--color-bg-light-3);
|
||||
border-radius: var(--border-radius-big);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: calc(50% - 120px);
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#drop-file-top-img {
|
||||
width: 450px;
|
||||
height: 175px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#drop-file-bottom-img {
|
||||
width: 450px;
|
||||
height: 167px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
#body-img {
|
||||
width: 74px;
|
||||
position: absolute;
|
||||
top: 94.05px;
|
||||
left: 46px;
|
||||
}
|
||||
#right-arm-img {
|
||||
width: 106px;
|
||||
position: absolute;
|
||||
top: 110.05px;
|
||||
left: 78px;
|
||||
}
|
||||
#left-arm-img {
|
||||
width: 114px;
|
||||
position: absolute;
|
||||
top: 69.05px;
|
||||
left: 69px;
|
||||
}
|
||||
#file-img {
|
||||
width: 125px;
|
||||
position: absolute;
|
||||
top: 15.66px;
|
||||
left: 152px;
|
||||
}
|
||||
|
||||
.swing {
|
||||
animation: swing ease-in-out 0.6s infinite alternate;
|
||||
}
|
||||
#left-arm-img.swing {
|
||||
transform-origin: 9px 83px;
|
||||
}
|
||||
#right-arm-img.swing {
|
||||
transform-origin: 0 56px;
|
||||
}
|
||||
#file-img.swing {
|
||||
transform-origin: -74px 139px;
|
||||
}
|
||||
@keyframes swing {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(-7deg); }
|
||||
}
|
||||
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
transition: top 1s ease-in-out, left 1s ease-in-out;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% { transform: rotate(360deg) scale(0.5); }
|
||||
}
|
||||
</style>
|
||||
276
src/components/DbUploader.vue
Normal file
@@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<div class="db-uploader-container" :style="{ width }">
|
||||
<change-db-icon v-if="type === 'small'" @click="browse" />
|
||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||
<div
|
||||
class="drop-area"
|
||||
@dragover.prevent="state = 'dragover'"
|
||||
@dragleave.prevent="state = ''"
|
||||
@drop.prevent="drop"
|
||||
@click="browse"
|
||||
>
|
||||
<div class="text">
|
||||
Drop the database, CSV, JSON or NDJSON file here or click to choose a
|
||||
file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="type === 'illustrated'" id="img-container">
|
||||
<img id="drop-file-top-img" src="~@/assets/images/top.svg" />
|
||||
<img
|
||||
id="left-arm-img"
|
||||
:class="{ swing: state === 'dragover' }"
|
||||
src="~@/assets/images/leftArm.svg"
|
||||
/>
|
||||
<img
|
||||
id="file-img"
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
swing: state === 'dragover',
|
||||
fly: state === 'dropping',
|
||||
hidden: state === 'dropped'
|
||||
}"
|
||||
src="~@/assets/images/file.png"
|
||||
/>
|
||||
<img id="drop-file-bottom-img" src="~@/assets/images/bottom.svg" />
|
||||
<img id="body-img" src="~@/assets/images/body.svg" />
|
||||
<img
|
||||
id="right-arm-img"
|
||||
:class="{ swing: state === 'dragover' }"
|
||||
src="~@/assets/images/rightArm.svg"
|
||||
/>
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv or json dialog -->
|
||||
<csv-json-import
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialogName="importFromCsvJson"
|
||||
@cancel="cancelImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import database from '@/lib/database'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
name: 'DbUploader',
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
CsvJsonImport
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'small',
|
||||
validator: value => {
|
||||
return ['illustrated', 'small'].includes(value)
|
||||
}
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'unset'
|
||||
}
|
||||
},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
state: '',
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.type === 'illustrated') {
|
||||
this.animationPromise = new Promise(resolve => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
this.state = 'dropped'
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelImport() {
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
}
|
||||
},
|
||||
|
||||
async finish() {
|
||||
this.$store.commit('setDb', this.newDb)
|
||||
if (this.$route.path !== '/workspace') {
|
||||
this.$router.push('/workspace')
|
||||
}
|
||||
},
|
||||
|
||||
loadDb(file) {
|
||||
return Promise.all([this.newDb.loadDb(file), this.animationPromise]).then(
|
||||
this.finish
|
||||
)
|
||||
},
|
||||
|
||||
async checkFile(file) {
|
||||
this.state = 'dropping'
|
||||
this.newDb = database.getNewDatabase()
|
||||
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||
events.send('database.import', file.size, {
|
||||
from: isJson ? 'json' : 'csv',
|
||||
new_db: true
|
||||
})
|
||||
|
||||
this.file = file
|
||||
await this.$nextTick()
|
||||
const csvJsonImportModal = this.$refs.addCsvJson
|
||||
csvJsonImportModal.reset()
|
||||
return Promise.all([
|
||||
csvJsonImportModal.preview(),
|
||||
this.animationPromise
|
||||
]).then(csvJsonImportModal.open)
|
||||
}
|
||||
},
|
||||
browse() {
|
||||
fIo
|
||||
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
drop(event) {
|
||||
this.checkFile(event.dataTransfer.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.db-uploader-container {
|
||||
position: relative;
|
||||
}
|
||||
.drop-area-container {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: 8px;
|
||||
border-radius: var(--border-radius-big);
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.drop-area {
|
||||
background-color: var(--color-bg-light-3);
|
||||
border-radius: var(--border-radius-big);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#drop-file-top-img {
|
||||
width: 450px;
|
||||
height: 175px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
#drop-file-bottom-img {
|
||||
width: 450px;
|
||||
height: 167px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
#body-img {
|
||||
width: 74px;
|
||||
position: absolute;
|
||||
top: 94.05px;
|
||||
left: 46px;
|
||||
}
|
||||
#right-arm-img {
|
||||
width: 106px;
|
||||
position: absolute;
|
||||
top: 110.05px;
|
||||
left: 78px;
|
||||
}
|
||||
#left-arm-img {
|
||||
width: 114px;
|
||||
position: absolute;
|
||||
top: 69.05px;
|
||||
left: 69px;
|
||||
}
|
||||
#file-img {
|
||||
width: 125px;
|
||||
position: absolute;
|
||||
top: 15.66px;
|
||||
left: 152px;
|
||||
}
|
||||
|
||||
.swing {
|
||||
animation: swing ease-in-out 0.6s infinite alternate;
|
||||
}
|
||||
#left-arm-img.swing {
|
||||
transform-origin: 9px 83px;
|
||||
}
|
||||
#right-arm-img.swing {
|
||||
transform-origin: 0 56px;
|
||||
}
|
||||
#file-img.swing {
|
||||
transform-origin: -74px 139px;
|
||||
}
|
||||
@keyframes swing {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-7deg);
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.fly {
|
||||
animation: fly ease-in-out 1s 1 normal;
|
||||
transform-origin: center center;
|
||||
}
|
||||
@keyframes fly {
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
124
src/components/IconButton.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<button
|
||||
:class="['icon-btn', { active }]"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<div class="icon"><slot /></div>
|
||||
<div v-show="loading" class="icon-in-progress">
|
||||
<loading-indicator />
|
||||
</div>
|
||||
<span
|
||||
v-if="tooltip"
|
||||
ref="tooltip"
|
||||
class="icon-tooltip"
|
||||
:style="tooltipStyle"
|
||||
>
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
|
||||
export default {
|
||||
name: 'SideBarButton',
|
||||
components: { LoadingIndicator },
|
||||
mixins: [tooltipMixin],
|
||||
props: {
|
||||
active: Boolean,
|
||||
disabled: Boolean,
|
||||
tooltip: String,
|
||||
tooltipPosition: String,
|
||||
loading: Boolean
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon-btn {
|
||||
box-sizing: border-box;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
}
|
||||
|
||||
.icon-btn:hover .icon :deep(path),
|
||||
.icon-btn.active .icon :deep(path),
|
||||
.icon-btn:hover .icon :deep(circle),
|
||||
.icon-btn.active .icon :deep(circle) {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.icon-btn:disabled .icon :deep(path),
|
||||
.icon-btn:disabled .icon :deep(circle) {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.icon-btn:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.disabled.icon-btn:hover .icon :deep(path) {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.icon-in-progress {
|
||||
position: absolute;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: var(--color-bg-light);
|
||||
will-change: opacity;
|
||||
/*
|
||||
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||
the Main Thread processes a result set.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
animation: show-loader 1s linear 0s 1;
|
||||
}
|
||||
|
||||
@keyframes show-loader {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
115
src/components/LoadingDialog.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<modal
|
||||
:modalId="name"
|
||||
class="dialog"
|
||||
:clickToClose="false"
|
||||
:contentTransition="{ name: 'loading-dialog' }"
|
||||
:overlayTransition="{ name: 'loading-dialog' }"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
{{ title }}
|
||||
<close-icon :disabled="loading" @click="$emit('cancel')" />
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-if="loading" class="loading-dialog-body">
|
||||
<loading-indicator :size="30" class="state-icon" />
|
||||
{{ loadingMsg }}
|
||||
</div>
|
||||
<div v-else class="loading-dialog-body">
|
||||
<img
|
||||
src="~@/assets/images/success.svg"
|
||||
class="success-icon state-icon"
|
||||
/>
|
||||
{{ successMsg }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="$emit('cancel')"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="primary"
|
||||
type="button"
|
||||
:disabled="loading"
|
||||
@click="$emit('action')"
|
||||
>
|
||||
{{ actionBtnName }}
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'LoadingDialog',
|
||||
components: { LoadingIndicator, CloseIcon },
|
||||
props: {
|
||||
loadingMsg: String,
|
||||
successMsg: String,
|
||||
actionBtnName: String,
|
||||
name: String,
|
||||
title: String,
|
||||
loading: Boolean
|
||||
},
|
||||
emits: ['cancel', 'action'],
|
||||
watch: {
|
||||
loading() {
|
||||
if (this.loading) {
|
||||
this.$modal.show(this.name)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancel() {
|
||||
this.$emit('cancel')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.loading-dialog-enter-active {
|
||||
animation: show-modal 1s linear 0s 1;
|
||||
}
|
||||
.loading-dialog-leave-active {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@keyframes show-modal {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
99% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-modal {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.loading-dialog-body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.success-icon {
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
129
src/components/LoadingIndicator.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="animationClass"
|
||||
:height="size"
|
||||
:width="size"
|
||||
:viewBox="`0 0 ${size} ${size}`"
|
||||
>
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
:style="{ strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
:style="{
|
||||
strokeDasharray: circleProgress,
|
||||
strokeDashoffset: offset,
|
||||
strokeWidth
|
||||
}"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIndicator',
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
emits: [],
|
||||
computed: {
|
||||
circleProgress() {
|
||||
const circle = this.radius * 3.14 * 2
|
||||
const dash = this.progress
|
||||
? (circle * this.progress) / 100
|
||||
: (circle * 1) / 3
|
||||
const space = circle - dash
|
||||
return `${dash}px, ${space}px`
|
||||
},
|
||||
animationClass() {
|
||||
return this.progress === undefined ? 'loading' : 'progress'
|
||||
},
|
||||
radius() {
|
||||
return this.size / 2 - this.strokeWidth
|
||||
},
|
||||
offset() {
|
||||
return (this.radius * 3.14) / 2
|
||||
},
|
||||
strokeWidth() {
|
||||
return this.size / 10
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader-svg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
|
||||
.loader-svg.bg {
|
||||
stroke: var(--color-gray-light-3);
|
||||
}
|
||||
|
||||
.loading .loader-svg.front {
|
||||
will-change: transform;
|
||||
animation: fill-animation-loading 1s cubic-bezier(1, 1, 1, 1) 0s infinite;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
/*
|
||||
We can't change anything in loading animation except transform, opacity and filter. Because in
|
||||
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
|
||||
from the web-worker after query execution).
|
||||
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
|
||||
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
|
||||
processes something else.
|
||||
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||
*/
|
||||
@keyframes fill-animation-loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress .loader-svg.front {
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
.progress .loader-svg.bg {
|
||||
animation: bg-animation 1.5s cubic-bezier(1, 1, 1, 1) 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes bg-animation {
|
||||
0% {
|
||||
r: 8;
|
||||
}
|
||||
50% {
|
||||
stroke: var(--color-gray-light-2);
|
||||
r: 9;
|
||||
}
|
||||
100% {
|
||||
r: 8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
94
src/components/Logs.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<div ref="logsContainer" class="logs-container">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="msg">
|
||||
<img v-if="msg.type === 'error'" src="~@/assets/images/error.svg" />
|
||||
<img
|
||||
v-if="msg.type === 'info'"
|
||||
src="~@/assets/images/info.svg"
|
||||
width="20px"
|
||||
/>
|
||||
<img v-if="msg.type === 'success'" src="~@/assets/images/success.svg" />
|
||||
<loading-indicator
|
||||
v-if="msg.type === 'loading'"
|
||||
:progress="msg.progress"
|
||||
/>
|
||||
<span class="msg-text">{{ serializeMessage(msg) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
|
||||
export default {
|
||||
name: 'Logs',
|
||||
components: { LoadingIndicator },
|
||||
props: { messages: Array },
|
||||
emits: [],
|
||||
watch: {
|
||||
'messages.length': 'scrollToBottom'
|
||||
},
|
||||
mounted() {
|
||||
this.scrollToBottom()
|
||||
},
|
||||
methods: {
|
||||
async scrollToBottom() {
|
||||
const container = this.$refs.logsContainer
|
||||
if (container) {
|
||||
await this.$nextTick()
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
},
|
||||
|
||||
serializeMessage(msg) {
|
||||
let result = ''
|
||||
if (msg.row !== null && msg.row !== undefined) {
|
||||
if (msg.type === 'error') {
|
||||
result += `Error in row ${msg.row}. `
|
||||
} else {
|
||||
result += `Information about row ${msg.row}. `
|
||||
}
|
||||
}
|
||||
|
||||
result += msg.message
|
||||
if (!/(\.|!|\?)$/.test(result)) {
|
||||
result += '.'
|
||||
}
|
||||
|
||||
if (msg.hint) {
|
||||
result += ` ${msg.hint}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-container {
|
||||
background-color: var(--color-white);
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.msg {
|
||||
padding: 12px 7px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.msg:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
margin-left: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<nav>
|
||||
<div>
|
||||
<router-link to="/editor">Editor</router-link>
|
||||
<router-link to="/my-queries">My queries</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
v-if="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="runDisabled"
|
||||
@click="currentQuery.execute"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
v-show="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="!isUnsaved"
|
||||
@click="checkQueryBeforeSave"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button class="primary" @click="createNewQuery">Create</button>
|
||||
</div>
|
||||
|
||||
<!--Save Query dialog -->
|
||||
<modal name="save" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Save query
|
||||
<close-icon @click="cancelSave"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-show="isPredefined" id="save-note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries can't be edited.
|
||||
That's why your modifications will be saved as a new query. Enter the name for it.
|
||||
</div>
|
||||
<text-field
|
||||
label="Query name"
|
||||
:error-msg="errorMsg"
|
||||
v-model="name"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||
<button class="primary" @click="saveQuery">Save</button>
|
||||
</div>
|
||||
</modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nanoid } from 'nanoid'
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
components: {
|
||||
TextField,
|
||||
CloseIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
errorMsg: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQuery () {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isUnsaved () {
|
||||
if (!this.currentQuery) {
|
||||
return false
|
||||
}
|
||||
const tabIndex = this.currentQuery.tabIndex
|
||||
return this.$store.state.tabs[tabIndex].isUnsaved
|
||||
},
|
||||
isPredefined () {
|
||||
if (this.currentQuery) {
|
||||
return this.currentQuery.isPredefined
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
runDisabled () {
|
||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('createNewQuery', this.createNewQuery)
|
||||
this.$root.$on('saveQuery', this.checkQueryBeforeSave)
|
||||
document.addEventListener('keydown', this._keyListener)
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keydown', this._keyListener)
|
||||
},
|
||||
methods: {
|
||||
createNewQuery () {
|
||||
const tab = {
|
||||
id: nanoid(),
|
||||
name: null,
|
||||
tempName: this.$store.state.untitledLastIndex
|
||||
? `Untitled ${this.$store.state.untitledLastIndex}`
|
||||
: 'Untitled',
|
||||
isUnsaved: true
|
||||
}
|
||||
this.$store.commit('addTab', tab)
|
||||
this.$store.commit('setCurrentTabId', tab.id)
|
||||
},
|
||||
cancelSave () {
|
||||
this.$modal.hide('save')
|
||||
this.$root.$off('querySaved')
|
||||
},
|
||||
checkQueryBeforeSave () {
|
||||
this.errorMsg = null
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
|
||||
if (isFromScratch || this.isPredefined) {
|
||||
this.name = ''
|
||||
this.$modal.show('save')
|
||||
} else {
|
||||
this.saveQuery()
|
||||
}
|
||||
},
|
||||
saveQuery () {
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
if ((isFromScratch || this.isPredefined) && !this.name) {
|
||||
this.errorMsg = 'Query name can\'t be empty'
|
||||
return
|
||||
}
|
||||
const dataSet = this.currentQuery.result
|
||||
const tabView = this.currentQuery.view
|
||||
// Prepare query
|
||||
const value = {
|
||||
id: this.isPredefined ? nanoid() : this.currentQuery.id,
|
||||
query: this.currentQuery.query,
|
||||
chart: this.currentQuery.getChartSatateForSave(),
|
||||
name: (!this.isPredefined && this.currentQuery.initName) || this.name,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
// Save query
|
||||
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
|
||||
if (!myQueries) {
|
||||
myQueries = [value]
|
||||
} else if (isFromScratch || this.isPredefined) {
|
||||
myQueries.push(value)
|
||||
} else {
|
||||
const queryIndex = myQueries.findIndex(query => query.id === this.currentQuery.id)
|
||||
value.createdAt = myQueries[queryIndex].createdAt
|
||||
myQueries[queryIndex] = value
|
||||
}
|
||||
localStorage.setItem('myQueries', JSON.stringify(myQueries))
|
||||
|
||||
// Update tab
|
||||
this.$store.commit('updateTab', {
|
||||
index: this.currentQuery.tabIndex,
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
chart: value.chart,
|
||||
isUnsaved: false
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
// e.g. if we save predefined query the tab will be created again
|
||||
// (because of new id) and
|
||||
// it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view
|
||||
this.$nextTick(() => {
|
||||
this.currentQuery.result = dataSet
|
||||
this.currentQuery.view = tabView
|
||||
})
|
||||
|
||||
// Hide dialog
|
||||
this.$modal.hide('save')
|
||||
|
||||
// Signal about saving
|
||||
this.$root.$emit('querySaved')
|
||||
},
|
||||
_keyListener (e) {
|
||||
if (this.$route.path === '/editor') {
|
||||
// Run query Ctrl+R
|
||||
if (e.key === 'r' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (!this.runDisabled) {
|
||||
this.currentQuery.execute()
|
||||
}
|
||||
}
|
||||
|
||||
// Save query Ctrl+S
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (this.isUnsaved) {
|
||||
this.checkQueryBeforeSave()
|
||||
}
|
||||
}
|
||||
|
||||
// New (blank) query Ctrl+B
|
||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.createNewQuery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
box-shadow: var(--shadow-1);
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 0 52px;
|
||||
z-index: 999;
|
||||
}
|
||||
a {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-base);
|
||||
text-transform: none;
|
||||
text-decoration: none;
|
||||
margin-right: 28px;
|
||||
}
|
||||
a.router-link-active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#save-note {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#save-note img {
|
||||
margin: -3px 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -1,97 +0,0 @@
|
||||
<template>
|
||||
<paginate
|
||||
:page-count="pageCount"
|
||||
:page-range="5"
|
||||
:margin-pages="1"
|
||||
:prev-text="chevron"
|
||||
:next-text="chevron"
|
||||
:no-li-surround="true"
|
||||
container-class="paginator-continer"
|
||||
page-link-class="paginator-page-link"
|
||||
active-class="paginator-active-page"
|
||||
break-view-link-class="paginator-break"
|
||||
next-link-class="paginator-next"
|
||||
prev-link-class="paginator-prev"
|
||||
disabled-class="paginator-disabled"
|
||||
v-model="page"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Paginate from 'vuejs-paginate'
|
||||
|
||||
export default {
|
||||
name: 'Pager',
|
||||
components: { Paginate },
|
||||
props: ['pageCount', 'value'],
|
||||
data () {
|
||||
return {
|
||||
page: this.value,
|
||||
chevron: `
|
||||
<svg width="9" height="9" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"/>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page () {
|
||||
this.$emit('input', this.page)
|
||||
},
|
||||
value () {
|
||||
this.page = this.value
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.paginator-continer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
>>> .paginator-page-link {
|
||||
padding: 2px 3px;
|
||||
margin: 0 5px;
|
||||
display: block;
|
||||
color: var(--color-text-base);
|
||||
font-size: 11px;
|
||||
}
|
||||
>>> .paginator-page-link:hover {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
>>> .paginator-page-link:active,
|
||||
>>> .paginator-page-link:visited,
|
||||
>>> .paginator-page-link:focus,
|
||||
>>> .paginator-next:active,
|
||||
>>> .paginator-next:visited,
|
||||
>>> .paginator-next:focus,
|
||||
>>> .paginator-prev:active,
|
||||
>>> .paginator-prev:visited,
|
||||
>>> .paginator-prev:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
>>> .paginator-active-page,
|
||||
>>> .paginator-active-page:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
>>> .paginator-break:hover,
|
||||
>>> .paginator-disabled:hover {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
>>> .paginator-prev svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
>>> .paginator-next:hover path,
|
||||
>>> .paginator-prev:hover path {
|
||||
fill: var(--color-text-active);
|
||||
}
|
||||
>>> .paginator-disabled path,
|
||||
>>> .paginator-disabled:hover path {
|
||||
fill: var(--color-text-light-2);
|
||||
}
|
||||
</style>
|
||||
@@ -1,117 +0,0 @@
|
||||
<template>
|
||||
<div id="schema-container">
|
||||
<div id="schema-filter">
|
||||
<text-field placeholder="Search table" width="100%" v-model="filter"/>
|
||||
</div>
|
||||
<div id="db">
|
||||
<div @click="schemaVisible = !schemaVisible" class="db-name">
|
||||
<tree-chevron :expanded="schemaVisible"/>
|
||||
{{ dbName }}
|
||||
</div>
|
||||
<div class="db-edit">
|
||||
<input type="file" id="actual-btn" ref="dbfile" hidden @change="changeDb"/>
|
||||
<label for="actual-btn">
|
||||
<change-db-icon />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="schemaVisible" class="schema">
|
||||
<table-description
|
||||
v-for="table in schema"
|
||||
:key="table.name"
|
||||
:name="table.name"
|
||||
:columns="table.columns"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import TableDescription from '@/components/TableDescription'
|
||||
import TextField from '@/components/TextField'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import TreeChevron from '@/components/svg/treeChevron'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
components: {
|
||||
TableDescription,
|
||||
TextField,
|
||||
ChangeDbIcon,
|
||||
TreeChevron
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
schemaVisible: true,
|
||||
filter: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
schema () {
|
||||
if (!this.$store.state.schema) {
|
||||
return []
|
||||
}
|
||||
|
||||
return !this.filter
|
||||
? this.$store.state.schema
|
||||
: this.$store.state.schema.filter(
|
||||
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
|
||||
)
|
||||
},
|
||||
dbName () {
|
||||
return this.$store.state.dbName
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
changeDb () {
|
||||
this.$db.loadDb(this.$refs.dbfile.files[0])
|
||||
.then((schema) => {
|
||||
this.$store.commit('saveSchema', schema)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#schema-container {
|
||||
position: relative;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.schema {
|
||||
margin-left: 12px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
#schema-filter {
|
||||
padding: 32px 12px;
|
||||
position: sticky;
|
||||
position: -webkit-sticky;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
box-sizing: border-box;
|
||||
background-image: linear-gradient(white 73%, transparent);;
|
||||
z-index: 2;
|
||||
}
|
||||
.schema, .db-name {
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
#db {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: -5px;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.db-name {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.db-name:hover .chevron-icon path,
|
||||
>>> .table-name:hover .chevron-icon path {
|
||||
fill: var(--color-gray-dark);
|
||||
}
|
||||
</style>
|
||||
@@ -1,385 +0,0 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[
|
||||
'splitpanes',
|
||||
`splitpanes--${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes--dragging': touch.dragging }
|
||||
]"
|
||||
>
|
||||
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
ref="left"
|
||||
:size="paneBefore.size"
|
||||
max-size="30"
|
||||
:style="styles.before"
|
||||
>
|
||||
<slot name="left-pane" />
|
||||
</div>
|
||||
<!-- Splitter start-->
|
||||
<div
|
||||
class="splitpanes__splitter"
|
||||
@mousedown="onMouseDown"
|
||||
@touchstart="onMouseDown"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'toggle-btns',
|
||||
{'both': after.max === 100 && before.max === 100 && after.size > 0 && before.size > 0}
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="after.max === 100 && after.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane('before')"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
:src="require('@/assets/images/chevron.svg')"
|
||||
:style="directionBeforeIconStyle"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="before.max === 100 && before.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane('after')"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
:src="require('@/assets/images/chevron.svg')"
|
||||
:style="directionAfterIconStyle"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- splitter end -->
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
ref="right"
|
||||
:style="styles.after"
|
||||
>
|
||||
<slot name="right-pane" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
props: {
|
||||
horizontal: { type: Boolean, default: false },
|
||||
before: { type: Object },
|
||||
after: { type: Object }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
container: null,
|
||||
paneBefore: this.before,
|
||||
paneAfter: this.after,
|
||||
beforeMinimising: {
|
||||
before: this.before.size,
|
||||
after: this.after.size
|
||||
},
|
||||
touch: {
|
||||
mouseDown: false,
|
||||
dragging: false
|
||||
},
|
||||
movableSplitter: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles () {
|
||||
return {
|
||||
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
|
||||
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
|
||||
}
|
||||
},
|
||||
movableSplitterStyle () {
|
||||
const style = { ...this.movableSplitter }
|
||||
style.top += '%'
|
||||
style.left += '%'
|
||||
return style
|
||||
},
|
||||
directionBeforeIconStyle () {
|
||||
const expanded = this.paneBefore.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
if (this.horizontal) {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(90deg)' : 'rotate(-90deg)'}`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(0deg)' : 'rotate(180deg)'}`
|
||||
}
|
||||
}
|
||||
},
|
||||
directionAfterIconStyle () {
|
||||
const expanded = this.paneAfter.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
if (this.horizontal) {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(180deg)' : 'rotate(0deg)'}`
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bindEvents () {
|
||||
document.addEventListener('mousemove', this.onMouseMove, { passive: false })
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
// Passive: false to prevent scrolling while touch dragging.
|
||||
if ('ontouchstart' in window) {
|
||||
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
|
||||
document.addEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
unbindEvents () {
|
||||
document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
|
||||
document.removeEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown () {
|
||||
this.bindEvents()
|
||||
this.touch.mouseDown = true
|
||||
},
|
||||
|
||||
onMouseMove (event) {
|
||||
if (this.touch.mouseDown) {
|
||||
// Prevent scrolling while touch dragging (only works with an active event, eg. passive: false).
|
||||
event.preventDefault()
|
||||
this.touch.dragging = true
|
||||
this.$set(this.movableSplitter, 'visibility', 'visible')
|
||||
this.moveSplitter(event)
|
||||
}
|
||||
},
|
||||
|
||||
onMouseUp () {
|
||||
this.touch.mouseDown = false
|
||||
if (this.touch.dragging) {
|
||||
const dragPercentage = this.horizontal
|
||||
? this.movableSplitter.top
|
||||
: this.movableSplitter.left
|
||||
|
||||
this.paneBefore.size = dragPercentage
|
||||
this.paneAfter.size = 100 - dragPercentage
|
||||
|
||||
this.movableSplitter = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
|
||||
// Keep dragging flag until click event is finished (click happens immediately after mouseup)
|
||||
// in order to prevent emitting `splitter-click` event if splitter was dragged.
|
||||
setTimeout(() => {
|
||||
this.touch.dragging = false
|
||||
this.unbindEvents()
|
||||
}, 100)
|
||||
},
|
||||
|
||||
// Get the cursor position relative to the splitpane container.
|
||||
getCurrentMouseDrag (event) {
|
||||
const rect = this.container.getBoundingClientRect()
|
||||
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
|
||||
? event.touches[0]
|
||||
: event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
|
||||
// if the sum of size of the 2 cells is 60%, the dragPercentage range will be 0 to 100% of this 60%.
|
||||
getCurrentDragPercentage (drag) {
|
||||
drag = drag[this.horizontal ? 'y' : 'x']
|
||||
// In the code bellow 'size' refers to 'width' for vertical and 'height' for horizontal layout.
|
||||
const containerSize = this.container[this.horizontal ? 'clientHeight' : 'clientWidth']
|
||||
return drag * 100 / containerSize
|
||||
},
|
||||
|
||||
moveSplitter (event) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(this.getCurrentMouseDrag(event))
|
||||
const paneBefore = this.paneBefore
|
||||
const paneAfter = this.paneAfter
|
||||
|
||||
const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= paneBefore.max)
|
||||
const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - paneAfter.max)
|
||||
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
if (paneBeforeMaxReached) {
|
||||
this.$set(this.movableSplitter, dir, paneBefore.max)
|
||||
} else {
|
||||
this.$set(this.movableSplitter, dir, Math.max(100 - paneAfter.max, 0))
|
||||
}
|
||||
} else {
|
||||
this.$set(this.movableSplitter, dir, Math.min(Math.max(dragPercentage, 0), paneBefore.max))
|
||||
}
|
||||
},
|
||||
togglePane (toggledPane) {
|
||||
const pane = toggledPane === 'before' ? this.paneBefore : this.paneAfter
|
||||
if (pane.size > 0) {
|
||||
this.beforeMinimising.before = this.paneBefore.size
|
||||
this.beforeMinimising.after = this.paneAfter.size
|
||||
pane.size = 0
|
||||
const otherPane = toggledPane === 'before' ? this.paneAfter : this.paneBefore
|
||||
otherPane.size = 100 - pane.size
|
||||
} else {
|
||||
this.paneBefore.size = this.beforeMinimising.before
|
||||
this.paneAfter.size = this.beforeMinimising.after
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.container = this.$refs.container
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.splitpanes {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitpanes--vertical {flex-direction: row;}
|
||||
.splitpanes--horizontal {flex-direction: column;}
|
||||
.splitpanes--dragging * {user-select: none;}
|
||||
|
||||
.splitpanes__pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Splitter */
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical.splitpanes--dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal.splitpanes--dragging {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.splitpanes__splitter {
|
||||
touch-action: none;
|
||||
background-color: var(--color-bg-light);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter {
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.movable-splitter {
|
||||
position: absolute;
|
||||
background-color:rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical > .movable-splitter {
|
||||
width: 8px;
|
||||
z-index: 5;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal > .movable-splitter {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
/* Toggle buttons */
|
||||
.toggle-btns {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: var(--color-border-light);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid var(--color-border);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btn {
|
||||
height: 49px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btn {
|
||||
width: 49px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.toggle-btn .direction-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
|
||||
margin-top: -1px;
|
||||
}
|
||||
</style>
|
||||
394
src/components/Splitpanes/index.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:class="[
|
||||
'splitpanes',
|
||||
`splitpanes-${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes-dragging': dragging }
|
||||
]"
|
||||
>
|
||||
<div
|
||||
ref="movableSplitter"
|
||||
class="movable-splitter"
|
||||
:style="movableSplitterStyle"
|
||||
/>
|
||||
<div
|
||||
ref="left"
|
||||
class="splitpanes-pane"
|
||||
:size="paneBefore.size"
|
||||
max-size="30"
|
||||
:style="styles.before"
|
||||
>
|
||||
<slot name="left-pane" />
|
||||
</div>
|
||||
<!-- Splitter start-->
|
||||
<div
|
||||
class="splitpanes-splitter"
|
||||
@mousedown="bindEvents"
|
||||
@touchstart="bindEvents"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'toggle-btns',
|
||||
{
|
||||
both:
|
||||
after.max === 100 &&
|
||||
before.max === 100 &&
|
||||
paneAfter.size > 0 &&
|
||||
paneBefore.size > 0
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="after.max === 100 && paneAfter.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane(paneBefore)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
src="~@/assets/images/chevron.svg"
|
||||
:style="directionBeforeIconStyle"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="before.max === 100 && paneBefore.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane(paneAfter)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
src="~@/assets/images/chevron.svg"
|
||||
:style="directionAfterIconStyle"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- splitter end -->
|
||||
<div ref="right" class="splitpanes-pane" :style="styles.after">
|
||||
<slot name="right-pane" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import splitter from './splitter'
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
props: {
|
||||
horizontal: { type: Boolean, default: false },
|
||||
before: { type: Object },
|
||||
after: { type: Object },
|
||||
default: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
before: 50,
|
||||
after: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: [],
|
||||
data() {
|
||||
return {
|
||||
container: null,
|
||||
paneBefore: this.before,
|
||||
paneAfter: this.after,
|
||||
beforeMinimising:
|
||||
!this.after.size || !this.before.size
|
||||
? this.default
|
||||
: {
|
||||
before: this.before.size,
|
||||
after: this.after.size
|
||||
},
|
||||
dragging: false,
|
||||
movableSplitter: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
styles() {
|
||||
return {
|
||||
before: {
|
||||
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
|
||||
},
|
||||
after: {
|
||||
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
|
||||
}
|
||||
}
|
||||
},
|
||||
movableSplitterStyle() {
|
||||
const style = { ...this.movableSplitter }
|
||||
style.top += '%'
|
||||
style.left += '%'
|
||||
return style
|
||||
},
|
||||
directionBeforeIconStyle() {
|
||||
const expanded = this.paneBefore.size !== 0
|
||||
const translation = 'translate(-50%, -50%) '
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
rotation = expanded ? 'rotate(90deg)' : 'rotate(-90deg)'
|
||||
} else {
|
||||
rotation = expanded ? 'rotate(0deg)' : 'rotate(180deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
},
|
||||
directionAfterIconStyle() {
|
||||
const expanded = this.paneAfter.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
rotation = expanded ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
} else {
|
||||
rotation = expanded ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.container = this.$refs.container
|
||||
},
|
||||
|
||||
methods: {
|
||||
bindEvents() {
|
||||
// Passive: false to prevent scrolling while touch dragging.
|
||||
document.addEventListener('mousemove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
document.addEventListener('touchmove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.addEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
unbindEvents() {
|
||||
document.removeEventListener('mousemove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.removeEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
if ('ontouchstart' in window) {
|
||||
document.removeEventListener('touchmove', this.onMouseMove, {
|
||||
passive: false
|
||||
})
|
||||
document.removeEventListener('touchend', this.onMouseUp)
|
||||
}
|
||||
},
|
||||
|
||||
onMouseMove(event) {
|
||||
event.preventDefault()
|
||||
this.dragging = true
|
||||
this.movableSplitter.visibility = 'visible'
|
||||
this.moveSplitter(event)
|
||||
},
|
||||
|
||||
onMouseUp() {
|
||||
if (this.dragging) {
|
||||
const dragPercentage = this.horizontal
|
||||
? this.movableSplitter.top
|
||||
: this.movableSplitter.left
|
||||
|
||||
this.paneBefore.size = dragPercentage
|
||||
this.paneAfter.size = 100 - dragPercentage
|
||||
|
||||
this.movableSplitter = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
|
||||
this.dragging = false
|
||||
}
|
||||
|
||||
this.unbindEvents()
|
||||
},
|
||||
|
||||
moveSplitter(event) {
|
||||
const splitterInfo = {
|
||||
container: this.container,
|
||||
paneBeforeMax: this.paneBefore.max,
|
||||
paneAfterMax: this.paneAfter.max,
|
||||
isHorisontal: this.horizontal
|
||||
}
|
||||
const offset = splitter.calculateOffset(event, splitterInfo)
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
this.movableSplitter[dir] = offset
|
||||
},
|
||||
|
||||
togglePane(pane) {
|
||||
if (pane.size > 0) {
|
||||
this.beforeMinimising.before = this.paneBefore.size
|
||||
this.beforeMinimising.after = this.paneAfter.size
|
||||
pane.size = 0
|
||||
const otherPane =
|
||||
pane === this.paneBefore ? this.paneAfter : this.paneBefore
|
||||
otherPane.size = 100 - pane.size
|
||||
} else {
|
||||
this.paneBefore.size = this.beforeMinimising.before
|
||||
this.paneAfter.size = this.beforeMinimising.after
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.splitpanes {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitpanes-vertical {
|
||||
flex-direction: row;
|
||||
}
|
||||
.splitpanes-horizontal {
|
||||
flex-direction: column;
|
||||
}
|
||||
.splitpanes-dragging * {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.splitpanes-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Splitter */
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical.splitpanes-dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal.splitpanes-dragging {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.splitpanes-splitter {
|
||||
touch-action: none;
|
||||
background-color: var(--color-bg-light);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter {
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.movable-splitter {
|
||||
position: absolute;
|
||||
background-color: rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical > .movable-splitter {
|
||||
width: 8px;
|
||||
z-index: 5;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal > .movable-splitter {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
}
|
||||
/* Toggle buttons */
|
||||
.toggle-btns {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.toggle-btn {
|
||||
background-color: var(--color-border-light);
|
||||
border-radius: var(--border-radius-small);
|
||||
border: 1px solid var(--color-border);
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btn {
|
||||
height: 49px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btn {
|
||||
width: 49px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
.toggle-btn .direction-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.splitpanes-horizontal
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
|
||||
}
|
||||
|
||||
.splitpanes-horizontal
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:last-child {
|
||||
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.splitpanes-vertical
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
|
||||
}
|
||||
|
||||
.splitpanes-vertical
|
||||
> .splitpanes-splitter
|
||||
.toggle-btns.both
|
||||
.toggle-btn:last-child {
|
||||
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
|
||||
margin-top: -1px;
|
||||
}
|
||||
</style>
|
||||
47
src/components/Splitpanes/splitter.js
Normal file
@@ -0,0 +1,47 @@
|
||||
export default {
|
||||
// Get the cursor position relative to the splitpane container.
|
||||
getCurrentMouseDrag(event, container) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const { clientX, clientY } =
|
||||
'ontouchstart' in window && event.touches ? event.touches[0] : event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
|
||||
getCurrentDragPercentage(event, container, isHorisontal) {
|
||||
let drag = this.getCurrentMouseDrag(event, container)
|
||||
drag = drag[isHorisontal ? 'y' : 'x']
|
||||
const containerSize =
|
||||
container[isHorisontal ? 'clientHeight' : 'clientWidth']
|
||||
return (drag * 100) / containerSize
|
||||
},
|
||||
|
||||
// Returns the new position in percents.
|
||||
calculateOffset(
|
||||
event,
|
||||
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
|
||||
) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(
|
||||
event,
|
||||
container,
|
||||
isHorisontal
|
||||
)
|
||||
|
||||
const paneBeforeMaxReached =
|
||||
paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
|
||||
const paneAfterMaxReached =
|
||||
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
return paneBeforeMaxReached
|
||||
? paneBeforeMax
|
||||
: Math.max(100 - paneAfterMax, 0)
|
||||
} else {
|
||||
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<div class="codemirror-container">
|
||||
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CM from 'codemirror'
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/sql/sql.js'
|
||||
import 'codemirror/theme/neo.css'
|
||||
import 'codemirror/addon/hint/show-hint.js'
|
||||
import 'codemirror/addon/hint/show-hint.css'
|
||||
import 'codemirror/addon/hint/sql-hint.js'
|
||||
import { debounce } from 'debounce'
|
||||
|
||||
const sqlHint = CM.hint.sql
|
||||
CM.hint.sql = (cm, options) => {
|
||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||
const result = sqlHint(cm, options)
|
||||
// Don't show the hint if there is only one option
|
||||
// and the token is already completed with this option
|
||||
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
|
||||
result.list = []
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'SqlEditor',
|
||||
props: ['value'],
|
||||
components: {
|
||||
codemirror
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.value,
|
||||
cmOptions: {
|
||||
// codemirror options
|
||||
tabSize: 4,
|
||||
mode: 'text/x-mysql',
|
||||
theme: 'neo',
|
||||
lineNumbers: true,
|
||||
line: true
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tables () {
|
||||
const tables = {}
|
||||
if (this.$store.state.schema) {
|
||||
this.$store.state.schema.forEach(table => {
|
||||
tables[table.name] = table.columns.map(column => column.name)
|
||||
})
|
||||
}
|
||||
return tables
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.$emit('input', this.query)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCmChange: debounce(function (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
|
||||
const tokenType = editor.getTokenAt(editor.getCursor()).type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
const hintOptions = {
|
||||
tables: this.tables,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
}
|
||||
|
||||
CM.showHint(editor, CM.hint.sql, hintOptions)
|
||||
}, 400)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.codemirror-container {
|
||||
flex-grow: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
>>> .vue-codemirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror-cursor {
|
||||
width: 1px;
|
||||
background: var(--color-text-base);
|
||||
}
|
||||
</style>
|
||||
@@ -1,115 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="rounded-bg">
|
||||
<div class="header-container" ref="header-container">
|
||||
<div>
|
||||
<div
|
||||
v-for="(th, index) in header"
|
||||
class="fixed-header"
|
||||
:style="{ width: `${th.width}px` }"
|
||||
:key="index"
|
||||
>
|
||||
{{ th.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="table-container"
|
||||
ref="table-container"
|
||||
@scroll="onScrollTable"
|
||||
:style="{maxHeight: `${height}px`}"
|
||||
>
|
||||
<table ref="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(th,index) in dataSet.columns" :key="index" ref="th">
|
||||
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row,index) in currentPageData" :key="index">
|
||||
<td v-for="(value, valIndex) in row" :key="valIndex">
|
||||
<div class="cell-data" :style="cellStyle">{{ value }}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<div class="table-footer-count">
|
||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pager from '@/components/Pager'
|
||||
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: ['dataSet', 'height'],
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
tableWidth: null,
|
||||
currentPage: 1,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
cellStyle () {
|
||||
const eq = this.tableWidth / this.dataSet.columns.length
|
||||
|
||||
return { maxWidth: `${Math.max(eq, 100)}px` }
|
||||
},
|
||||
pageSize () {
|
||||
return Math.max(Math.floor(this.height / 40), 20)
|
||||
},
|
||||
pageCount () {
|
||||
return Math.ceil(this.dataSet.values.length / this.pageSize)
|
||||
},
|
||||
currentPageData () {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
return this.dataSet.values.slice(start, start + this.pageSize)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
calculateHeadersWidth () {
|
||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||
this.$nextTick(() => {
|
||||
this.header = this.$refs.th.map(th => {
|
||||
return { name: th.innerText, width: th.getBoundingClientRect().width }
|
||||
})
|
||||
})
|
||||
},
|
||||
onScrollTable () {
|
||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||
},
|
||||
functionName () {
|
||||
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
watch: {
|
||||
currentPageData: 'calculateHeadersWidth',
|
||||
dataSet () {
|
||||
this.currentPage = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
109
src/components/SqlTable/Pager.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<paginate
|
||||
v-model="page"
|
||||
:pageCount="pageCount"
|
||||
:pageRange="5"
|
||||
:marginPages="1"
|
||||
:prevText="chevron"
|
||||
:nextText="chevron"
|
||||
:noLiSurround="true"
|
||||
containerClass="paginator-continer"
|
||||
pageLinkClass="paginator-page-link"
|
||||
activeClass="paginator-active-page"
|
||||
breakViewLinkClass="paginator-break"
|
||||
nextLinkClass="paginator-next"
|
||||
prevLinkClass="paginator-prev"
|
||||
disabledClass="paginator-disabled"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Paginate from 'vuejs-paginate-next'
|
||||
|
||||
export default {
|
||||
name: 'Pager',
|
||||
components: { Paginate },
|
||||
props: {
|
||||
pageCount: Number,
|
||||
modelValue: Number
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
data() {
|
||||
return {
|
||||
page: this.modelValue,
|
||||
chevron: `
|
||||
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
|
||||
<path
|
||||
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
|
||||
5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
`
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
this.$emit('update:modelValue', this.page)
|
||||
},
|
||||
modelValue() {
|
||||
this.page = this.modelValue
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.paginator-continer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
:deep(a) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.paginator-page-link) {
|
||||
padding: 2px 3px;
|
||||
margin: 0 5px;
|
||||
display: block;
|
||||
color: var(--color-text-base);
|
||||
font-size: 11px;
|
||||
}
|
||||
:deep(.paginator-page-link:hover) {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
:deep(.paginator-page-link:active),
|
||||
:deep(.paginator-page-link:visited),
|
||||
:deep(.paginator-page-link:focus),
|
||||
:deep(.paginator-next:active),
|
||||
:deep(.paginator-next:visited),
|
||||
:deep(.paginator-next:focus),
|
||||
:deep(.paginator-prev:active),
|
||||
:deep(.paginator-prev:visited),
|
||||
:deep(.paginator-prev:focus) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:deep(.paginator-active-page),
|
||||
:deep(.paginator-active-page:hover) {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
:deep(.paginator-break:hover),
|
||||
:deep(.paginator-disabled:hover) {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
:deep(.paginator-prev svg) {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
:deep(.paginator-next:hover path),
|
||||
:deep(.paginator-prev:hover path) {
|
||||
fill: var(--color-text-active);
|
||||
}
|
||||
:deep(.paginator-disabled path),
|
||||
:deep(.paginator-disabled:hover path) {
|
||||
fill: var(--color-text-light-2);
|
||||
}
|
||||
</style>
|
||||
281
src/components/SqlTable/index.vue
Normal file
@@ -0,0 +1,281 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="rounded-bg">
|
||||
<div ref="header-container" class="header-container">
|
||||
<div>
|
||||
<div
|
||||
v-for="(th, index) in header"
|
||||
:key="index"
|
||||
class="fixed-header"
|
||||
:style="{ width: `${th.width}px` }"
|
||||
:title="th.name"
|
||||
>
|
||||
{{ th.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="table-container"
|
||||
class="table-container"
|
||||
@scroll="onScrollTable"
|
||||
>
|
||||
<table
|
||||
ref="table"
|
||||
class="sqliteviz-table"
|
||||
tabindex="0"
|
||||
@keydown="onTableKeydown"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||
<td
|
||||
v-for="(col, colIndex) in columns"
|
||||
:key="colIndex"
|
||||
:data-col="colIndex"
|
||||
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
|
||||
:data-isNull="isNull(getCellValue(col, rowIndex))"
|
||||
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
|
||||
:aria-selected="false"
|
||||
@click="onCellClick"
|
||||
>
|
||||
<div class="cell-data" :style="cellStyle">
|
||||
{{ getCellText(col, rowIndex) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<div class="table-footer-count">
|
||||
{{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
|
||||
<span v-if="preview">for preview</span>
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</div>
|
||||
<pager
|
||||
v-show="pageCount > 1"
|
||||
v-model="currentPage"
|
||||
:pageCount="pageCount"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pager from './Pager.vue'
|
||||
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: {
|
||||
dataSet: Object,
|
||||
time: [String, Number],
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
preview: Boolean,
|
||||
selectedCellCoordinates: Object
|
||||
},
|
||||
emits: ['updateSelectedCell'],
|
||||
data() {
|
||||
return {
|
||||
header: null,
|
||||
tableWidth: null,
|
||||
currentPage: this.page,
|
||||
resizeObserver: null,
|
||||
selectedCellElement: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns() {
|
||||
return this.dataSet.columns
|
||||
},
|
||||
rowCount() {
|
||||
return this.dataSet.values[this.columns[0]].length
|
||||
},
|
||||
cellStyle() {
|
||||
const eq = this.tableWidth / this.columns.length
|
||||
return { maxWidth: `${Math.max(eq, 100)}px` }
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.rowCount / this.pageSize)
|
||||
},
|
||||
currentPageData() {
|
||||
const start = (this.currentPage - 1) * this.pageSize
|
||||
let end = start + this.pageSize
|
||||
if (end > this.rowCount - 1) {
|
||||
end = this.rowCount - 1
|
||||
}
|
||||
return {
|
||||
start,
|
||||
end,
|
||||
count: end - start + 1
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentPageData() {
|
||||
this.calculateHeadersWidth()
|
||||
this.selectCell(null)
|
||||
},
|
||||
dataSet() {
|
||||
this.currentPage = 1
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
|
||||
if (this.selectedCellCoordinates) {
|
||||
const { row, col } = this.selectedCellCoordinates
|
||||
const cell = this.$refs.table.querySelector(
|
||||
`td[data-col="${col}"][data-row="${row}"]`
|
||||
)
|
||||
if (cell) {
|
||||
this.selectCell(cell)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeUnmount() {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
methods: {
|
||||
isBlob(value) {
|
||||
return value && ArrayBuffer.isView(value)
|
||||
},
|
||||
isNull(value) {
|
||||
return value === null
|
||||
},
|
||||
getCellValue(col, rowIndex) {
|
||||
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
|
||||
},
|
||||
getCellText(col, rowIndex) {
|
||||
const value = this.getCellValue(col, rowIndex)
|
||||
if (this.isNull(value)) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (this.isBlob(value)) {
|
||||
return 'BLOB'
|
||||
}
|
||||
return value
|
||||
},
|
||||
calculateHeadersWidth() {
|
||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||
this.$nextTick(() => {
|
||||
this.header = this.$refs.th.map(th => {
|
||||
return { name: th.innerText, width: th.getBoundingClientRect().width }
|
||||
})
|
||||
})
|
||||
},
|
||||
onScrollTable() {
|
||||
this.$refs['header-container'].scrollLeft =
|
||||
this.$refs['table-container'].scrollLeft
|
||||
},
|
||||
onTableKeydown(e) {
|
||||
const keyCodeMap = {
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
}
|
||||
|
||||
if (
|
||||
!this.selectedCellElement ||
|
||||
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||
},
|
||||
onCellClick(e) {
|
||||
this.selectCell(e.target.closest('td'), false)
|
||||
},
|
||||
selectCell(cell, scrollTo = true) {
|
||||
if (!cell) {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
this.selectedCellElement = cell
|
||||
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
cell.ariaSelected = 'true'
|
||||
this.selectedCellElement = cell
|
||||
} else {
|
||||
cell.ariaSelected = 'false'
|
||||
this.selectedCellElement = null
|
||||
}
|
||||
|
||||
if (this.selectedCellElement && scrollTo) {
|
||||
this.selectedCellElement.scrollIntoView()
|
||||
}
|
||||
|
||||
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||
},
|
||||
moveFocusInTable(initialCell, direction) {
|
||||
const currentRowIndex = +initialCell.dataset.row
|
||||
const currentColIndex = +initialCell.dataset.col
|
||||
let newRowIndex, newColIndex
|
||||
|
||||
if (direction === 'right') {
|
||||
if (currentColIndex === this.columns.length - 1) {
|
||||
newRowIndex = currentRowIndex + 1
|
||||
newColIndex = 0
|
||||
} else {
|
||||
newRowIndex = currentRowIndex
|
||||
newColIndex = currentColIndex + 1
|
||||
}
|
||||
} else if (direction === 'left') {
|
||||
if (currentColIndex === 0) {
|
||||
newRowIndex = currentRowIndex - 1
|
||||
newColIndex = this.columns.length - 1
|
||||
} else {
|
||||
newRowIndex = currentRowIndex
|
||||
newColIndex = currentColIndex - 1
|
||||
}
|
||||
} else if (direction === 'up') {
|
||||
newRowIndex = currentRowIndex - 1
|
||||
newColIndex = currentColIndex
|
||||
} else if (direction === 'down') {
|
||||
newRowIndex = currentRowIndex + 1
|
||||
newColIndex = currentColIndex
|
||||
}
|
||||
|
||||
const newCell = this.$refs.table.querySelector(
|
||||
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
|
||||
)
|
||||
if (newCell) {
|
||||
this.selectCell(newCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table.sqliteviz-table:focus {
|
||||
outline: none;
|
||||
}
|
||||
.sqliteviz-table tbody td:hover {
|
||||
background-color: var(--color-bg-light-3);
|
||||
}
|
||||
.sqliteviz-table tbody td[aria-selected='true'] {
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
@@ -1,192 +0,0 @@
|
||||
<template>
|
||||
<div class="tab-content-container" v-show="isActive">
|
||||
<splitpanes
|
||||
class="query-results-splitter"
|
||||
horizontal
|
||||
:before="{ size: 50, max: 100 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<div class="query-editor">
|
||||
<sql-editor v-model="query" />
|
||||
</div>
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<div id="bottomPane" ref="bottomPane">
|
||||
<view-switcher :view.sync="view" />
|
||||
<div v-show="view === 'table'" class="table-view">
|
||||
<div v-show="result === null && !isGettingResults && !error" class="table-preview">
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview">
|
||||
Fetching results...
|
||||
</div>
|
||||
<div v-show="result === undefined && !isGettingResults && !error" class="table-preview">
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<div v-show="error" class="table-preview error">
|
||||
{{ error }}
|
||||
</div>
|
||||
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
|
||||
</div>
|
||||
<chart
|
||||
:visible="view === 'chart'"
|
||||
:sql-result="result"
|
||||
:init-chart="initChart"
|
||||
ref="chart"
|
||||
@update="$store.commit('updateTabState', { index: tabIndex, isUnsaved: true })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import SqlEditor from '@/components/SqlEditor'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
||||
import Chart from '@/components/Chart'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex', 'isPredefined'],
|
||||
components: {
|
||||
SqlEditor,
|
||||
SqlTable,
|
||||
Splitpanes,
|
||||
ViewSwitcher,
|
||||
Chart
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.initQuery,
|
||||
result: null,
|
||||
view: 'table',
|
||||
tableViewHeight: 0,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
resizeObserver: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isActive () {
|
||||
return this.id === this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
this.resizeObserver.observe(this.$refs.bottomPane)
|
||||
this.calculateTableHeight()
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.bottomPane)
|
||||
},
|
||||
watch: {
|
||||
isActive () {
|
||||
if (this.isActive) {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
}
|
||||
},
|
||||
query () {
|
||||
this.$store.commit('updateTabState', { index: this.tabIndex, isUnsaved: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Run a command in the database
|
||||
execute () {
|
||||
// this.$refs.output.textContent = 'Fetching results...' */
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.$db.execute(this.query + ';')
|
||||
.then(result => {
|
||||
this.result = result
|
||||
})
|
||||
.catch(err => {
|
||||
this.error = err
|
||||
})
|
||||
.finally(() => {
|
||||
this.isGettingResults = false
|
||||
})
|
||||
},
|
||||
handleResize () {
|
||||
if (this.view === 'chart') {
|
||||
// hack react-chart editor: hidden and show in order to make the graph resize
|
||||
this.view = 'not chart'
|
||||
this.$nextTick(() => {
|
||||
this.view = 'chart'
|
||||
})
|
||||
}
|
||||
this.calculateTableHeight()
|
||||
},
|
||||
calculateTableHeight () {
|
||||
const bottomPane = this.$refs.bottomPane
|
||||
// 88 - view swittcher height
|
||||
// 42 - table footer width
|
||||
// 30 - desirable space after the table
|
||||
// 5 - padding-bottom of rounded table container
|
||||
// 40 - height of table header
|
||||
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
|
||||
this.tableViewHeight = freeSpace - (freeSpace % 40)
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
return this.$refs.chart.getChartSatateForSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tab-content-container {
|
||||
background-color: var(--color-white);
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
#bottomPane {
|
||||
height: 100%;
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.query-results-splitter {
|
||||
height: calc(100vh - 104px);
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.query-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
box-sizing: border-box;
|
||||
min-height: 190px;
|
||||
}
|
||||
|
||||
.table-view {
|
||||
margin: 0 52px;
|
||||
height: calc(100% - 88px);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-preview {
|
||||
position: absolute;
|
||||
top: 40%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.table-preview.error {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.table-preview.error::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
</style>
|
||||
@@ -1,22 +1,46 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="['text-field-label', { error: errorMsg }]">{{ label }}</div>
|
||||
<div
|
||||
v-if="label"
|
||||
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
|
||||
>
|
||||
{{ label }}
|
||||
<hint-icon
|
||||
v-if="hint"
|
||||
class="hint"
|
||||
:hint="hint"
|
||||
:maxWidth="maxHintWidth || '149px'"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
:class="{ error: errorMsg }"
|
||||
:style="{ width: width }"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:value="modelValue"
|
||||
:disabled="disabled"
|
||||
@input="$emit('update:modelValue', $event.target.value)"
|
||||
/>
|
||||
<div class="text-field-error">{{ errorMsg }}</div>
|
||||
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HintIcon from '@/components/svg/hint'
|
||||
export default {
|
||||
name: 'textField',
|
||||
props: ['placeholder', 'label', 'errorMsg', 'value', 'width']
|
||||
name: 'TextField',
|
||||
components: { HintIcon },
|
||||
props: {
|
||||
placeholder: String,
|
||||
label: String,
|
||||
errorMsg: String,
|
||||
modelValue: String,
|
||||
width: String,
|
||||
hint: String,
|
||||
maxHintWidth: String,
|
||||
disabled: Boolean
|
||||
},
|
||||
emits: ['update:modelValue']
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,6 +54,7 @@ input {
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
@@ -40,24 +65,47 @@ input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border-color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-base);
|
||||
padding-left: 8px;
|
||||
margin-bottom: 2px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-field-label .hint {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -22px;
|
||||
}
|
||||
|
||||
.text-field-label.error {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label.disabled {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.text-field-error {
|
||||
color: var(--color-text-error);
|
||||
font-size: 12px;
|
||||
padding-left: 8px;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
.text-field-error:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
<template>
|
||||
<div class="view-switcher">
|
||||
<div
|
||||
:class="['table-mode', {'active-mode': view === 'table'}]"
|
||||
@click="$emit('update:view','table')"
|
||||
>
|
||||
Table
|
||||
</div>
|
||||
<div
|
||||
:class="['chart-mode', {'active-mode': view === 'chart'}]"
|
||||
@click="$emit('update:view','chart')"
|
||||
>
|
||||
Chart
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ViewSwitcher',
|
||||
props: ['view']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.view-switcher {
|
||||
height: 28px;
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
.view-switcher div {
|
||||
height: 100%;
|
||||
width: 136px;
|
||||
box-sizing: border-box;
|
||||
line-height: 28px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-base);
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
}
|
||||
.view-switcher div:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
.view-switcher div.active-mode {
|
||||
background: var(--color-accent);
|
||||
border: 1px solid var(--color-accent-shade);
|
||||
color: var(--color-text-light);
|
||||
text-shadow: var(--shadow);
|
||||
z-index: 1;
|
||||
font-weight: 600;
|
||||
}
|
||||
.view-switcher div.active-mode:hover {
|
||||
background: var(--color-accent-shade);
|
||||
}
|
||||
.table-mode {
|
||||
border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
|
||||
}
|
||||
.chart-mode {
|
||||
margin-left: -1px;
|
||||
border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
|
||||
}
|
||||
</style>
|
||||
69
src/components/svg/addTable.vue
Normal file
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<g clip-path="url(#clip0)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
:d="
|
||||
`M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
|
||||
1.77485 15.1649 2.59985
|
||||
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
|
||||
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
|
||||
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
|
||||
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
|
||||
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.` +
|
||||
'1717H13.1702V10.8434Z'
|
||||
"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="18" height="18" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||
Add new table from CSV, JSON or NDJSON
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'AddTableIcon',
|
||||
mixins: [tooltipMixin],
|
||||
props: { tooltip: String },
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
display: block;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
19
src/components/svg/arrow.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<svg
|
||||
width="28"
|
||||
height="27"
|
||||
viewBox="0 0 28 27"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
|
||||
13.5L16.35 6.75L17.9475 8.33625Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
@@ -1,32 +1,45 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg
|
||||
class="db-edit-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<path
|
||||
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12 9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3 6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75 8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995 9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Change database
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<svg
|
||||
class="db-edit-icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<path
|
||||
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3
|
||||
10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12
|
||||
9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3
|
||||
6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75
|
||||
8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995
|
||||
9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78
|
||||
8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||
Load another database, CSV, JSON or NDJSON
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'changeDbIcon',
|
||||
mixins: [tooltipMixin]
|
||||
name: 'ChangeDbIcon',
|
||||
mixins: [tooltipMixin],
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
47
src/components/svg/chart.vue
Normal file
@@ -0,0 +1,47 @@
|
||||
<template>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M8.41943 16V10H10.4194V16H8.41943Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M2.41943 16L2.41943 10H4.41943V16H2.41943Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M11.4194 16V7H13.4194V16H11.4194Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.4194 16V8H16.4194V16H14.4194Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.41943 12V16H5.41943V12H7.41943Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M12.4332 1.80676L16.6265 6.00001L15.9194 6.70712L12.4055 3.19326L5.93169 9.1691L1.71436
|
||||
5.55424L2.36515 4.79499L5.90707 7.83092L12.4332 1.80676Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ChartIcon'
|
||||
}
|
||||
</script>
|
||||
46
src/components/svg/clear.vue
Normal file
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="['clear-icon', { disabled: disabled }]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M14.3481 14.849C13.8791 15.318 13.1191 15.318 12.6511 14.849L10.0001 11.819L7.34907
|
||||
14.848C6.88007 15.317 6.12007 15.317 5.65207 14.848C5.18307 14.379 5.18307 13.619
|
||||
5.65207 13.151L8.41007 10.001L5.65107 6.84898C5.18207 6.37998 5.18207 5.62098 5.65107
|
||||
5.15198C6.12007 4.68298 6.87907 4.68298 7.34807 5.15198L10.0001 8.18298L12.6511
|
||||
5.15198C13.1201 4.68298 13.8791 4.68298 14.3481 5.15198C14.8171 5.62098 14.8171 6.38098
|
||||
14.3481 6.84898L11.5901 10.001L14.3481 13.151C14.8171 13.62 14.8171 14.38 14.3481
|
||||
14.849V14.849Z"
|
||||
fill="#C8D4E3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClearIcon',
|
||||
props: { disabled: Boolean }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clear-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-icon:hover path {
|
||||
fill: var(--color-danger);
|
||||
}
|
||||
|
||||
.disabled.clear-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled.clear-icon:hover path {
|
||||
fill: #c8d4e3;
|
||||
}
|
||||
</style>
|
||||
27
src/components/svg/clipboard.vue
Normal file
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path
|
||||
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
|
||||
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
|
||||
3.14291V16.2422C2.07422 17.2115 2.86276 18 3.83203 18H14.1917C15.161 18 15.9495 17.2115
|
||||
15.9495 16.2422V3.14291C15.9495 2.17365 15.1609 1.3851 14.1917 1.3851V1.3851ZM11.0744
|
||||
1.40625V2.77034H6.94899C6.94899 2.30301 6.94899 1.91698 6.94899 1.40625H11.0744ZM14.5433
|
||||
16.2422C14.5433 16.4361 14.3855 16.5938 14.1917 16.5938H3.83203C3.63812 16.5938 3.48047
|
||||
16.4361 3.48047 16.2422V3.14291C3.48047 2.94914 3.63812 2.79135 3.83203
|
||||
2.79135H5.54274V3.47346C5.54274 3.86183 5.85764 4.17659 6.24586 4.17659H11.7775C12.1657
|
||||
4.17659 12.4806 3.86183 12.4806 3.47346V2.79135H14.1917C14.3855 2.79135 14.5433 2.94914
|
||||
14.5433 3.14291V16.2422ZM12.2745 8.20967C12.5491 8.48433 12.5491 8.92955 12.2745
|
||||
9.20407L8.5795 12.899C8.30498 13.1737 7.85976 13.1737 7.5851 12.899L5.74887
|
||||
11.0628C5.47435 10.7883 5.47435 10.3431 5.74887 10.0684C6.02353 9.7939 6.46861 9.7939
|
||||
6.74327 10.0684L8.08237 11.4075L11.2801 8.20981C11.5547 7.93515 11.9998 7.93515 12.2745
|
||||
8.20967V8.20967Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ClipboardIcon'
|
||||
}
|
||||
</script>
|
||||
@@ -1,15 +1,16 @@
|
||||
<template>
|
||||
<svg
|
||||
@click.stop="$emit('click')"
|
||||
class="icon"
|
||||
:class="['icon', { disabled: disabled }]"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
>
|
||||
<path
|
||||
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z"
|
||||
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14
|
||||
12.59L8.41 7L14 1.41Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
@@ -23,8 +24,14 @@ export default {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 14
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
},
|
||||
emits: ['click']
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -32,6 +39,9 @@ export default {
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
.icon:hover path {
|
||||
fill: var(--color-text-base);
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<path
|
||||
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Duplicate query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'CopyIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
margin: 0 12px;
|
||||
}
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
32
src/components/svg/dataView.vue
Normal file
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M13.5552 6.91522C13.7584 6.91357 13.9535 6.99442 14.096 7.13926L17.2582 10.3541C17.5486
|
||||
10.6494 17.5447 11.1242 17.2494 11.4147C16.9541 11.7051 16.4793 11.7012 16.1888
|
||||
11.4059L13.57 8.74357L9.29577 13.2318C9.01977 13.5216 8.56484 13.5436 8.2621
|
||||
13.2819L5.35435 10.7677L2.03285 13.7321C1.72382 14.0079 1.24971 13.981 0.973901
|
||||
13.6719C0.69809 13.3629 0.725022 12.8888 1.03406 12.613L4.8471 9.20986C5.12827 8.95892
|
||||
5.55198 8.95559 5.83705 9.20208L8.70249 11.6797L13.0182 7.14796C13.1583 7.00084 13.3521
|
||||
6.91686 13.5552 6.91522Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6" />
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M16.036 1.21788L1.96546 1.213C1.14046 1.213 0.465462 1.888 0.465462 2.713L0.464111
|
||||
15.2821C0.464111 16.1071 1.13911 16.7821 1.96411 16.7821L16.0347 16.787C16.8674 16.787
|
||||
17.5347 16.1121 17.5347 15.287L17.536 2.71788C17.536 1.88787 16.866 1.21788 16.036
|
||||
1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DataViewIcon'
|
||||
}
|
||||
</script>
|
||||
@@ -1,43 +0,0 @@
|
||||
<template>
|
||||
<span>
|
||||
<svg
|
||||
class="icon"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
>
|
||||
<path
|
||||
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Delete query
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
|
||||
export default {
|
||||
name: 'DeleteIcon',
|
||||
mixins: [tooltipMixin]
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
40
src/components/svg/dropDownChevron.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="['chevron-icon', { disabled: disabled }]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M6.175 7.15002L10 10.975L13.825 7.15002L15 8.33336L10 13.3334L5 8.33336L6.175 7.15002Z"
|
||||
fill="#C8D4E3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'DropDownChevron',
|
||||
props: { disabled: Boolean }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chevron-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chevron-icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.disabled.chevron-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled.chevron-icon:hover path {
|
||||
fill: #c8d4e3;
|
||||
}
|
||||
</style>
|
||||
25
src/components/svg/edgeArrow.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<svg
|
||||
width="27"
|
||||
height="27"
|
||||
viewBox="0 0 27 27"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
|
||||
13.5L15.7499 6.75L17.3474 8.33625Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {}
|
||||
</script>
|
||||
@@ -6,35 +6,48 @@
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<path
|
||||
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
|
||||
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5
|
||||
15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425
|
||||
12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Export query to file
|
||||
</span>
|
||||
</svg>
|
||||
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'ExportIcon',
|
||||
mixins: [tooltipMixin]
|
||||
mixins: [tooltipMixin],
|
||||
props: {
|
||||
tooltip: String,
|
||||
tooltipPosition: String
|
||||
},
|
||||
emits: ['click'],
|
||||
methods: {
|
||||
onClick() {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
|
||||
56
src/components/svg/exportToCsv.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
|
||||
<path
|
||||
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
|
||||
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
|
||||
1.93115 15.5561 1.44775 15.0082C0.964355 14.4604 0.722656 13.687 0.722656
|
||||
12.6879V12.3715C0.722656 11.7446 0.831055 11.1967 1.04785 10.728C1.26758 10.2563 1.58252
|
||||
9.89301 1.99268 9.63812C2.40283 9.38031 2.87744 9.2514 3.4165 9.2514C4.19287 9.2514
|
||||
4.81689 9.45648 5.28857 9.86664C5.76025 10.2739 6.02832 10.8364 6.09277
|
||||
11.5541H4.55469C4.54297 11.1645 4.44482 10.8847 4.26025 10.7148C4.07568 10.5449 3.79443
|
||||
10.4599 3.4165 10.4599C3.03271 10.4599 2.75146 10.6035 2.57275 10.8906C2.39404 11.1777
|
||||
2.30029 11.6362 2.2915 12.2661V12.7187C2.2915 13.4013 2.37646 13.8891 2.54639
|
||||
14.1821C2.71924 14.475 3.01514 14.6215 3.43408 14.6215C3.78857 14.6215 4.05957 14.538
|
||||
4.24707 14.371C4.43457 14.204 4.53418 13.9389 4.5459 13.5756H6.07959ZM10.1094
|
||||
14.0414C10.1094 13.8159 10.0288 13.6401 9.86768 13.5141C9.70947 13.3881 9.42969 13.2578
|
||||
9.02832 13.123C8.62695 12.9882 8.29883 12.8578 8.04395 12.7319C7.21484 12.3246 6.80029
|
||||
11.7651 6.80029 11.0532C6.80029 10.6987 6.90283 10.3867 7.10791 10.1171C7.31592 9.84467
|
||||
7.60889 9.63373 7.98682 9.48431C8.36475 9.33197 8.78955 9.2558 9.26123 9.2558C9.72119
|
||||
9.2558 10.1328 9.33783 10.4961 9.50189C10.8623 9.66595 11.1465 9.90033 11.3486
|
||||
10.205C11.5508 10.5068 11.6519 10.8525 11.6519 11.2421H10.1138C10.1138 10.9814 10.0332
|
||||
10.7792 9.87207 10.6357C9.71387 10.4921 9.49854 10.4203 9.22607 10.4203C8.95068 10.4203
|
||||
8.73242 10.4819 8.57129 10.6049C8.41309 10.725 8.33398 10.8788 8.33398 11.0663C8.33398
|
||||
11.2304 8.42188 11.3798 8.59766 11.5146C8.77344 11.6464 9.08252 11.7841 9.5249
|
||||
11.9277C9.96729 12.0683 10.3306 12.2206 10.6147 12.3847C11.3062 12.7831 11.6519 13.3325
|
||||
11.6519 14.0327C11.6519 14.5922 11.4409 15.0317 11.019 15.351C10.5972 15.6703 10.0186
|
||||
15.83 9.2832 15.83C8.76465 15.83 8.29443 15.7377 7.87256 15.5532C7.45361 15.3657 7.13721
|
||||
15.1108 6.92334 14.7885C6.7124 14.4633 6.60693 14.0898 6.60693 13.6679H8.15381C8.15381
|
||||
14.0107 8.2417 14.2641 8.41748 14.4282C8.59619 14.5893 8.88477 14.6699 9.2832
|
||||
14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407
|
||||
10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463
|
||||
9.34369H13.6558L14.9258 14.0019Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
|
||||
2.20845 0.991333 3.03345 0.991333Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M15.917 0.991333H14.0517V2.49133H15.917V7.93074H17.417V2.49133C17.417 1.66633 16.742
|
||||
0.991333 15.917 0.991333Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M9.53043 1.28245e-05L13.2199 3.83975L10.9929 3.7953L10.9337 6.76455L7.96442
|
||||
6.70529L8.02368 3.73603L5.6907 3.68947L9.53043 1.28245e-05Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExportToCsvIcon'
|
||||
}
|
||||
</script>
|
||||
55
src/components/svg/exportToSvg.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
|
||||
<path
|
||||
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
|
||||
13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798
|
||||
0.974609 11.7203 0.974609 11.0084C0.974609 10.6539 1.07715 10.3419 1.28223
|
||||
10.0723C1.49023 9.79987 1.7832 9.58893 2.16113 9.43951C2.53906 9.28717 2.96387 9.211
|
||||
3.43555 9.211C3.89551 9.211 4.30713 9.29303 4.67041 9.45709C5.03662 9.62115 5.3208
|
||||
9.85553 5.52295 10.1602C5.7251 10.462 5.82617 10.8077 5.82617 11.1973H4.28809C4.28809
|
||||
10.9366 4.20752 10.7344 4.04639 10.5909C3.88818 10.4473 3.67285 10.3755 3.40039
|
||||
10.3755C3.125 10.3755 2.90674 10.4371 2.74561 10.5601C2.5874 10.6802 2.5083 10.834
|
||||
2.5083 11.0215C2.5083 11.1856 2.59619 11.335 2.77197 11.4698C2.94775 11.6016 3.25684
|
||||
11.7393 3.69922 11.8829C4.1416 12.0235 4.50488 12.1758 4.78906 12.3399C5.48047 12.7383
|
||||
5.82617 13.2877 5.82617 13.9879C5.82617 14.5474 5.61523 14.9869 5.19336 15.3062C4.77148
|
||||
15.6255 4.19287 15.7852 3.45752 15.7852C2.93896 15.7852 2.46875 15.6929 2.04688
|
||||
15.5084C1.62793 15.3209 1.31152 15.066 1.09766 14.7437C0.886719 14.4185 0.78125 14.045
|
||||
0.78125 13.6231H2.32812C2.32812 13.9659 2.41602 14.2193 2.5918 14.3834C2.77051 14.5445
|
||||
3.05908 14.6251 3.45752 14.6251C3.7124 14.6251 3.91309 14.5709 4.05957 14.4625C4.20898
|
||||
14.3511 4.28369 14.1959 4.28369 13.9966ZM9.1001 13.9571L10.3745 9.29889H12.0972L9.94385
|
||||
15.6973H8.25635L6.12061 9.29889H7.83008L9.1001 13.9571ZM17.7441 14.9063C17.5068 15.17
|
||||
17.1597 15.3824 16.7026 15.5435C16.2456 15.7047 15.7446 15.7852 15.1997 15.7852C14.3618
|
||||
15.7852 13.6924 15.5289 13.1914 15.0162C12.6904 14.5035 12.4224 13.7901 12.3872
|
||||
12.876L12.3828 12.3223C12.3828 11.6924 12.4941 11.1431 12.7168 10.6744C12.9395 10.2027
|
||||
13.2573 9.84088 13.6704 9.58893C14.0864 9.33405 14.5669 9.2066 15.1118 9.2066C15.9087
|
||||
9.2066 16.5269 9.38971 16.9663 9.75592C17.4087 10.1192 17.6665 10.6627 17.7397
|
||||
11.3863H16.2544C16.2017 11.0289 16.0874 10.774 15.9116 10.6216C15.7358 10.4693 15.4868
|
||||
10.3931 15.1646 10.3931C14.7778 10.3931 14.479 10.5572 14.2681 10.8853C14.0571 11.2134
|
||||
13.9502 11.6822 13.9473 12.2916V12.6783C13.9473 13.317 14.0557 13.7974 14.2725
|
||||
14.1197C14.4922 14.439 14.8364 14.5987 15.3052 14.5987C15.7065 14.5987 16.0054 14.5093
|
||||
16.2017 14.3306V13.3375H15.1294V12.2784H17.7441V14.9063Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M2.86768 0.991333H4.73292V2.49133H2.86768V7.93074H1.36768V2.49133C1.36768 1.66633
|
||||
2.04268 0.991333 2.86768 0.991333Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M15.7512 0.991333H13.886V2.49133H15.7512V7.93074H17.2512V2.49133C17.2512 1.66633 16.5762
|
||||
0.991333 15.7512 0.991333Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
d="M9.36466 -1.76931e-05L13.0541 3.83972L10.8272 3.79527L10.7679 6.76452L7.79865
|
||||
6.70526L7.85791 3.736L5.52492 3.68944L9.36466 -1.76931e-05Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'ExportToSvgIcon'
|
||||
}
|
||||
</script>
|
||||