Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a59946c09d | ||
|
|
7b06b3d9c8 | ||
|
|
ced933f497 | ||
|
|
cda368f109 | ||
|
|
df67466c2f | ||
|
|
528549ae5a | ||
|
|
20f4dcc645 | ||
|
|
b8353ef0ce | ||
|
|
7975f419c9 | ||
|
|
72aa0dd80b | ||
|
|
e000ee71fc | ||
|
|
b6a12668d3 | ||
|
|
713f5ac768 | ||
|
|
5492609c3a | ||
|
|
8bfd0f5944 | ||
|
|
a8006bcf52 | ||
|
|
1463f93bb0 | ||
|
|
5108495430 | ||
|
|
d28968e539 | ||
|
|
68221cba6d | ||
|
|
65c1c18fcb | ||
|
|
d7db6a0f5d | ||
|
|
0a2af0bba3 | ||
|
|
e4b35bac0a | ||
|
|
3d1e822cdc | ||
|
|
3d6479be7a | ||
|
|
218ab52ab3 | ||
|
|
f178937440 | ||
|
|
411bd694c0 | ||
|
|
d2969de127 | ||
|
|
b59c21c14e | ||
|
|
4ed4b54a28 | ||
|
|
2c2bb7d6d3 | ||
|
|
efbd985b36 | ||
|
|
9cf7d0e5dc | ||
|
|
0a8c09b58d | ||
|
|
931cf380bc | ||
|
|
f0f96ac663 | ||
|
|
45530cc9d6 | ||
|
|
6fbf75b601 | ||
|
|
d3fbf08569 | ||
|
|
be6a19a30f | ||
|
|
07d7a9d54b | ||
|
|
cdd925b8af | ||
|
|
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 |
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -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']
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -7,34 +7,39 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: Update npm
|
||||||
|
run: npm install -g npm@10
|
||||||
|
|
||||||
- name: npm install and build
|
- name: npm install and build
|
||||||
run: |
|
run: |
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
- name: Create archive
|
- name: Create archives
|
||||||
run: |
|
run: |
|
||||||
cd dist
|
cd dist
|
||||||
zip -9 -r dist.zip . -x "js/*.map"
|
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
|
- name: Create release
|
||||||
uses: ncipollo/release-action@v1
|
uses: ncipollo/release-action@v1
|
||||||
with:
|
with:
|
||||||
artifacts: "dist/dist.zip"
|
artifacts: 'dist.zip,dist_map.zip'
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
bodyFile: 'CHANGELOG.md'
|
||||||
- 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
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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 \
|
||||||
|
xvfb
|
||||||
|
|
||||||
|
- 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: xvfb-run -a npm test
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "none",
|
||||||
|
"tabWidth": 2,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true,
|
||||||
|
"arrowParens": "avoid"
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -4,22 +4,50 @@
|
|||||||
|
|
||||||
# sqliteviz
|
# 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, graphs 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'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
|
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
|
||||||
of SQL and richness of Plotly's `react-chart-editor`
|
and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
||||||
2. Visualisation pallete, SQL and chart definitions, kept in local
|
Graphs are visualized with [Sigma.js][13] and [Graphology][14].
|
||||||
storage with import/export
|
|
||||||
3. Integration-side provided pallete
|
|
||||||
|
|
||||||
[1]: https://github.com/plotly/falcon
|
[1]: https://github.com/plotly/falcon
|
||||||
[2]: https://github.com/getredash/redash
|
[2]: https://github.com/getredash/redash
|
||||||
[3]: https://github.com/plotly/react-chart-editor
|
[3]: https://github.com/plotly/react-chart-editor
|
||||||
[4]: https://github.com/sql-js/sql.js
|
[4]: https://github.com/sql-js/sql.js
|
||||||
[5]: https://github.com/vuejs/vue
|
[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
|
||||||
|
[13]: https://www.sigmajs.org/
|
||||||
|
[14]: https://graphology.github.io/
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
presets: [
|
presets: ['@vue/cli-plugin-babel/preset']
|
||||||
'@vue/cli-plugin-babel/preset'
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"include": ["src/**/*", "tests/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"],
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@\/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
|
}
|
||||||
@@ -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/"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
benchmark
|
||||||
|
dist
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
/lib/build-*
|
||||||
|
/lib/dist
|
||||||
|
/build-*-result.json
|
||||||
|
/sample.csv
|
||||||
@@ -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
|
||||||
@@ -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/
|
||||||
@@ -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' }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "sql.js",
|
||||||
|
"main": "./dist/sql-wasm.js",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "sql.js",
|
||||||
|
"main": "./dist/sql-wasm.js",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
@@ -1,52 +1,85 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.8.0",
|
"version": "0.28.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"dev": "vite",
|
||||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
"build": "vite build",
|
||||||
"test:unit": "vue-cli-service test:unit",
|
"serve": "vite preview",
|
||||||
"lint": "vue-cli-service lint"
|
"test": "karma start karma.conf.cjs",
|
||||||
|
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
|
||||||
|
"format": "prettier . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"@sigma/export-image": "^3.0.0",
|
||||||
|
"buffer": "^6.0.3",
|
||||||
|
"codemirror": "^5.65.18",
|
||||||
|
"codemirror-editor-vue3": "^2.8.0",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
"debounce": "^1.2.0",
|
"dataurl-to-blob": "^0.0.1",
|
||||||
|
"graphology": "^0.26.0",
|
||||||
|
"graphology-layout": "^0.6.1",
|
||||||
|
"graphology-layout-forceatlas2": "^0.10.1",
|
||||||
|
"html2canvas": "^1.1.4",
|
||||||
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"plotly.js": "^1.57.1",
|
"papaparse": "^5.4.1",
|
||||||
"react": "^16.13.1",
|
"pivottable": "^2.23.0",
|
||||||
"react-chart-editor": "^0.42.0",
|
"plotly.js": "^2.35.2",
|
||||||
"react-dom": "^16.13.1",
|
"promise-worker": "^2.0.1",
|
||||||
"sql.js": "^1.3.0",
|
"react": "^16.14.0",
|
||||||
"sqlite-parser": "^1.0.1",
|
"react-chart-editor": "^0.46.1",
|
||||||
"vue": "^2.6.11",
|
"react-dom": "^16.14.0",
|
||||||
"vue-codemirror": "^4.0.6",
|
"seedrandom": "^3.0.5",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"sigma": "^3.0.1",
|
||||||
"vue-router": "^3.2.0",
|
"sql.js": "file:./lib/sql-js",
|
||||||
"vuejs-paginate": "^2.1.0",
|
"tiny-emitter": "^2.1.0",
|
||||||
"vuera": "^0.2.7",
|
"veaury": "^2.5.1",
|
||||||
"vuex": "^3.4.0"
|
"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": {
|
"devDependencies": {
|
||||||
"@vue/cli-plugin-babel": "^4.4.0",
|
"@babel/core": "^7.25.7",
|
||||||
"@vue/cli-plugin-eslint": "^4.4.0",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/cli-plugin-router": "^4.4.0",
|
"@vue/eslint-config-standard": "^8.0.1",
|
||||||
"@vue/cli-plugin-vuex": "^4.4.0",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"@vue/cli-service": "^4.4.0",
|
|
||||||
"@vue/eslint-config-standard": "^5.1.2",
|
|
||||||
"@vue/test-utils": "^1.0.3",
|
|
||||||
"babel-eslint": "^10.1.0",
|
|
||||||
"chai": "^4.1.2",
|
"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-import": "^2.20.2",
|
||||||
"eslint-plugin-node": "^11.1.0",
|
"eslint-plugin-node": "^11.1.0",
|
||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^9.28.0",
|
||||||
"karma": "^3.1.4",
|
"flush-promises": "^1.0.2",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma": "^6.4.4",
|
||||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
"karma-coverage": "^2.2.1",
|
||||||
"vue-template-compiler": "^2.6.11"
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
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>
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
[]
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -1,58 +1,45 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<router-view/>
|
<router-view />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
inquiries() {
|
||||||
|
return this.$store.state.inquiries
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
inquiries: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
|
addEventListener('storage', event => {
|
||||||
|
if (event.key === storedInquiries.myInquiriesKey) {
|
||||||
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
|
||||||
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-weight: 600;
|
|
||||||
font-style: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
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-weight: 400;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
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-weight: 700;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app,
|
#app,
|
||||||
|
.dialog,
|
||||||
input,
|
input,
|
||||||
label,
|
label,
|
||||||
button,
|
button,
|
||||||
.plotly_editor * {
|
.plotly_editor * {
|
||||||
font-family: "Open Sans", Helvetica, Arial, sans-serif;
|
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
@@ -60,4 +47,7 @@ button,
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
.CodeMirror-hints {
|
||||||
|
z-index: 999 !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
|
After Width: | Height: | Size: 83 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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);
|
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 {
|
button.secondary {
|
||||||
background: white;
|
background: white;
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -59,5 +51,11 @@ button.toolbar:hover {
|
|||||||
color: var(--color-accent);
|
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 {
|
.dialog {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.dialog .vfm__content {
|
||||||
border-radius: var(--border-radius-big);
|
border-radius: var(--border-radius-big);
|
||||||
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
|
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
|
||||||
|
background-color: white;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-header {
|
.dialog-header {
|
||||||
@@ -16,7 +23,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dialog-body {
|
.dialog-body {
|
||||||
min-height: 60px;
|
min-height: 56px;
|
||||||
background-color: var(--color-bg-light);
|
background-color: var(--color-bg-light);
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
border-top: 1px solid var(--color-border-light);
|
border-top: 1px solid var(--color-border-light);
|
||||||
@@ -35,6 +42,6 @@
|
|||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vm--overlay {
|
.vfm__overlay.vfm--overlay {
|
||||||
background-color: rgba(162, 177, 198, 0.5);
|
background-color: rgba(162, 177, 198, 0.5);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,3 +4,10 @@
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
padding: 0 24px;
|
padding: 0 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-view-warning {
|
||||||
|
height: 40px;
|
||||||
|
line-height: 40px;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
.rounded-bg {
|
||||||
padding: 40px 5px 5px;
|
padding: 35px 5px 5px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: 1px solid var(--color-border-light);
|
border: 1px solid var(--color-border-light);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.straight .rounded-bg {
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -18,7 +24,20 @@
|
|||||||
border-radius: 5px 5px 0 0;
|
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 {
|
.header-container {
|
||||||
top: 0;
|
top: 0;
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
@@ -32,41 +51,47 @@
|
|||||||
}
|
}
|
||||||
.table-container {
|
.table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
table {
|
table.sqliteviz-table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-top: -40px;
|
margin-top: -35px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
thead th, .fixed-header {
|
.sqliteviz-table thead th,
|
||||||
|
.fixed-header {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--color-bg-dark);
|
background-color: var(--color-bg-dark);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
tbody td {
|
.sqliteviz-table tbody td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
background-color:white;
|
background-color: white;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
td, th, .fixed-header {
|
.sqliteviz-table td,
|
||||||
padding: 12px 24px;
|
.sqliteviz-table th,
|
||||||
|
.fixed-header {
|
||||||
|
padding: 8px 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr td:last-child,
|
.sqliteviz-table tbody tr td:last-child,
|
||||||
thead tr th:last-child,
|
.sqliteviz-table thead tr th:last-child,
|
||||||
.header-container div .fixed-header:last-child {
|
.header-container div .fixed-header:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
td > div.cell-data {
|
.sqliteviz-table td > div.cell-data {
|
||||||
width: -webkit-max-content;
|
width: -webkit-max-content;
|
||||||
width: -moz-max-content;
|
width: -moz-max-content;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@@ -83,3 +108,9 @@ td > div.cell-data {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text-base);
|
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 {
|
.icon-tooltip {
|
||||||
visibility: hidden;
|
|
||||||
background-color: rgba(80, 103, 132, 0.85);
|
background-color: rgba(80, 103, 132, 0.85);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 11px;
|
font-size: 12px;
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
line-height: 19px;;
|
line-height: 19px;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 5;
|
|
||||||
height: 19px;
|
height: 19px;
|
||||||
border-radius: var(--border-radius-medium);
|
border-radius: var(--border-radius-medium);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@font-face {
|
||||||
|
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-weight: 600;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
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-weight: 400;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
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-weight: 700;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--color-accent-shade);
|
||||||
|
}
|
||||||
@@ -1,29 +1,33 @@
|
|||||||
:root {
|
:root {
|
||||||
--color-white: #ffffff;
|
--color-white: #ffffff;
|
||||||
--color-gray-light: #F3F6FA;
|
--color-gray-light: #f3f6fa;
|
||||||
--color-gray-light-2: #DFE8F3;
|
--color-gray-light-2: #dfe8f3;
|
||||||
--color-gray-light-3: #C8D4E3;
|
--color-gray-light-3: #c8d4e3;
|
||||||
--color-gray-light-4:#EBF0F8;
|
--color-gray-light-4: #ebf0f8;
|
||||||
--color-gray-light-5:#f8f8f9;
|
--color-gray-light-5: #f8f8f9;
|
||||||
--color-gray-medium: #A2B1C6;
|
--color-gray-medium: #a2b1c6;
|
||||||
--color-gray-dark: #506784;
|
--color-gray-dark: #506784;
|
||||||
--color-blue-medium: #119DFF;
|
--color-blue-medium: #119dff;
|
||||||
--color-blue-dark: #0D76BF;
|
--color-blue-dark: #0d76bf;
|
||||||
--color-blue-dark-2: #2A3F5F;
|
--color-blue-dark-2: #2a3f5f;
|
||||||
--color-red: #EF553B;
|
--color-red: #ef553b;
|
||||||
--color-yellow: #FBEFCB;
|
--color-red-2: #de350b;
|
||||||
|
--color-red-light: #ffbdad;
|
||||||
|
--color-yellow: #fbefcb;
|
||||||
|
|
||||||
--color-bg-light: var(--color-gray-light);
|
--color-bg-light: var(--color-gray-light);
|
||||||
--color-bg-light-2: var(--color-gray-light-2);
|
--color-bg-light-2: var(--color-gray-light-2);
|
||||||
--color-bg-light-3: var(--color-gray-light-5);
|
--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-dark: var(--color-gray-dark);
|
||||||
--color-bg-warning: var(--color-yellow);
|
--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: var(--color-blue-medium);
|
||||||
--color-accent-shade: var(--color-blue-dark);
|
--color-accent-shade: var(--color-blue-dark);
|
||||||
--color-border-light: var(--color-gray-light-2);
|
--color-border-light: var(--color-gray-light-2);
|
||||||
--color-border: var(--color-gray-light-3);
|
--color-border: var(--color-gray-light-3);
|
||||||
|
--color-border-dark: var(--color-gray-medium);
|
||||||
--color-text-light: var(--color-white);
|
--color-text-light: var(--color-white);
|
||||||
--color-text-light-2: var(--color-gray-medium);
|
--color-text-light-2: var(--color-gray-medium);
|
||||||
--color-text-base: var(--color-gray-dark);
|
--color-text-base: var(--color-gray-dark);
|
||||||
@@ -42,6 +46,3 @@
|
|||||||
.plotly-editor--theme-provider {
|
.plotly-editor--theme-provider {
|
||||||
--sidebar-width: 112px;
|
--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>
|
<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" />
|
<div v-show="!checked" class="unchecked" />
|
||||||
<img
|
<img
|
||||||
v-show="checked && theme === 'accent'"
|
v-show="checked && !disabled && theme === 'light'"
|
||||||
:src="require('@/assets/images/checkbox_checked.svg')"
|
class="checked-light"
|
||||||
|
src="~@/assets/images/checkbox_checked_light.svg"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
v-show="checked && theme === 'light'"
|
v-show="checked && !disabled && theme !== 'light'"
|
||||||
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'checkBox',
|
name: 'CheckBox',
|
||||||
props: {
|
props: {
|
||||||
theme: {
|
theme: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: 'accent',
|
default: 'accent',
|
||||||
validator: (value) => {
|
validator: value => {
|
||||||
return ['accent', 'light'].includes(value)
|
return ['accent', 'light'].includes(value)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -28,25 +43,38 @@ export default {
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
data () {
|
emits: ['click'],
|
||||||
|
data() {
|
||||||
return {
|
return {
|
||||||
checked: this.init
|
checked: this.init
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onClick () {
|
onClick() {
|
||||||
|
if (!this.disabled) {
|
||||||
this.checked = !this.checked
|
this.checked = !this.checked
|
||||||
this.$emit('click', this.checked)
|
this.$emit('click', this.checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.checkbox-container {
|
.checkbox-container {
|
||||||
display: inline-block;
|
display: inline-flex;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.unchecked {
|
.unchecked {
|
||||||
@@ -64,4 +92,24 @@ export default {
|
|||||||
img {
|
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>
|
</style>
|
||||||
|
|||||||
@@ -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)' }
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Scaling ratio" fieldContainerClassName="test_fa2_scaling">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scalingRatio"
|
||||||
|
@update="update('scalingRatio', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Prevent overlapping"
|
||||||
|
fieldContainerClassName="test_fa2_adjustSizes"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.adjustSizes"
|
||||||
|
@option-change="update('adjustSizes', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Barnes-Hut optimize"
|
||||||
|
fieldContainerClassName="test_fa2_barnes_hut"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.barnesHutOptimize"
|
||||||
|
@option-change="update('barnesHutOptimize', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-show="modelValue.barnesHutOptimize"
|
||||||
|
label="Barnes-Hut Theta"
|
||||||
|
fieldContainerClassName="test_fa2_barnes_theta"
|
||||||
|
>
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.barnesHutTheta"
|
||||||
|
@update="update('barnesHutTheta', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Strong gravity mode"
|
||||||
|
fieldContainerClassName="test_fa2_strong_gravity"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.strongGravityMode"
|
||||||
|
@option-change="update('strongGravityMode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Noack's LinLog model"
|
||||||
|
fieldContainerClassName="test_fa2_lin_log"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.linLogMode"
|
||||||
|
@option-change="update('linLogMode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
label="Outbound attraction distribution"
|
||||||
|
fieldContainerClassName="test_fa2_outbound_attraction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="booleanOptions"
|
||||||
|
:activeOption="modelValue.outboundAttractionDistribution"
|
||||||
|
@option-change="update('outboundAttractionDistribution', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Slow down" fieldContainerClassName="test_fa2_slow_down">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.slowDown"
|
||||||
|
:min="0"
|
||||||
|
@update="update('slowDown', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge weight">
|
||||||
|
<Dropdown
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.weightSource"
|
||||||
|
className="test_fa2_weight_source"
|
||||||
|
@change="update('weightSource', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<Field
|
||||||
|
v-show="modelValue.weightSource"
|
||||||
|
label="Edge weight influence"
|
||||||
|
fieldContainerClassName="test_fa2_weight_influence"
|
||||||
|
>
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.edgeWeightInfluence"
|
||||||
|
@update="update('edgeWeightInfluence', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
booleanOptions: markRaw([
|
||||||
|
{ label: 'Yes', value: true },
|
||||||
|
{ label: 'No', value: false }
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
update(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<Field
|
||||||
|
label="Hierarchy attributes"
|
||||||
|
fieldContainerClassName="multiselect-field"
|
||||||
|
>
|
||||||
|
<multiselect
|
||||||
|
:modelValue="modelValue.hierarchyAttributes"
|
||||||
|
class="sqliteviz-select"
|
||||||
|
:options="keyOptions"
|
||||||
|
:multiple="true"
|
||||||
|
:hideSelected="true"
|
||||||
|
:closeOnSelect="true"
|
||||||
|
:showLabels="false"
|
||||||
|
:max="keyOptions.length"
|
||||||
|
placeholder=""
|
||||||
|
openDirection="bottom"
|
||||||
|
@update:model-value="update('hierarchyAttributes', $event)"
|
||||||
|
>
|
||||||
|
<template #maxElements>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #placeholder>Select an Option</template>
|
||||||
|
|
||||||
|
<template #noResult>
|
||||||
|
<span class="no-results">No Results</span>
|
||||||
|
</template>
|
||||||
|
</multiselect>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Seed value">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.seedValue"
|
||||||
|
@update="update('seedValue', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Multiselect from 'vue-multiselect'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
Multiselect
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
methods: {
|
||||||
|
update(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.sqliteviz-select.multiselect--active .multiselect__input) {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
:deep(.multiselect-field .field__widget > *) {
|
||||||
|
flex-grow: 1 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Color" fieldContainerClassName="test_edge_color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeColorTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateColorType"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
fieldContainerClassName="test_edge_color_value"
|
||||||
|
>
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="modelValue.value"
|
||||||
|
@color-change="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field fieldContainerClassName="test_edge_color_value">
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field fieldContainerClassName="test_edge_color_mapping_mode">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="modelValue.sourceUsage"
|
||||||
|
@option-change="updateSettings('sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field v-if="modelValue.sourceUsage === 'map_to'">
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="modelValue.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateSettings('colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
||||||
|
label="Color as"
|
||||||
|
fieldContainerClassName="test_edge_color_as"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="modelValue.type !== 'constant' && modelValue.sourceUsage === 'map_to'"
|
||||||
|
label="Colorscale direction"
|
||||||
|
fieldContainerClassName="test_edge_color_colorscale_direction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
@option-change="updateSettings('colorscaleDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
edgeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
сolorAsOptions: markRaw([
|
||||||
|
{ label: 'Continious', value: 'continious' },
|
||||||
|
{ label: 'Categorical', value: 'categorical' }
|
||||||
|
]),
|
||||||
|
сolorscaleDirections: markRaw([
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Recersed', value: 'reversed' }
|
||||||
|
]),
|
||||||
|
colorSourceUsageOptions: markRaw([
|
||||||
|
{ label: 'Direct', value: 'direct' },
|
||||||
|
{ label: 'Map to', value: 'map_to' }
|
||||||
|
]),
|
||||||
|
defaultColorSettings: {
|
||||||
|
constant: { value: '#1F77B4' },
|
||||||
|
variable: {
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'categorical',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
updateColorType(newColorType) {
|
||||||
|
const currentColorType = this.modelValue.type
|
||||||
|
this.defaultColorSettings[currentColorType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newColorType,
|
||||||
|
...this.defaultColorSettings[newColorType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Size" fieldContainerClassName="test_edge_size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="edgeSizeTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateSizeType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field fieldContainerClassName="test_edge_size_value">
|
||||||
|
<NumericInput
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
:value="modelValue.value"
|
||||||
|
:min="1"
|
||||||
|
@update="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="modelValue.type !== 'constant'">
|
||||||
|
<Field label="Size scale" fieldContainerClassName="test_edge_size_scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scale"
|
||||||
|
@update="updateSettings('scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size" fieldContainerClassName="test_edge_size_min">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.min"
|
||||||
|
@update="updateSettings('min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
edgeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' }
|
||||||
|
]),
|
||||||
|
defaultSizeSettings: {
|
||||||
|
constant: { value: 4 },
|
||||||
|
variable: { source: null, scale: 1, min: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateSizeType(newSizeType) {
|
||||||
|
const currentSizeType = this.modelValue.type
|
||||||
|
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newSizeType,
|
||||||
|
...this.defaultSizeSettings[newSizeType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<Field
|
||||||
|
label="Initial iterations"
|
||||||
|
fieldContainerClassName="test_fa2_iteration_amount"
|
||||||
|
>
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.initialIterationsAmount"
|
||||||
|
:min="1"
|
||||||
|
@update="update('initialIterationsAmount', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Gravity" fieldContainerClassName="test_fa2_gravity">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.gravity"
|
||||||
|
@update="update('gravity', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
methods: {
|
||||||
|
update(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,672 @@
|
|||||||
|
<template>
|
||||||
|
<div :class="['plotly_editor', { with_controls: showViewSettings }]">
|
||||||
|
<GraphEditorControls v-show="showViewSettings">
|
||||||
|
<PanelMenuWrapper>
|
||||||
|
<Panel group="Structure" name="Graph">
|
||||||
|
<Fold name="Graph">
|
||||||
|
<Field>
|
||||||
|
Map your result set records to node and edge properties required
|
||||||
|
to build a graph. Learn more about result set requirements in the
|
||||||
|
<a href="https://sqliteviz.com/docs/graph/" target="_blank">
|
||||||
|
documentation</a
|
||||||
|
>.
|
||||||
|
</Field>
|
||||||
|
<Field ref="objectTypeField" label="Object type">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.objectType"
|
||||||
|
className="test_object_type_select"
|
||||||
|
@change="updateStructure('objectType', $event)"
|
||||||
|
/>
|
||||||
|
<Field>
|
||||||
|
A field indicating if the record is node (value 0) or edge
|
||||||
|
(value 1).
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Node Id">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.nodeId"
|
||||||
|
className="test_node_id_select"
|
||||||
|
@change="updateStructure('nodeId', $event)"
|
||||||
|
/>
|
||||||
|
<Field> A field keeping unique node identifier. </Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge source">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.edgeSource"
|
||||||
|
className="test_edge_source_select"
|
||||||
|
@change="updateStructure('edgeSource', $event)"
|
||||||
|
/>
|
||||||
|
<Field>
|
||||||
|
A field keeping a node identifier where the edge starts.
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Edge target">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.structure.edgeTarget"
|
||||||
|
className="test_edge_target_select"
|
||||||
|
@change="updateStructure('edgeTarget', $event)"
|
||||||
|
/>
|
||||||
|
<Field>
|
||||||
|
A field keeping a node identifier where the edge ends.
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
<Panel group="Style" name="General">
|
||||||
|
<Fold name="General">
|
||||||
|
<Field label="Background color">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.backgroundColor"
|
||||||
|
@color-change="settings.style.backgroundColor = $event"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
<Panel group="Style" name="Nodes">
|
||||||
|
<Fold name="Nodes">
|
||||||
|
<Field label="Label">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.nodes.label.source"
|
||||||
|
className="test_label_select"
|
||||||
|
@change="updateNodes('label.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Label color">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.nodes.label.color"
|
||||||
|
@color-change="updateNodes('label.color', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<NodeSizeSettings
|
||||||
|
v-model="settings.style.nodes.size"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateNodes('size', $event)"
|
||||||
|
/>
|
||||||
|
<NodeColorSettings
|
||||||
|
v-model="settings.style.nodes.color"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateNodes('color', $event)"
|
||||||
|
/>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<Panel group="Style" name="Edges">
|
||||||
|
<Fold name="Edges">
|
||||||
|
<Field
|
||||||
|
label="Direction"
|
||||||
|
fieldContainerClassName="test_edge_direction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="visibilityOptions"
|
||||||
|
:activeOption="settings.style.edges.showDirection"
|
||||||
|
@option-change="updateEdges('showDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Label">
|
||||||
|
<Dropdown
|
||||||
|
:options="keysOptions"
|
||||||
|
:value="settings.style.edges.label.source"
|
||||||
|
className="test_edge_label_select"
|
||||||
|
@change="updateEdges('label.source', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Label color">
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="settings.style.edges.label.color"
|
||||||
|
@color-change="updateEdges('label.color', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<EdgeSizeSettings
|
||||||
|
v-model="settings.style.edges.size"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateEdges('size', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EdgeColorSettings
|
||||||
|
v-model="settings.style.edges.color"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateEdges('color', $event)"
|
||||||
|
/>
|
||||||
|
</Fold>
|
||||||
|
</Panel>
|
||||||
|
<Panel group="Style" name="Layout">
|
||||||
|
<Fold name="Layout">
|
||||||
|
<Field label="Algorithm">
|
||||||
|
<Dropdown
|
||||||
|
:options="layoutOptions"
|
||||||
|
:value="settings.layout.type"
|
||||||
|
:clearable="false"
|
||||||
|
className="test_layout_algorithm_select"
|
||||||
|
@change="updateLayout($event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<component
|
||||||
|
:is="layoutSettingsComponentMap[settings.layout.type]"
|
||||||
|
v-if="settings.layout.type !== 'circular'"
|
||||||
|
v-model="settings.layout.options"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateLayout(settings.layout.type)"
|
||||||
|
/>
|
||||||
|
</Fold>
|
||||||
|
<template v-if="settings.layout.type === 'forceAtlas2'">
|
||||||
|
<Fold name="Advanced layout settings">
|
||||||
|
<AdvancedForceAtlasLayoutSettings
|
||||||
|
v-model="settings.layout.options"
|
||||||
|
:keyOptions="keysOptions"
|
||||||
|
@update:model-value="updateLayout(settings.layout.type)"
|
||||||
|
/>
|
||||||
|
</Fold>
|
||||||
|
<div class="force-atlas-buttons">
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
class="test_fa2_reset"
|
||||||
|
@click="resetFA2LayoutSettings"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
class="test_fa2_toggle"
|
||||||
|
@click="toggleFA2Layout"
|
||||||
|
>
|
||||||
|
<template #node:icon>
|
||||||
|
<div
|
||||||
|
:style="{
|
||||||
|
padding: '0 3px'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<RunIcon v-if="!fa2Running" />
|
||||||
|
<StopIcon v-else />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
{{ fa2Running ? 'Stop' : 'Start' }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Panel>
|
||||||
|
</PanelMenuWrapper>
|
||||||
|
</GraphEditorControls>
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref="graph"
|
||||||
|
class="test_graph_output"
|
||||||
|
:style="{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%',
|
||||||
|
backgroundColor: settings.style.backgroundColor
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import GraphEditorControls from '@/lib/GraphEditorControls.jsx'
|
||||||
|
import { PanelMenuWrapper, Panel, Fold, Section } from 'react-chart-editor'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Button from 'react-chart-editor/lib/components/widgets/Button'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import RandomLayoutSettings from '@/components/Graph/RandomLayoutSettings.vue'
|
||||||
|
import ForceAtlasLayoutSettings from '@/components/Graph/ForceAtlasLayoutSettings.vue'
|
||||||
|
// eslint-disable-next-line max-len
|
||||||
|
import AdvancedForceAtlasLayoutSettings from '@/components/Graph/AdvancedForceAtlasLayoutSettings.vue'
|
||||||
|
import CirclePackLayoutSettings from '@/components/Graph/CirclePackLayoutSettings.vue'
|
||||||
|
import FA2Layout from 'graphology-layout-forceatlas2/worker'
|
||||||
|
import * as forceAtlas2 from 'graphology-layout-forceatlas2'
|
||||||
|
import RunIcon from '@/components/svg/run.vue'
|
||||||
|
import StopIcon from '@/components/svg/stop.vue'
|
||||||
|
import { downloadAsPNG, drawOnCanvas } from '@sigma/export-image'
|
||||||
|
import {
|
||||||
|
buildNodes,
|
||||||
|
buildEdges,
|
||||||
|
updateNodes,
|
||||||
|
updateEdges
|
||||||
|
} from '@/lib/graphHelper'
|
||||||
|
import Graph from 'graphology'
|
||||||
|
import { circular, random, circlepack } from 'graphology-layout'
|
||||||
|
import Sigma from 'sigma'
|
||||||
|
import seedrandom from 'seedrandom'
|
||||||
|
import NodeColorSettings from '@/components/Graph/NodeColorSettings.vue'
|
||||||
|
import NodeSizeSettings from '@/components/Graph/NodeSizeSettings.vue'
|
||||||
|
import EdgeSizeSettings from '@/components/Graph/EdgeSizeSettings.vue'
|
||||||
|
import EdgeColorSettings from '@/components/Graph/EdgeColorSettings.vue'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
GraphEditorControls: applyPureReactInVue(GraphEditorControls),
|
||||||
|
PanelMenuWrapper: applyPureReactInVue(PanelMenuWrapper),
|
||||||
|
Panel: applyPureReactInVue(Panel),
|
||||||
|
PanelSection: applyPureReactInVue(Section),
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
Fold: applyPureReactInVue(Fold),
|
||||||
|
Button: applyPureReactInVue(Button),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker),
|
||||||
|
RunIcon,
|
||||||
|
StopIcon,
|
||||||
|
RandomLayoutSettings,
|
||||||
|
CirclePackLayoutSettings,
|
||||||
|
NodeColorSettings,
|
||||||
|
NodeSizeSettings,
|
||||||
|
EdgeSizeSettings,
|
||||||
|
EdgeColorSettings,
|
||||||
|
AdvancedForceAtlasLayoutSettings
|
||||||
|
},
|
||||||
|
inject: ['tabLayout'],
|
||||||
|
props: {
|
||||||
|
dataSources: Object,
|
||||||
|
initOptions: Object,
|
||||||
|
showViewSettings: Boolean
|
||||||
|
},
|
||||||
|
emits: ['update'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
graph: new Graph({ multi: true, allowSelfLoops: true }),
|
||||||
|
renderer: null,
|
||||||
|
fa2Layout: null,
|
||||||
|
fa2Running: false,
|
||||||
|
checkIteration: null,
|
||||||
|
visibilityOptions: markRaw([
|
||||||
|
{ label: 'Show', value: true },
|
||||||
|
{ label: 'Hide', value: false }
|
||||||
|
]),
|
||||||
|
layoutOptions: markRaw([
|
||||||
|
{ label: 'Circular', value: 'circular' },
|
||||||
|
{ label: 'Random', value: 'random' },
|
||||||
|
{ label: 'Circle pack', value: 'circlepack' },
|
||||||
|
{ label: 'ForceAtlas2', value: 'forceAtlas2' }
|
||||||
|
]),
|
||||||
|
layoutSettingsComponentMap: markRaw({
|
||||||
|
random: RandomLayoutSettings,
|
||||||
|
circlepack: CirclePackLayoutSettings,
|
||||||
|
forceAtlas2: ForceAtlasLayoutSettings
|
||||||
|
}),
|
||||||
|
|
||||||
|
settings: this.initOptions
|
||||||
|
? JSON.parse(JSON.stringify(this.initOptions))
|
||||||
|
: {
|
||||||
|
structure: {
|
||||||
|
nodeId: null,
|
||||||
|
objectType: null,
|
||||||
|
edgeSource: null,
|
||||||
|
edgeTarget: null
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
backgroundColor: 'white',
|
||||||
|
nodes: {
|
||||||
|
size: {
|
||||||
|
type: 'constant',
|
||||||
|
value: 10
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'constant',
|
||||||
|
value: '#1F77B4'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null,
|
||||||
|
color: '#444444'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
edges: {
|
||||||
|
showDirection: true,
|
||||||
|
size: {
|
||||||
|
type: 'constant',
|
||||||
|
value: 2
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: 'constant',
|
||||||
|
value: '#a2b1c6'
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
source: null,
|
||||||
|
color: '#a2b1c6'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layout: {
|
||||||
|
type: 'circular',
|
||||||
|
options: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
layoutOptionsArchive: {
|
||||||
|
random: null,
|
||||||
|
circlepack: null,
|
||||||
|
forceAtlas2: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
records() {
|
||||||
|
if (!this.dataSources) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const firstColumnName = Object.keys(this.dataSources)[0]
|
||||||
|
try {
|
||||||
|
return (
|
||||||
|
this.dataSources[firstColumnName].map(json => JSON.parse(json)) || []
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
keysOptions() {
|
||||||
|
if (!this.dataSources) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
const keySet = this.records.reduce((result, currentRecord) => {
|
||||||
|
Object.keys(currentRecord).forEach(key => result.add(key))
|
||||||
|
return result
|
||||||
|
}, new Set())
|
||||||
|
|
||||||
|
return Array.from(keySet)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
dataSources() {
|
||||||
|
if (this.dataSources) {
|
||||||
|
this.buildGraph()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.$emit('update')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'settings.structure': {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.buildGraph()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'settings.layout.type': {
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
events.send('viz_graph.render', null, {
|
||||||
|
layout: this.settings.layout.type
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tabLayout: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
if (this.tabLayout.dataView !== 'hidden' && this.renderer) {
|
||||||
|
this.renderer.scheduleRender()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
if (this.dataSources) {
|
||||||
|
this.buildGraph()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
buildGraph() {
|
||||||
|
if (this.renderer) {
|
||||||
|
this.renderer.kill()
|
||||||
|
}
|
||||||
|
this.graph.clear()
|
||||||
|
|
||||||
|
buildNodes(this.graph, this.dataSources, this.settings)
|
||||||
|
buildEdges(this.graph, this.dataSources, this.settings)
|
||||||
|
|
||||||
|
// Apply visual settings
|
||||||
|
updateNodes(this.graph, this.settings.style.nodes)
|
||||||
|
updateEdges(this.graph, this.settings.style.edges)
|
||||||
|
|
||||||
|
this.updateLayout(this.settings.layout.type)
|
||||||
|
this.renderer = new Sigma(this.graph, this.$refs.graph, {
|
||||||
|
renderEdgeLabels: true,
|
||||||
|
allowInvalidContainer: true
|
||||||
|
})
|
||||||
|
if (this.settings.layout.type === 'forceAtlas2') {
|
||||||
|
this.autoRunFA2Layout()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateStructure(attributeName, value) {
|
||||||
|
this.settings.structure[attributeName] = value
|
||||||
|
},
|
||||||
|
updateNodes(attributeName, value) {
|
||||||
|
const attributePath = attributeName.split('.')
|
||||||
|
attributePath.reduce((result, current, index) => {
|
||||||
|
if (index === attributePath.length - 1) {
|
||||||
|
return (result[current] = value)
|
||||||
|
} else {
|
||||||
|
return result[current]
|
||||||
|
}
|
||||||
|
}, this.settings.style.nodes)
|
||||||
|
|
||||||
|
updateNodes(this.graph, {
|
||||||
|
[attributePath[0]]: this.settings.style.nodes[attributePath[0]]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateEdges(attributeName, value) {
|
||||||
|
const attributePath = attributeName.split('.')
|
||||||
|
attributePath.reduce((result, current, index) => {
|
||||||
|
if (index === attributePath.length - 1) {
|
||||||
|
return (result[current] = value)
|
||||||
|
} else {
|
||||||
|
return result[current]
|
||||||
|
}
|
||||||
|
}, this.settings.style.edges)
|
||||||
|
|
||||||
|
updateEdges(this.graph, {
|
||||||
|
[attributePath[0]]: this.settings.style.edges[attributePath[0]]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateLayout(layoutType) {
|
||||||
|
const prevLayout = this.settings.layout.type
|
||||||
|
|
||||||
|
// Change layout type? - restore layout settings or set default settings
|
||||||
|
if (layoutType !== prevLayout) {
|
||||||
|
this.layoutOptionsArchive[prevLayout] = this.settings.layout.options
|
||||||
|
this.settings.layout.options = this.layoutOptionsArchive[layoutType]
|
||||||
|
|
||||||
|
if (!this.settings.layout.options) {
|
||||||
|
if (layoutType === 'forceAtlas2') {
|
||||||
|
this.setRecommendedFA2Settings()
|
||||||
|
} else if (['random', 'circlepack'].includes(layoutType)) {
|
||||||
|
this.settings.layout.options = {
|
||||||
|
seedValue: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.settings.layout.type = layoutType
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any case kill FA2 if it exists
|
||||||
|
if (this.fa2Layout) {
|
||||||
|
if (this.fa2Layout.isRunning()) {
|
||||||
|
this.stopFA2Layout()
|
||||||
|
}
|
||||||
|
this.fa2Layout.kill()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'circular') {
|
||||||
|
circular.assign(this.graph)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'random') {
|
||||||
|
random.assign(this.graph, {
|
||||||
|
rng: seedrandom(this.settings.layout.options.seedValue)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'circlepack') {
|
||||||
|
this.graph.forEachNode(nodeId => {
|
||||||
|
this.graph.updateNode(nodeId, attributes => {
|
||||||
|
const newAttributes = { ...attributes }
|
||||||
|
// Delete old hierarchy attributes
|
||||||
|
Object.keys(newAttributes)
|
||||||
|
.filter(key => key.startsWith('hierarchyAttribute'))
|
||||||
|
.forEach(
|
||||||
|
hierarchyAttributeKey =>
|
||||||
|
delete newAttributes[hierarchyAttributeKey]
|
||||||
|
)
|
||||||
|
// Set new hierarchy attributes
|
||||||
|
this.settings.layout.options.hierarchyAttributes?.forEach(
|
||||||
|
(hierarchyAttribute, index) => {
|
||||||
|
newAttributes['hierarchyAttribute' + index] =
|
||||||
|
attributes.data[hierarchyAttribute]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return newAttributes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
circlepack.assign(this.graph, {
|
||||||
|
hierarchyAttributes:
|
||||||
|
this.settings.layout.options.hierarchyAttributes?.map(
|
||||||
|
(_, index) => 'hierarchyAttribute' + index
|
||||||
|
) || [],
|
||||||
|
rng: seedrandom(this.settings.layout.options.seedValue)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layoutType === 'forceAtlas2') {
|
||||||
|
if (
|
||||||
|
!this.graph.someNode(
|
||||||
|
(nodeKey, attributes) =>
|
||||||
|
typeof attributes.x === 'number' &&
|
||||||
|
typeof attributes.y === 'number'
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
circular.assign(this.graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.fa2Layout = markRaw(
|
||||||
|
new FA2Layout(this.graph, {
|
||||||
|
getEdgeWeight: (_, attr) =>
|
||||||
|
this.settings.layout.options.weightSource
|
||||||
|
? attr.data[this.settings.layout.options.weightSource]
|
||||||
|
: 1,
|
||||||
|
settings: this.settings.layout.options
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if (layoutType !== prevLayout) {
|
||||||
|
this.autoRunFA2Layout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toggleFA2Layout() {
|
||||||
|
if (this.fa2Layout.isRunning()) {
|
||||||
|
this.stopFA2Layout()
|
||||||
|
} else {
|
||||||
|
this.fa2Running = true
|
||||||
|
this.fa2Layout.start()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stopFA2Layout() {
|
||||||
|
this.fa2Running = false
|
||||||
|
this.fa2Layout.stop()
|
||||||
|
if (this.checkIteration) {
|
||||||
|
this.fa2Layout.worker.removeEventListener(
|
||||||
|
'message',
|
||||||
|
this.checkIteration
|
||||||
|
)
|
||||||
|
this.checkIteration = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
autoRunFA2Layout() {
|
||||||
|
let iteration = 1
|
||||||
|
this.checkIteration = () => {
|
||||||
|
if (
|
||||||
|
iteration === this.settings.layout.options.initialIterationsAmount
|
||||||
|
) {
|
||||||
|
this.stopFA2Layout()
|
||||||
|
}
|
||||||
|
iteration++
|
||||||
|
}
|
||||||
|
this.fa2Layout.worker.addEventListener('message', this.checkIteration)
|
||||||
|
this.fa2Running = true
|
||||||
|
this.fa2Layout.start()
|
||||||
|
},
|
||||||
|
setRecommendedFA2Settings() {
|
||||||
|
const sensibleSettings = forceAtlas2.default.inferSettings(this.graph)
|
||||||
|
this.settings.layout.options = {
|
||||||
|
initialIterationsAmount: 50,
|
||||||
|
adjustSizes: false,
|
||||||
|
barnesHutOptimize: false,
|
||||||
|
barnesHutTheta: 0.5,
|
||||||
|
edgeWeightInfluence: 0,
|
||||||
|
gravity: 1,
|
||||||
|
linLogMode: false,
|
||||||
|
outboundAttractionDistribution: false,
|
||||||
|
scalingRatio: 1,
|
||||||
|
slowDown: 1,
|
||||||
|
strongGravityMode: false,
|
||||||
|
...sensibleSettings
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
[Infinity, -Infinity].includes(this.settings.layout.options.slowDown)
|
||||||
|
) {
|
||||||
|
this.settings.layout.options.slowDown = 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
resetFA2LayoutSettings() {
|
||||||
|
if (this.initOptions?.layout.type === 'forceAtlas2') {
|
||||||
|
this.settings.layout = JSON.parse(
|
||||||
|
JSON.stringify(this.initOptions.layout)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.setRecommendedFA2Settings()
|
||||||
|
}
|
||||||
|
this.updateLayout(this.settings.layout.type)
|
||||||
|
},
|
||||||
|
saveAsPng() {
|
||||||
|
return downloadAsPNG(this.renderer, {
|
||||||
|
backgroundColor: this.settings.style.backgroundColor
|
||||||
|
})
|
||||||
|
},
|
||||||
|
prepareCopy() {
|
||||||
|
return drawOnCanvas(this.renderer, {
|
||||||
|
backgroundColor: this.settings.style.backgroundColor
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.plotly_editor.with_controls > div {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.force-atlas-buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.force-atlas-buttons :deep(button) {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-basis: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Color" fieldContainerClassName="test_node_color">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeColorTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateColorType"
|
||||||
|
/>
|
||||||
|
<Field
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
fieldContainerClassName="test_node_color_value"
|
||||||
|
>
|
||||||
|
<ColorPicker
|
||||||
|
:selectedColor="modelValue.value"
|
||||||
|
@color-change="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
<template v-else>
|
||||||
|
<Field fieldContainerClassName="test_node_color_value">
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedColorMethodOptions"
|
||||||
|
:value="modelValue.method"
|
||||||
|
:clearable="false"
|
||||||
|
@change="updateSettings('method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
fieldContainerClassName="test_node_color_mapping_mode"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="colorSourceUsageOptions"
|
||||||
|
:activeOption="modelValue.sourceUsage"
|
||||||
|
@option-change="updateSettings('sourceUsage', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="
|
||||||
|
modelValue.sourceUsage === 'map_to' ||
|
||||||
|
modelValue.type === 'calculated'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<ColorscalePicker
|
||||||
|
:selected="modelValue.colorscale"
|
||||||
|
className="colorscale-picker"
|
||||||
|
@colorscale-change="updateSettings('colorscale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="
|
||||||
|
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
|
||||||
|
"
|
||||||
|
label="Color as"
|
||||||
|
fieldContainerClassName="test_node_color_as"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorAsOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field
|
||||||
|
v-if="
|
||||||
|
modelValue.sourceUsage === 'map_to' || modelValue.type === 'calculated'
|
||||||
|
"
|
||||||
|
label="Colorscale direction"
|
||||||
|
fieldContainerClassName="test_node_color_colorscale_direction"
|
||||||
|
>
|
||||||
|
<RadioBlocks
|
||||||
|
:options="сolorscaleDirections"
|
||||||
|
:activeOption="modelValue.colorscaleDirection"
|
||||||
|
@option-change="updateSettings('colorscaleDirection', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
|
||||||
|
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
|
||||||
|
ColorPicker: applyPureReactInVue(ColorPicker)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeColorTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedColorMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
сolorAsOptions: markRaw([
|
||||||
|
{ label: 'Continious', value: 'continious' },
|
||||||
|
{ label: 'Categorical', value: 'categorical' }
|
||||||
|
]),
|
||||||
|
сolorscaleDirections: markRaw([
|
||||||
|
{ label: 'Normal', value: 'normal' },
|
||||||
|
{ label: 'Recersed', value: 'reversed' }
|
||||||
|
]),
|
||||||
|
colorSourceUsageOptions: markRaw([
|
||||||
|
{ label: 'Direct', value: 'direct' },
|
||||||
|
{ label: 'Map to', value: 'map_to' }
|
||||||
|
]),
|
||||||
|
defaultColorSettings: {
|
||||||
|
constant: { value: '#1F77B4' },
|
||||||
|
variable: {
|
||||||
|
source: null,
|
||||||
|
sourceUsage: 'map_to',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'categorical',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
},
|
||||||
|
calculated: {
|
||||||
|
method: 'degree',
|
||||||
|
colorscale: null,
|
||||||
|
mode: 'continious',
|
||||||
|
colorscaleDirection: 'normal'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateColorType(newColorType) {
|
||||||
|
const currentColorType = this.modelValue.type
|
||||||
|
this.defaultColorSettings[currentColorType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newColorType,
|
||||||
|
...this.defaultColorSettings[newColorType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
:deep(.customPickerContainer) {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Size" fieldContainerClassName="test_node_size">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeTypeOptions"
|
||||||
|
:activeOption="modelValue.type"
|
||||||
|
@option-change="updateSizeType"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Field fieldContainerClassName="test_node_size_value">
|
||||||
|
<NumericInput
|
||||||
|
v-if="modelValue.type === 'constant'"
|
||||||
|
:value="modelValue.value"
|
||||||
|
:min="1"
|
||||||
|
class="test_node_size_value"
|
||||||
|
@update="updateSettings('value', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'variable'"
|
||||||
|
:options="keyOptions"
|
||||||
|
:value="modelValue.source"
|
||||||
|
@change="updateSettings('source', $event)"
|
||||||
|
/>
|
||||||
|
<Dropdown
|
||||||
|
v-if="modelValue.type === 'calculated'"
|
||||||
|
:options="nodeCalculatedSizeMethodOptions"
|
||||||
|
:value="modelValue.method"
|
||||||
|
:clearable="false"
|
||||||
|
@change="updateSettings('method', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<template v-if="modelValue.type !== 'constant'">
|
||||||
|
<Field label="Size scale" fieldContainerClassName="test_node_size_scale">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.scale"
|
||||||
|
@update="updateSettings('scale', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Size mode" fieldContainerClassName="test_node_size_mode">
|
||||||
|
<RadioBlocks
|
||||||
|
:options="nodeSizeModeOptions"
|
||||||
|
:activeOption="modelValue.mode"
|
||||||
|
@option-change="updateSettings('mode', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Minimum size" fieldContainerClassName="test_node_size_min">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.min"
|
||||||
|
@update="updateSettings('min', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { markRaw } from 'vue'
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
|
||||||
|
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown: applyPureReactInVue(Dropdown),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput),
|
||||||
|
RadioBlocks: applyPureReactInVue(RadioBlocks),
|
||||||
|
Field: applyPureReactInVue(Field)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object,
|
||||||
|
keyOptions: Array
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
nodeSizeTypeOptions: markRaw([
|
||||||
|
{ label: 'Constant', value: 'constant' },
|
||||||
|
{ label: 'Variable', value: 'variable' },
|
||||||
|
{ label: 'Calculated', value: 'calculated' }
|
||||||
|
]),
|
||||||
|
nodeCalculatedSizeMethodOptions: markRaw([
|
||||||
|
{ label: 'Degree', value: 'degree' },
|
||||||
|
{ label: 'In degree', value: 'inDegree' },
|
||||||
|
{ label: 'Out degree', value: 'outDegree' }
|
||||||
|
]),
|
||||||
|
nodeSizeModeOptions: markRaw([
|
||||||
|
{ label: 'Area', value: 'area' },
|
||||||
|
{ label: 'Diameter', value: 'diameter' }
|
||||||
|
]),
|
||||||
|
defaultSizeSettings: {
|
||||||
|
constant: { value: 4 },
|
||||||
|
variable: { source: null, scale: 1, mode: 'diameter', min: 1 },
|
||||||
|
calculated: { method: 'degree', scale: 1, mode: 'diameter', min: 1 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateSizeType(newSizeType) {
|
||||||
|
const currentSizeType = this.modelValue.type
|
||||||
|
this.defaultSizeSettings[currentSizeType] = this.modelValue
|
||||||
|
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
type: newSizeType,
|
||||||
|
...this.defaultSizeSettings[newSizeType]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateSettings(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<Field label="Seed value">
|
||||||
|
<NumericInput
|
||||||
|
:value="modelValue.seedValue"
|
||||||
|
@update="update('seedValue', $event)"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { applyPureReactInVue } from 'veaury'
|
||||||
|
import Field from 'react-chart-editor/lib/components/fields/Field'
|
||||||
|
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
|
||||||
|
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Field: applyPureReactInVue(Field),
|
||||||
|
NumericInput: applyPureReactInVue(NumericInput)
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
modelValue: Object
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue'],
|
||||||
|
methods: {
|
||||||
|
update(attributeName, value) {
|
||||||
|
this.$emit('update:modelValue', {
|
||||||
|
...this.modelValue,
|
||||||
|
[attributeName]: value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<modal
|
||||||
|
v-model="show"
|
||||||
|
class="dialog"
|
||||||
|
:clickToClose="false"
|
||||||
|
:contentTransition="{ name: 'loading-dialog' }"
|
||||||
|
:overlayTransition="{ name: 'loading-dialog' }"
|
||||||
|
@update:model-value="$emit('update:modelValue', $event)"
|
||||||
|
>
|
||||||
|
<div class="dialog-header">
|
||||||
|
{{ title }}
|
||||||
|
<close-icon :disabled="loading" @click="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="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: {
|
||||||
|
modelValue: Boolean,
|
||||||
|
loadingMsg: String,
|
||||||
|
successMsg: String,
|
||||||
|
actionBtnName: String,
|
||||||
|
title: String,
|
||||||
|
loading: Boolean
|
||||||
|
},
|
||||||
|
emits: ['cancel', 'action', 'update:modelValue'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show: this.modelValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
modelValue() {
|
||||||
|
this.show = this.modelValue
|
||||||
|
},
|
||||||
|
loading() {
|
||||||
|
if (this.loading) {
|
||||||
|
this.$emit('update:modelValue', true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
cancel() {
|
||||||
|
this.$emit('cancel')
|
||||||
|
this.$emit('update:modelValue', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
<template>
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="placeholder"
|
:placeholder="placeholder"
|
||||||
:class="{ error: errorMsg }"
|
:class="{ error: errorMsg }"
|
||||||
:style="{ width: width }"
|
:style="{ width: width }"
|
||||||
:value="value"
|
:value="modelValue"
|
||||||
@input="$emit('input', $event.target.value)"
|
: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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import HintIcon from '@/components/svg/hint'
|
||||||
export default {
|
export default {
|
||||||
name: 'textField',
|
name: 'TextField',
|
||||||
props: ['placeholder', 'label', 'errorMsg', 'value', 'width']
|
components: { HintIcon },
|
||||||
|
props: {
|
||||||
|
placeholder: String,
|
||||||
|
label: String,
|
||||||
|
errorMsg: String,
|
||||||
|
modelValue: String,
|
||||||
|
width: String,
|
||||||
|
hint: String,
|
||||||
|
maxHintWidth: String,
|
||||||
|
disabled: Boolean
|
||||||
|
},
|
||||||
|
emits: ['update:modelValue']
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -30,6 +54,7 @@ input {
|
|||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
@@ -40,24 +65,47 @@ input:focus {
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
background: var(--color-bg-light);
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
input.error {
|
input.error {
|
||||||
border-color: var(--color-text-error);
|
border-color: var(--color-text-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-field-label {
|
.text-field-label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
margin-bottom: 2px;
|
margin-bottom: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-field-label .hint {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-field-label.error {
|
.text-field-label.error {
|
||||||
color: var(--color-text-error);
|
color: var(--color-text-error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text-field-label.disabled {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
.text-field-error {
|
.text-field-error {
|
||||||
color: var(--color-text-error);
|
color: var(--color-text-error);
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.text-field-error:first-letter {
|
||||||
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
</style>
|
</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>
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<svg
|
<svg
|
||||||
class="db-edit-icon"
|
class="db-edit-icon"
|
||||||
width="18"
|
width="18"
|
||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 18 18"
|
viewBox="0 0 18 18"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click.stop="onClick"
|
||||||
@mouseover="showTooltip"
|
@mouseenter="showTooltip"
|
||||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<path
|
<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"
|
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"
|
fill="#A2B1C6"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
|
||||||
Change database
|
Load another database, CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import tooltipMixin from '@/mixins/tooltips'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'changeDbIcon',
|
name: 'ChangeDbIcon',
|
||||||
mixins: [tooltipMixin]
|
mixins: [tooltipMixin],
|
||||||
|
emits: ['click'],
|
||||||
|
methods: {
|
||||||
|
onClick() {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||