mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
21
.github/workflows/main.yml
vendored
21
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -7,7 +7,7 @@ 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
|
||||||
@@ -24,10 +24,11 @@ jobs:
|
|||||||
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" -x "/*.map"
|
zip -9 -r ../dist.zip . -x "js/*.map" -x "/*.map"
|
||||||
|
zip -9 -r ../dist_map.zip .
|
||||||
|
|
||||||
- name: Create Release Notes
|
- name: Create Release Notes
|
||||||
run: |
|
run: |
|
||||||
@@ -39,16 +40,6 @@ jobs:
|
|||||||
- 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"
|
bodyFile: "CHANGELOG.md"
|
||||||
|
|
||||||
- name: Deploy 🚀
|
|
||||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
branch: build # The branch the action should deploy to.
|
|
||||||
folder: dist/ # The folder the action should deploy.
|
|
||||||
clean: true # Automatically remove deleted files from the deploy branch
|
|
||||||
clean-exclude: .nojekyll
|
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -11,7 +11,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
name: Run tests
|
name: Run tests
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
|
|||||||
24
Dockerfile.test
Normal file
24
Dockerfile.test
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# An easy way to run tests locally without Nodejs installed:
|
||||||
|
#
|
||||||
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
|
#
|
||||||
|
|
||||||
|
FROM node:12
|
||||||
|
|
||||||
|
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
|
||||||
@@ -18,10 +18,10 @@ With sqliteviz you can:
|
|||||||
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
|
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
|
||||||
|
|
||||||
## Wiki
|
## Wiki
|
||||||
For user documentation, check out sqliteviz [Wiki][7].
|
For user documentation, check out sqliteviz [documentation][7].
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
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].
|
||||||
@@ -34,8 +34,8 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
|
|||||||
[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://lana-k.github.io/sqliteviz/
|
[6]: https://sqliteviz.com/app/
|
||||||
[7]: https://github.com/lana-k/sqliteviz/wiki
|
[7]: https://sqliteviz.com/docs
|
||||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||||
[9]: https://www.papaparse.com/
|
[9]: https://www.papaparse.com/
|
||||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM emscripten/emsdk:2.0.24
|
FROM emscripten/emsdk:3.0.1
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ SQLite [miscellaneous extensions][3] included:
|
|||||||
SQLite 3rd party extensions included:
|
SQLite 3rd party extensions included:
|
||||||
|
|
||||||
1. [pivot_vtab][5] -- a pivot virtual table
|
1. [pivot_vtab][5] -- a pivot virtual table
|
||||||
|
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||||
|
(which is part of [squib][20])
|
||||||
|
|
||||||
To ease the step to have working clone locally, the build is committed into
|
To ease the step to have working clone locally, the build is committed into
|
||||||
the repository.
|
the repository.
|
||||||
@@ -99,3 +101,5 @@ described in [this message from SQLite Forum][12]:
|
|||||||
[17]: https://sqlite.org/contrib/
|
[17]: https://sqlite.org/contrib/
|
||||||
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
|
[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
|
[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
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:12-buster
|
FROM node:14-bullseye
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
echo 'deb http://deb.debian.org/debian unstable main' \
|
echo 'deb http://deb.debian.org/debian unstable main' \
|
||||||
|
|||||||
@@ -1,14 +1,25 @@
|
|||||||
# SQLite WebAssembly build micro-benchmark
|
# SQLite WebAssembly build micro-benchmark
|
||||||
|
|
||||||
This directory contains a micro-benchmark for evaluating SQLite
|
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
|
||||||
WebAssembly builds performance on typical SQL queries, run from
|
builds performance on read and write SQL queries, run from `make.sh` script. If
|
||||||
`make.sh` script. It can also serve as a smoke test.
|
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
|
The benchmark operates on a set of SQLite WebAssembly builds expected in
|
||||||
in `lib/build-$NAME` directories each containing `sql-wasm.js` and
|
`lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||||
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs
|
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
|
||||||
the benchmark in Firefox and Chromium using Karma in the container.
|
benchmark in Firefox and Chromium using Karma in the container.
|
||||||
|
|
||||||
After successful run, the benchmark result of each build is contained
|
After successful run, the benchmark produces the following per each build:
|
||||||
in `build-$NAME-result.json`. The JSON result files can be analysed
|
|
||||||
using `result-analysis.ipynb` Jupyter notebook.
|
- `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/
|
||||||
|
|||||||
@@ -1,21 +1,47 @@
|
|||||||
#!/bin/bash -e
|
#!/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
|
if [ ! -f sample.csv ]; then
|
||||||
wget --header="accept-encoding: gzip" -q -O- \
|
wget --header="accept-encoding: gzip" -q -O- \
|
||||||
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
||||||
| gunzip -c > sample.csv
|
| gunzip -c > sample.csv
|
||||||
fi
|
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
|
for d in lib/build-* ; do
|
||||||
rm -r lib/dist || true
|
rm -rf lib/dist
|
||||||
cp -r $d lib/dist
|
cp -r $d lib/dist
|
||||||
|
sample_name=$(basename $d)
|
||||||
|
|
||||||
name=$(basename $d)
|
docker build -t sqliteviz/sqljs-benchmark .
|
||||||
docker build -t sqliteviz/sqljs-benchmark:$name .
|
docker rm sqljs-benchmark-run 2> /dev/null || true
|
||||||
docker rm sqljs-benchmark-$name 2> /dev/null || true
|
docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
|
||||||
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
|
{
|
||||||
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
|
rm -f ${sample_name}.sqlite
|
||||||
docker rm sqljs-benchmark-$name
|
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
|
done
|
||||||
|
|
||||||
rm -r lib/dist
|
|
||||||
|
|||||||
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This command may run when "sqljs-benchmark-run" does not yet exist or run
|
||||||
|
[renice:watch]
|
||||||
|
interval: 2
|
||||||
|
repeat: 30
|
||||||
|
environment:
|
||||||
|
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
|
||||||
|
query:
|
||||||
|
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
|
||||||
|
command:
|
||||||
|
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
|
||||||
|
|
||||||
|
# Expected input arguments: database_file
|
||||||
|
[track:record]
|
||||||
|
interval: 1
|
||||||
|
stop_without_result: 1
|
||||||
|
environment:
|
||||||
|
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
|
||||||
|
query:
|
||||||
|
$..children[?(@.stat.pid == $ROOT_PID)]
|
||||||
|
pid_list: $ROOT_PID
|
||||||
|
|
||||||
|
# Expected input arguments: database_file, plot_file
|
||||||
|
[track:plot]
|
||||||
|
moving_average_window: 5
|
||||||
|
title: Chromium vs Firefox (№1 RSS, №2 CPU)
|
||||||
|
custom_query_file:
|
||||||
|
procpath/top2_rss.sql
|
||||||
|
procpath/top2_cpu.sql
|
||||||
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
WITH diff_all AS (
|
||||||
|
SELECT
|
||||||
|
record_id,
|
||||||
|
ts,
|
||||||
|
stat_pid,
|
||||||
|
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
|
||||||
|
PARTITION BY stat_pid
|
||||||
|
ORDER BY record_id
|
||||||
|
) tick_diff,
|
||||||
|
ts - LAG(ts) OVER (
|
||||||
|
PARTITION BY stat_pid
|
||||||
|
ORDER BY record_id
|
||||||
|
) ts_diff
|
||||||
|
FROM record
|
||||||
|
), diff AS (
|
||||||
|
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
|
||||||
|
), one_time_pid_condition AS (
|
||||||
|
SELECT stat_pid
|
||||||
|
FROM record
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY SUM(stat_utime + stat_stime) DESC
|
||||||
|
LIMIT 2
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ts,
|
||||||
|
stat_pid pid,
|
||||||
|
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
|
||||||
|
FROM diff
|
||||||
|
JOIN one_time_pid_condition USING(stat_pid)
|
||||||
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
WITH one_time_pid_condition AS (
|
||||||
|
SELECT stat_pid
|
||||||
|
FROM record
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY SUM(stat_rss) DESC
|
||||||
|
LIMIT 2
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ts,
|
||||||
|
stat_pid pid,
|
||||||
|
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
|
||||||
|
FROM record
|
||||||
|
JOIN one_time_pid_condition USING(stat_pid)
|
||||||
File diff suppressed because one or more lines are too long
@@ -2,9 +2,11 @@ import logging
|
|||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
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 = (
|
cflags = (
|
||||||
'-O2',
|
# SQLite configuration
|
||||||
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
||||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||||
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
||||||
@@ -13,26 +15,26 @@ cflags = (
|
|||||||
'-DSQLITE_ENABLE_FTS3',
|
'-DSQLITE_ENABLE_FTS3',
|
||||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||||
'-DSQLITE_ENABLE_FTS5',
|
'-DSQLITE_ENABLE_FTS5',
|
||||||
'-DSQLITE_ENABLE_JSON1',
|
|
||||||
'-DSQLITE_ENABLE_NORMALIZE',
|
'-DSQLITE_ENABLE_NORMALIZE',
|
||||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||||
'-DSQLITE_OMIT_DEPRECATED',
|
'-DSQLITE_OMIT_DEPRECATED',
|
||||||
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||||
'-DSQLITE_OMIT_SHARED_CACHE',
|
'-DSQLITE_OMIT_SHARED_CACHE',
|
||||||
'-DSQLITE_THREADSAFE=0',
|
'-DSQLITE_THREADSAFE=0',
|
||||||
|
# Compile-time optimisation
|
||||||
|
'-Os', # reduces the code size about in half comparing to -O2
|
||||||
|
'-flto',
|
||||||
)
|
)
|
||||||
emflags = (
|
emflags = (
|
||||||
# Base
|
# Base
|
||||||
'--memory-init-file', '0',
|
'--memory-init-file', '0',
|
||||||
'-s', 'RESERVED_FUNCTION_POINTERS=64',
|
|
||||||
'-s', 'ALLOW_TABLE_GROWTH=1',
|
'-s', 'ALLOW_TABLE_GROWTH=1',
|
||||||
'-s', 'SINGLE_FILE=0',
|
|
||||||
# WASM
|
# WASM
|
||||||
'-s', 'WASM=1',
|
'-s', 'WASM=1',
|
||||||
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
||||||
# Optimisation
|
'-s', 'ENVIRONMENT=web,worker',
|
||||||
'-s', 'INLINING_LIMIT=50',
|
# Link-time optimisation
|
||||||
'-O3',
|
'-Os',
|
||||||
'-flto',
|
'-flto',
|
||||||
# sql.js
|
# sql.js
|
||||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||||
@@ -50,22 +52,22 @@ def build(src: Path, dst: Path):
|
|||||||
'emcc',
|
'emcc',
|
||||||
*cflags,
|
*cflags,
|
||||||
'-c', src / 'sqlite3.c',
|
'-c', src / 'sqlite3.c',
|
||||||
'-o', out / 'sqlite3.bc',
|
'-o', out / 'sqlite3.o',
|
||||||
])
|
])
|
||||||
logging.info('Building LLVM bitcode for extension-functions.c')
|
logging.info('Building LLVM bitcode for extension-functions.c')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
'emcc',
|
'emcc',
|
||||||
*cflags,
|
*cflags,
|
||||||
'-c', src / 'extension-functions.c',
|
'-c', src / 'extension-functions.c',
|
||||||
'-o', out / 'extension-functions.bc',
|
'-o', out / 'extension-functions.o',
|
||||||
])
|
])
|
||||||
|
|
||||||
logging.info('Building WASM from bitcode')
|
logging.info('Building WASM from bitcode')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
'emcc',
|
'emcc',
|
||||||
*emflags,
|
*emflags,
|
||||||
out / 'sqlite3.bc',
|
out / 'sqlite3.o',
|
||||||
out / 'extension-functions.bc',
|
out / 'extension-functions.o',
|
||||||
'-o', out / 'sql-wasm.js',
|
'-o', out / 'sql-wasm.js',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pathlib import Path
|
|||||||
from urllib import request
|
from urllib import request
|
||||||
|
|
||||||
|
|
||||||
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip'
|
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||||
|
|
||||||
# Extension-functions
|
# Extension-functions
|
||||||
# ===================
|
# ===================
|
||||||
@@ -20,7 +20,7 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
|
|||||||
extension_urls = (
|
extension_urls = (
|
||||||
# Miscellaneous extensions
|
# Miscellaneous extensions
|
||||||
# ========================
|
# ========================
|
||||||
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_init'),
|
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
|
||||||
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
|
||||||
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
|
||||||
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
|
||||||
@@ -28,10 +28,11 @@ extension_urls = (
|
|||||||
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||||
# Third-party extension
|
# Third-party extension
|
||||||
# =====================
|
# =====================
|
||||||
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
|
||||||
|
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.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):
|
def _generate_extra_init_c_function(init_function_names):
|
||||||
|
|||||||
2
lib/sql-js/dist/sql-wasm.js
vendored
2
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Binary file not shown.
26353
package-lock.json
generated
26353
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.18.0",
|
"version": "0.23.2",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
"html2canvas": "^1.1.4",
|
"html2canvas": "^1.1.4",
|
||||||
"jquery": "^3.6.0",
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.4.1",
|
||||||
"pivottable": "^2.23.0",
|
"pivottable": "^2.23.0",
|
||||||
"plotly.js": "^1.58.4",
|
"plotly.js": "^1.58.4",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
@@ -51,6 +51,7 @@
|
|||||||
"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": "^6.2.2",
|
||||||
|
"flush-promises": "^1.0.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^3.1.4",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma-firefox-launcher": "^2.1.0",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<!-- head extention slot start -->
|
||||||
|
<!-- head extention slot end -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
@@ -93,6 +95,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- extention slot start -->
|
||||||
|
<!-- extention slot end -->
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
3
src/assets/images/logo_simple.svg
Normal file
3
src/assets/images/logo_simple.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 862 B |
@@ -112,6 +112,7 @@ import SqlTable from '@/components/SqlTable'
|
|||||||
import Logs from '@/components/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CsvImport',
|
name: 'CsvImport',
|
||||||
@@ -335,6 +336,7 @@ export default {
|
|||||||
this.$store.commit('setCurrentTabId', tabId)
|
this.$store.commit('setCurrentTabId', tabId)
|
||||||
this.importCsvCompleted = false
|
this.importCsvCompleted = false
|
||||||
this.$emit('finish')
|
this.$emit('finish')
|
||||||
|
events.send('inquiry.create', null, { auto: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ import fIo from '@/lib/utils/fileIo'
|
|||||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvImport from '@/components/CsvImport'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DbUploader',
|
name: 'DbUploader',
|
||||||
@@ -127,6 +128,11 @@ export default {
|
|||||||
if (fIo.isDatabase(file)) {
|
if (fIo.isDatabase(file)) {
|
||||||
this.loadDb(file)
|
this.loadDb(file)
|
||||||
} else {
|
} else {
|
||||||
|
events.send('database.import', file.size, {
|
||||||
|
from: 'csv',
|
||||||
|
new_db: true
|
||||||
|
})
|
||||||
|
|
||||||
this.file = file
|
this.file = file
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const csvImport = this.$refs.addCsv
|
const csvImport = this.$refs.addCsv
|
||||||
|
|||||||
@@ -75,14 +75,23 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
horizontal: { type: Boolean, default: false },
|
horizontal: { type: Boolean, default: false },
|
||||||
before: { type: Object },
|
before: { type: Object },
|
||||||
after: { type: Object }
|
after: { type: Object },
|
||||||
|
default: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {
|
||||||
|
before: 50,
|
||||||
|
after: 50
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
container: null,
|
container: null,
|
||||||
paneBefore: this.before,
|
paneBefore: this.before,
|
||||||
paneAfter: this.after,
|
paneAfter: this.after,
|
||||||
beforeMinimising: {
|
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
|
||||||
before: this.before.size,
|
before: this.before.size,
|
||||||
after: this.after.size
|
after: this.after.size
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -73,21 +73,26 @@ export default {
|
|||||||
comments: false,
|
comments: false,
|
||||||
step: undefined,
|
step: undefined,
|
||||||
complete: results => {
|
complete: results => {
|
||||||
const res = {
|
let res
|
||||||
data: this.getResult(results),
|
try {
|
||||||
delimiter: results.meta.delimiter,
|
res = {
|
||||||
hasErrors: false,
|
data: this.getResult(results),
|
||||||
rowCount: results.data.length
|
delimiter: results.meta.delimiter,
|
||||||
|
hasErrors: false,
|
||||||
|
rowCount: results.data.length
|
||||||
|
}
|
||||||
|
res.messages = results.errors.map(msg => {
|
||||||
|
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
||||||
|
if (msg.type === 'error') res.hasErrors = true
|
||||||
|
msg.hint = hintsByCode[msg.code]
|
||||||
|
return msg
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
reject(error)
|
||||||
}
|
}
|
||||||
res.messages = results.errors.map(msg => {
|
|
||||||
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
|
||||||
if (msg.type === 'error') res.hasErrors = true
|
|
||||||
msg.hint = hintsByCode[msg.code]
|
|
||||||
return msg
|
|
||||||
})
|
|
||||||
resolve(res)
|
resolve(res)
|
||||||
},
|
},
|
||||||
error: (error, file) => {
|
error: error => {
|
||||||
reject(error)
|
reject(error)
|
||||||
},
|
},
|
||||||
download: false,
|
download: false,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import Worker from './_worker.js'
|
|||||||
// https://github.com/nolanlawson/promise-worker
|
// https://github.com/nolanlawson/promise-worker
|
||||||
import PromiseWorker from 'promise-worker'
|
import PromiseWorker from 'promise-worker'
|
||||||
|
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
function getNewDatabase () {
|
function getNewDatabase () {
|
||||||
const worker = new Worker()
|
const worker = new Worker()
|
||||||
return new Database(worker)
|
return new Database(worker)
|
||||||
@@ -75,7 +77,12 @@ class Database {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||||
this.refreshSchema()
|
await this.refreshSchema()
|
||||||
|
|
||||||
|
events.send('database.import', file ? file.size : 0, {
|
||||||
|
from: file ? 'sqlite' : 'none',
|
||||||
|
new_db: true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshSchema () {
|
async refreshSchema () {
|
||||||
@@ -114,6 +121,7 @@ class Database {
|
|||||||
throw new Error(data.error)
|
throw new Error(data.error)
|
||||||
}
|
}
|
||||||
fu.exportToFile(data, fileName)
|
fu.exportToFile(data, fileName)
|
||||||
|
events.send('database.export', data.byteLength, { to: 'sqlite' })
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateTableName (name) {
|
async validateTableName (name) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
import fu from '@/lib/utils/fileIo'
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
import migration from './_migrations'
|
import migration from './_migrations'
|
||||||
|
|
||||||
const migrate = migration._migrate
|
const migrate = migration._migrate
|
||||||
@@ -32,17 +33,16 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
isTabNeedName (inquiryTab) {
|
isTabNeedName (inquiryTab) {
|
||||||
const isFromScratch = !inquiryTab.initName
|
return inquiryTab.isPredefined || !inquiryTab.name
|
||||||
return inquiryTab.isPredefined || isFromScratch
|
|
||||||
},
|
},
|
||||||
|
|
||||||
save (inquiryTab, newName) {
|
save (inquiryTab, newName) {
|
||||||
const value = {
|
const value = {
|
||||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||||
query: inquiryTab.query,
|
query: inquiryTab.query,
|
||||||
viewType: inquiryTab.$refs.dataView.mode,
|
viewType: inquiryTab.dataView.mode,
|
||||||
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(),
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
name: newName || inquiryTab.initName
|
name: newName || inquiryTab.name
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get inquiries from local storage
|
// Get inquiries from local storage
|
||||||
@@ -103,9 +103,19 @@ export default {
|
|||||||
importInquiries () {
|
importInquiries () {
|
||||||
return fu.importFile()
|
return fu.importFile()
|
||||||
.then(str => {
|
.then(str => {
|
||||||
return this.deserialiseInquiries(str)
|
const inquires = this.deserialiseInquiries(str)
|
||||||
|
|
||||||
|
events.send('inquiry.import', inquires.length)
|
||||||
|
|
||||||
|
return inquires
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
export (inquiryList, fileName) {
|
||||||
|
const jsonStr = this.serialiseInquiries(inquiryList)
|
||||||
|
fu.exportToFile(jsonStr, fileName)
|
||||||
|
|
||||||
|
events.send('inquiry.export', inquiryList.length)
|
||||||
|
},
|
||||||
|
|
||||||
async readPredefinedInquiries () {
|
async readPredefinedInquiries () {
|
||||||
const res = await fu.readFile('./inquiries.json')
|
const res = await fu.readFile('./inquiries.json')
|
||||||
|
|||||||
59
src/lib/tab.js
Normal file
59
src/lib/tab.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default class Tab {
|
||||||
|
constructor (state, inquiry = {}) {
|
||||||
|
this.id = inquiry.id || nanoid()
|
||||||
|
this.name = inquiry.id ? inquiry.name : null
|
||||||
|
this.tempName = inquiry.name || (state.untitledLastIndex
|
||||||
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
|
: 'Untitled')
|
||||||
|
this.query = inquiry.query
|
||||||
|
this.viewOptions = inquiry.viewOptions || undefined
|
||||||
|
this.isPredefined = inquiry.isPredefined
|
||||||
|
this.viewType = inquiry.viewType || 'chart'
|
||||||
|
this.result = null
|
||||||
|
this.isGettingResults = false
|
||||||
|
this.error = null
|
||||||
|
this.time = 0
|
||||||
|
this.layout = inquiry.layout || {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
this.maximize = inquiry.maximize
|
||||||
|
|
||||||
|
this.isSaved = !!inquiry.id
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute () {
|
||||||
|
this.isGettingResults = true
|
||||||
|
this.result = null
|
||||||
|
this.error = null
|
||||||
|
const db = this.state.db
|
||||||
|
try {
|
||||||
|
const start = new Date()
|
||||||
|
this.result = await db.execute(this.query + ';')
|
||||||
|
this.time = time.getPeriod(start, new Date())
|
||||||
|
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send('resultset.create',
|
||||||
|
this.result.values[this.result.columns[0]].length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', parseFloat(this.time), { status: 'success' })
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', 0, { status: 'error' })
|
||||||
|
}
|
||||||
|
db.refreshSchema()
|
||||||
|
this.isGettingResults = false
|
||||||
|
}
|
||||||
|
}
|
||||||
12
src/lib/utils/events.js
Normal file
12
src/lib/utils/events.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
send (name, value, labels) {
|
||||||
|
const event = new CustomEvent('sqliteviz-app-event', {
|
||||||
|
detail: {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
labels: labels || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import events from '@/lib/utils/events'
|
||||||
let refresh = false
|
let refresh = false
|
||||||
|
|
||||||
function invokeServiceWorkerUpdateFlow (registration) {
|
function invokeServiceWorkerUpdateFlow (registration) {
|
||||||
@@ -41,4 +42,8 @@ if ('serviceWorker' in navigator) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
window.addEventListener('appinstalled', () => {
|
||||||
|
events.send('pwa.install')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import Workspace from '@/views/Main/Workspace'
|
|||||||
import Inquiries from '@/views/Main/Inquiries'
|
import Inquiries from '@/views/Main/Inquiries'
|
||||||
import Welcome from '@/views/Welcome'
|
import Welcome from '@/views/Welcome'
|
||||||
import Main from '@/views/Main'
|
import Main from '@/views/Main'
|
||||||
|
import LoadView from '@/views/LoadView'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
|
|
||||||
@@ -31,6 +32,11 @@ const routes = [
|
|||||||
component: Inquiries
|
component: Inquiries
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/load',
|
||||||
|
name: 'Load',
|
||||||
|
component: LoadView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -39,7 +45,7 @@ const router = new VueRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
if (!store.state.db) {
|
if (!store.state.db && to.name !== 'Load') {
|
||||||
const newDb = database.getNewDatabase()
|
const newDb = database.getNewDatabase()
|
||||||
await newDb.loadDb()
|
await newDb.loadDb()
|
||||||
store.commit('setDb', newDb)
|
store.commit('setDb', newDb)
|
||||||
|
|||||||
@@ -1,32 +1,17 @@
|
|||||||
import { nanoid } from 'nanoid'
|
import Tab from '@/lib/tab'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async addTab ({ state }, data) {
|
async addTab ({ state }, inquiry = {}) {
|
||||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
// add new tab only if it was not already opened
|
||||||
// If no data then create a new blank one...
|
if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
|
||||||
// No data.id means to create new tab, but not blank,
|
const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
|
||||||
// e.g. with 'select * from csv_import' inquiry after csv import
|
|
||||||
if (!data || !data.id) {
|
|
||||||
tab.id = nanoid()
|
|
||||||
tab.name = null
|
|
||||||
tab.tempName = state.untitledLastIndex
|
|
||||||
? `Untitled ${state.untitledLastIndex}`
|
|
||||||
: 'Untitled'
|
|
||||||
tab.viewType = 'chart'
|
|
||||||
tab.viewOptions = undefined
|
|
||||||
tab.isSaved = false
|
|
||||||
} else {
|
|
||||||
tab.isSaved = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// add new tab only if was not already opened
|
|
||||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
|
||||||
state.tabs.push(tab)
|
state.tabs.push(tab)
|
||||||
if (!tab.name) {
|
if (!tab.name) {
|
||||||
state.untitledLastIndex += 1
|
state.untitledLastIndex += 1
|
||||||
}
|
}
|
||||||
|
return tab.id
|
||||||
}
|
}
|
||||||
|
|
||||||
return tab.id
|
return inquiry.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
setDb (state, db) {
|
setDb (state, db) {
|
||||||
if (state.db) {
|
if (state.db) {
|
||||||
@@ -8,8 +6,8 @@ export default {
|
|||||||
state.db = db
|
state.db = db
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) {
|
updateTab (state, { tab, newValues }) {
|
||||||
const tab = state.tabs[index]
|
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||||
const oldId = tab.id
|
const oldId = tab.id
|
||||||
|
|
||||||
if (id && state.currentTabId === oldId) {
|
if (id && state.currentTabId === oldId) {
|
||||||
@@ -26,32 +24,41 @@ export default {
|
|||||||
// Saved inquiry is not predefined
|
// Saved inquiry is not predefined
|
||||||
delete tab.isPredefined
|
delete tab.isPredefined
|
||||||
}
|
}
|
||||||
|
|
||||||
Vue.set(state.tabs, index, tab)
|
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTab (state, index) {
|
deleteTab (state, tab) {
|
||||||
|
const index = state.tabs.indexOf(tab)
|
||||||
// If closing tab is the current opened
|
// If closing tab is the current opened
|
||||||
if (state.tabs[index].id === state.currentTabId) {
|
if (tab.id === state.currentTabId) {
|
||||||
if (index < state.tabs.length - 1) {
|
if (index < state.tabs.length - 1) {
|
||||||
state.currentTabId = state.tabs[index + 1].id
|
state.currentTabId = state.tabs[index + 1].id
|
||||||
} else if (index > 0) {
|
} else if (index > 0) {
|
||||||
state.currentTabId = state.tabs[index - 1].id
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
} else {
|
} else {
|
||||||
state.currentTabId = null
|
state.currentTabId = null
|
||||||
state.currentTab = null
|
|
||||||
state.untitledLastIndex = 0
|
state.untitledLastIndex = 0
|
||||||
}
|
}
|
||||||
|
state.currentTab = state.currentTabId
|
||||||
|
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
state.tabs.splice(index, 1)
|
state.tabs.splice(index, 1)
|
||||||
},
|
},
|
||||||
setCurrentTabId (state, id) {
|
setCurrentTabId (state, id) {
|
||||||
state.currentTabId = id
|
try {
|
||||||
},
|
state.currentTabId = id
|
||||||
setCurrentTab (state, tab) {
|
state.currentTab = state.tabs.find(tab => tab.id === id)
|
||||||
state.currentTab = tab
|
} catch (e) {
|
||||||
|
console.error('Can\'t open a tab id:' + id)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
updatePredefinedInquiries (state, inquiries) {
|
updatePredefinedInquiries (state, inquiries) {
|
||||||
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
||||||
|
},
|
||||||
|
setLoadingPredefinedInquiries (state, value) {
|
||||||
|
state.loadingPredefinedInquiries = value
|
||||||
|
},
|
||||||
|
setPredefinedInquiriesLoaded (state, value) {
|
||||||
|
state.predefinedInquiriesLoaded = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,7 @@ export default {
|
|||||||
currentTabId: null,
|
currentTabId: null,
|
||||||
untitledLastIndex: 0,
|
untitledLastIndex: 0,
|
||||||
predefinedInquiries: [],
|
predefinedInquiries: [],
|
||||||
|
loadingPredefinedInquiries: false,
|
||||||
|
predefinedInquiriesLoaded: false,
|
||||||
db: null
|
db: null
|
||||||
}
|
}
|
||||||
|
|||||||
200
src/views/LoadView.vue
Normal file
200
src/views/LoadView.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<logs
|
||||||
|
id="logs"
|
||||||
|
:messages="messages"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="hasErrors"
|
||||||
|
id="open-workspace-btn"
|
||||||
|
class="secondary"
|
||||||
|
@click="$router.push('/workspace?hide_schema=1')">
|
||||||
|
Open workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoadView',
|
||||||
|
components: {
|
||||||
|
Logs
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
newDb: null,
|
||||||
|
messages: [],
|
||||||
|
dataMsg: {},
|
||||||
|
inquiryMsg: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasErrors () {
|
||||||
|
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
const {
|
||||||
|
data_url: dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
inquiry_url: inquiryUrl,
|
||||||
|
inquiry_id: inquiryIds,
|
||||||
|
maximize
|
||||||
|
} = this.$route.query
|
||||||
|
|
||||||
|
events.send('share.load', null, {
|
||||||
|
has_data_url: !!dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
has_inquiry_url: !!inquiryUrl,
|
||||||
|
inquiry_id_count: (inquiryIds || []).length,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.loadData(dataUrl, dataFormat)
|
||||||
|
const inquiries = await this.loadInquiries(inquiryUrl, inquiryIds)
|
||||||
|
if (inquiries && inquiries.length > 0) {
|
||||||
|
await this.openInquiries(inquiries, maximize)
|
||||||
|
}
|
||||||
|
if (!this.hasErrors) {
|
||||||
|
this.$router.push('/workspace?hide_schema=1')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData (dataUrl, dataFormat) {
|
||||||
|
this.newDb = database.getNewDatabase()
|
||||||
|
if (dataUrl) {
|
||||||
|
this.dataMsg = {
|
||||||
|
message: 'Preparing data...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.dataMsg)
|
||||||
|
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingDataIndicator = setTimeout(() => {
|
||||||
|
if (this.dataMsg.type === 'info') {
|
||||||
|
this.dataMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
if (dataFormat === 'sqlite') {
|
||||||
|
await this.getSqliteDb(dataUrl)
|
||||||
|
} else {
|
||||||
|
this.dataMsg.message = 'Unknown data format'
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingDataIndicator)
|
||||||
|
} else {
|
||||||
|
await this.newDb.loadDb()
|
||||||
|
}
|
||||||
|
this.$store.commit('setDb', this.newDb)
|
||||||
|
},
|
||||||
|
async getSqliteDb (dataUrl) {
|
||||||
|
try {
|
||||||
|
const filename = new URL(dataUrl).pathname.split('/').pop()
|
||||||
|
const res = await fu.readFile(dataUrl)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fetching DB failed')
|
||||||
|
}
|
||||||
|
const file = await res.blob()
|
||||||
|
file.name = filename
|
||||||
|
|
||||||
|
await this.newDb.loadDb(file)
|
||||||
|
this.dataMsg.message = 'Data is ready'
|
||||||
|
this.dataMsg.type = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.dataMsg.message = error
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadInquiries (inquiryUrl, inquiryIds = []) {
|
||||||
|
if (!inquiryUrl) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingInquiriesIndicator = setTimeout(() => {
|
||||||
|
if (this.inquiryMsg.type === 'info') {
|
||||||
|
this.inquiryMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
try {
|
||||||
|
this.inquiryMsg = {
|
||||||
|
message: 'Preparing inquiries...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.inquiryMsg)
|
||||||
|
|
||||||
|
const res = await fu.readFile(inquiryUrl)
|
||||||
|
const file = await res.json()
|
||||||
|
|
||||||
|
this.inquiryMsg.message = 'Inquiries are ready'
|
||||||
|
this.inquiryMsg.type = 'success'
|
||||||
|
|
||||||
|
return inquiryIds.length > 0
|
||||||
|
? file.inquiries.filter(inquiry => inquiryIds.includes(inquiry.id))
|
||||||
|
: file.inquiries
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.inquiryMsg.message = error
|
||||||
|
this.inquiryMsg.type = 'error'
|
||||||
|
}
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingInquiriesIndicator)
|
||||||
|
},
|
||||||
|
async openInquiries (inquiries, maximize) {
|
||||||
|
let tabToOpen = null
|
||||||
|
const layout = maximize ? this.getLayout(maximize) : undefined
|
||||||
|
for (const inquiry of inquiries) {
|
||||||
|
const tabId = await this.$store.dispatch('addTab', {
|
||||||
|
...inquiry,
|
||||||
|
id: undefined,
|
||||||
|
layout,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
if (!tabToOpen) {
|
||||||
|
tabToOpen = tabId
|
||||||
|
this.$store.commit('setCurrentTabId', tabToOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.state.currentTab.execute()
|
||||||
|
},
|
||||||
|
|
||||||
|
getLayout (panelToMaximize) {
|
||||||
|
if (panelToMaximize === 'dataView') {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'hidden',
|
||||||
|
table: 'above',
|
||||||
|
dataView: 'bottom'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#logs {
|
||||||
|
margin: 8px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#open-workspace-btn {
|
||||||
|
margin: 16px auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -62,6 +62,7 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
#app-info-icon {
|
#app-info-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
#app-info-container {
|
#app-info-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div id="my-inquiries-container">
|
||||||
<div id="start-guide" v-if="allInquiries.length === 0">
|
<div id="start-guide" v-if="allInquiries.length === 0">
|
||||||
You don't have saved inquiries so far.
|
You don't have saved inquiries so far.
|
||||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
||||||
the one from scratch or
|
the one from scratch or
|
||||||
<span @click="importInquiries" class="link">import</span> from a file.
|
<span @click="importInquiries" class="link">import</span> from a file.
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
id="loading-predefined-status"
|
||||||
|
v-if="$store.state.loadingPredefinedInquiries"
|
||||||
|
>
|
||||||
|
<loading-indicator/>
|
||||||
|
Loading predefined inquiries...
|
||||||
|
</div>
|
||||||
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
|
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
|
||||||
<div id="my-inquiries-toolbar">
|
<div id="my-inquiries-toolbar">
|
||||||
<div id="toolbar-buttons">
|
<div id="toolbar-buttons">
|
||||||
@@ -157,9 +164,9 @@ import DeleteIcon from './svg/delete'
|
|||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import CheckBox from '@/components/CheckBox'
|
import CheckBox from '@/components/CheckBox'
|
||||||
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
import tooltipMixin from '@/tooltipMixin'
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
import fu from '@/lib/utils/fileIo'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Inquiries',
|
name: 'Inquiries',
|
||||||
@@ -170,7 +177,8 @@ export default {
|
|||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TextField,
|
TextField,
|
||||||
CheckBox
|
CheckBox,
|
||||||
|
LoadingIndicator
|
||||||
},
|
},
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
data () {
|
data () {
|
||||||
@@ -249,15 +257,21 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created () {
|
async created () {
|
||||||
storedInquiries.readPredefinedInquiries()
|
this.inquiries = storedInquiries.getStoredInquiries()
|
||||||
.then(inquiries => {
|
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
|
||||||
|
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
|
||||||
|
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
|
||||||
|
try {
|
||||||
|
this.$store.commit('setLoadingPredefinedInquiries', true)
|
||||||
|
const inquiries = await storedInquiries.readPredefinedInquiries()
|
||||||
this.$store.commit('updatePredefinedInquiries', inquiries)
|
this.$store.commit('updatePredefinedInquiries', inquiries)
|
||||||
})
|
this.$store.commit('setPredefinedInquiriesLoaded', true)
|
||||||
.catch(console.error)
|
} catch (e) {
|
||||||
.finally(() => {
|
console.error(e)
|
||||||
this.inquiries = storedInquiries.getStoredInquiries()
|
}
|
||||||
})
|
this.$store.commit('setLoadingPredefinedInquiries', false)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
|
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
|
||||||
@@ -324,12 +338,14 @@ export default {
|
|||||||
storedInquiries.updateStorage(this.inquiries)
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
|
|
||||||
// update tab, if renamed inquiry is opened
|
// update tab, if renamed inquiry is opened
|
||||||
const tabIndex = this.findTabIndex(processedInquiry.id)
|
const tab = this.$store.state.tabs
|
||||||
if (tabIndex >= 0) {
|
.find(tab => tab.id === processedInquiry.id)
|
||||||
|
if (tab) {
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
index: tabIndex,
|
tab,
|
||||||
name: this.newName,
|
newValues: {
|
||||||
id: processedInquiry.id
|
name: this.newName
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// hide dialog
|
// hide dialog
|
||||||
@@ -353,9 +369,10 @@ export default {
|
|||||||
this.inquiries.splice(this.processedInquiryIndex, 1)
|
this.inquiries.splice(this.processedInquiryIndex, 1)
|
||||||
|
|
||||||
// Close deleted inquiry tab if it was opened
|
// Close deleted inquiry tab if it was opened
|
||||||
const tabIndex = this.findTabIndex(this.processedInquiryId)
|
const tab = this.$store.state.tabs
|
||||||
if (tabIndex >= 0) {
|
.find(tab => tab.id === this.processedInquiryId)
|
||||||
this.$store.commit('deleteTab', tabIndex)
|
if (tab) {
|
||||||
|
this.$store.commit('deleteTab', tab)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear checkbox
|
// Clear checkbox
|
||||||
@@ -369,10 +386,12 @@ export default {
|
|||||||
|
|
||||||
// Close deleted inquiries if it was opened
|
// Close deleted inquiries if it was opened
|
||||||
const tabs = this.$store.state.tabs
|
const tabs = this.$store.state.tabs
|
||||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
let i = tabs.length - 1
|
||||||
|
while (i > -1) {
|
||||||
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
||||||
this.$store.commit('deleteTab', i)
|
this.$store.commit('deleteTab', tabs[i])
|
||||||
}
|
}
|
||||||
|
i--
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear checkboxes
|
// Clear checkboxes
|
||||||
@@ -381,12 +400,8 @@ export default {
|
|||||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||||
storedInquiries.updateStorage(this.inquiries)
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
},
|
},
|
||||||
findTabIndex (id) {
|
|
||||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
|
||||||
},
|
|
||||||
exportToFile (inquiryList, fileName) {
|
exportToFile (inquiryList, fileName) {
|
||||||
const jsonStr = storedInquiries.serialiseInquiries(inquiryList)
|
storedInquiries.export(inquiryList, fileName)
|
||||||
fu.exportToFile(jsonStr, fileName)
|
|
||||||
},
|
},
|
||||||
exportSelectedInquiries () {
|
exportSelectedInquiries () {
|
||||||
const inquiryList = this.allInquiries.filter(
|
const inquiryList = this.allInquiries.filter(
|
||||||
@@ -443,6 +458,21 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
#my-inquiries-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loading-predefined-status {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
#start-guide {
|
#start-guide {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<nav>
|
<nav>
|
||||||
<div>
|
<div id="nav-links">
|
||||||
|
<a href="https://sqliteviz.com">
|
||||||
|
<img :src="require('@/assets/images/logo_simple.svg')">
|
||||||
|
</a>
|
||||||
<router-link to="/workspace">Workspace</router-link>
|
<router-link to="/workspace">Workspace</router-link>
|
||||||
<router-link to="/inquiries">Inquiries</router-link>
|
<router-link to="/inquiries">Inquiries</router-link>
|
||||||
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
|
<a href="https://sqliteviz.com/docs" target="_blank">Help</a>
|
||||||
</div>
|
</div>
|
||||||
<div id="nav-buttons">
|
<div id="nav-buttons">
|
||||||
<button
|
<button
|
||||||
@@ -57,6 +60,7 @@ import TextField from '@/components/TextField'
|
|||||||
import CloseIcon from '@/components/svg/close'
|
import CloseIcon from '@/components/svg/close'
|
||||||
import storedInquiries from '@/lib/storedInquiries'
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
import AppDiagnosticInfo from './AppDiagnosticInfo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MainMenu',
|
name: 'MainMenu',
|
||||||
@@ -76,19 +80,10 @@ export default {
|
|||||||
return this.$store.state.currentTab
|
return this.$store.state.currentTab
|
||||||
},
|
},
|
||||||
isSaved () {
|
isSaved () {
|
||||||
if (!this.currentInquiry) {
|
return this.currentInquiry && this.currentInquiry.isSaved
|
||||||
return false
|
|
||||||
}
|
|
||||||
const tabIndex = this.currentInquiry.tabIndex
|
|
||||||
const tab = this.$store.state.tabs[tabIndex]
|
|
||||||
return tab && tab.isSaved
|
|
||||||
},
|
},
|
||||||
isPredefined () {
|
isPredefined () {
|
||||||
if (this.currentInquiry) {
|
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||||
return this.currentInquiry.isPredefined
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
runDisabled () {
|
runDisabled () {
|
||||||
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
||||||
@@ -110,6 +105,8 @@ export default {
|
|||||||
this.$router.push('/workspace')
|
this.$router.push('/workspace')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
events.send('inquiry.create', null, { auto: false })
|
||||||
},
|
},
|
||||||
cancelSave () {
|
cancelSave () {
|
||||||
this.$modal.hide('save')
|
this.$modal.hide('save')
|
||||||
@@ -139,13 +136,15 @@ export default {
|
|||||||
|
|
||||||
// Update tab in store
|
// Update tab in store
|
||||||
this.$store.commit('updateTab', {
|
this.$store.commit('updateTab', {
|
||||||
index: this.currentInquiry.tabIndex,
|
tab: this.currentInquiry,
|
||||||
name: value.name,
|
newValues: {
|
||||||
id: value.id,
|
name: value.name,
|
||||||
query: value.query,
|
id: value.id,
|
||||||
viewType: value.viewType,
|
query: value.query,
|
||||||
viewOptions: value.viewOptions,
|
viewType: value.viewType,
|
||||||
isSaved: true
|
viewOptions: value.viewOptions,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Restore data:
|
// Restore data:
|
||||||
@@ -163,6 +162,7 @@ export default {
|
|||||||
|
|
||||||
// Signal about saving
|
// Signal about saving
|
||||||
this.$root.$emit('inquirySaved')
|
this.$root.$emit('inquirySaved')
|
||||||
|
events.send('inquiry.save')
|
||||||
},
|
},
|
||||||
_keyListener (e) {
|
_keyListener (e) {
|
||||||
if (this.$route.path === '/workspace') {
|
if (this.$route.path === '/workspace') {
|
||||||
@@ -237,4 +237,12 @@ button {
|
|||||||
#nav-buttons {
|
#nav-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
#nav-links img {
|
||||||
|
width: 32px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
import TableDescription from './TableDescription'
|
import TableDescription from './TableDescription'
|
||||||
import TextField from '@/components/TextField'
|
import TextField from '@/components/TextField'
|
||||||
import TreeChevron from '@/components/svg/treeChevron'
|
import TreeChevron from '@/components/svg/treeChevron'
|
||||||
@@ -86,6 +87,11 @@ export default {
|
|||||||
csvImport.reset()
|
csvImport.reset()
|
||||||
await csvImport.previewCsv()
|
await csvImport.previewCsv()
|
||||||
csvImport.open()
|
csvImport.open()
|
||||||
|
|
||||||
|
events.send('database.import', this.file.size, {
|
||||||
|
from: 'csv',
|
||||||
|
new_db: false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,15 @@ import PlotlyEditor from 'react-chart-editor'
|
|||||||
import chartHelper from '@/lib/chartHelper'
|
import chartHelper from '@/lib/chartHelper'
|
||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
import dereference from 'react-chart-editor/lib/lib/dereference'
|
||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Chart',
|
name: 'Chart',
|
||||||
props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'],
|
props: [
|
||||||
|
'dataSources', 'initOptions',
|
||||||
|
'importToPngEnabled', 'importToSvgEnabled',
|
||||||
|
'forPivot'
|
||||||
|
],
|
||||||
components: {
|
components: {
|
||||||
PlotlyEditor
|
PlotlyEditor
|
||||||
},
|
},
|
||||||
@@ -60,6 +65,18 @@ export default {
|
|||||||
plotly.setPlotConfig({
|
plotly.setPlotConfig({
|
||||||
notifyOnLogging: 1
|
notifyOnLogging: 1
|
||||||
})
|
})
|
||||||
|
this.$watch(
|
||||||
|
() => this.state && this.state.data && this.state.data
|
||||||
|
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
|
||||||
|
.join(','),
|
||||||
|
(value) => {
|
||||||
|
events.send('viz_plotly.render', null, {
|
||||||
|
type: value,
|
||||||
|
pivot: !!this.forPivot
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import pivotHelper from './pivotHelper'
|
|||||||
import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart'
|
import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart'
|
||||||
import chartHelper from '@/lib/chartHelper'
|
import chartHelper from '@/lib/chartHelper'
|
||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
const ChartClass = Vue.extend(Chart)
|
const ChartClass = Vue.extend(Chart)
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -87,8 +88,17 @@ export default {
|
|||||||
'pivotOptions.rendererName': {
|
'pivotOptions.rendererName': {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler () {
|
handler () {
|
||||||
this.$emit('update:importToPngEnabled', this.pivotOptions.rendererName !== 'TSV Export')
|
this.$emit(
|
||||||
this.$emit('update:importToSvgEnabled', this.viewStandartChart || this.viewCustomChart)
|
'update:importToPngEnabled',
|
||||||
|
this.pivotOptions.rendererName !== 'TSV Export'
|
||||||
|
)
|
||||||
|
this.$emit(
|
||||||
|
'update:importToSvgEnabled',
|
||||||
|
this.viewStandartChart || this.viewCustomChart
|
||||||
|
)
|
||||||
|
events.send('viz_pivot.render', null, {
|
||||||
|
type: this.pivotOptions.rendererName
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pivotOptions () {
|
pivotOptions () {
|
||||||
@@ -179,11 +189,11 @@ export default {
|
|||||||
async prepareCopy () {
|
async prepareCopy () {
|
||||||
if (this.viewCustomChart) {
|
if (this.viewCustomChart) {
|
||||||
return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy()
|
return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy()
|
||||||
} else if (this.viewStandartChart) {
|
|
||||||
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
|
|
||||||
} else {
|
|
||||||
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
|
|
||||||
}
|
}
|
||||||
|
if (this.viewStandartChart) {
|
||||||
|
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
|
||||||
|
}
|
||||||
|
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveAsSvg () {
|
async saveAsSvg () {
|
||||||
@@ -198,20 +208,23 @@ export default {
|
|||||||
saveAsHtml () {
|
saveAsHtml () {
|
||||||
if (this.viewCustomChart) {
|
if (this.viewCustomChart) {
|
||||||
this.pivotOptions.rendererOptions.customChartComponent.saveAsHtml()
|
this.pivotOptions.rendererOptions.customChartComponent.saveAsHtml()
|
||||||
} else if (this.viewStandartChart) {
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.viewStandartChart) {
|
||||||
const chartState = chartHelper.getChartData(this.$refs.pivotOutput)
|
const chartState = chartHelper.getChartData(this.$refs.pivotOutput)
|
||||||
fIo.exportToFile(
|
fIo.exportToFile(
|
||||||
chartHelper.getHtml(chartState),
|
chartHelper.getHtml(chartState),
|
||||||
'chart.html',
|
'chart.html',
|
||||||
'text/html'
|
'text/html'
|
||||||
)
|
)
|
||||||
} else {
|
return
|
||||||
fIo.exportToFile(
|
|
||||||
pivotHelper.getPivotHtml(this.$refs.pivotOutput),
|
|
||||||
'pivot.html',
|
|
||||||
'text/html'
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
fIo.exportToFile(
|
||||||
|
pivotHelper.getPivotHtml(this.$refs.pivotOutput),
|
||||||
|
'pivot.html',
|
||||||
|
'text/html'
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ export function _getDataSources (pivotData) {
|
|||||||
|
|
||||||
function customChartRenderer (data, options) {
|
function customChartRenderer (data, options) {
|
||||||
options.customChartComponent.dataSources = _getDataSources(data)
|
options.customChartComponent.dataSources = _getDataSources(data)
|
||||||
|
options.customChartComponent.forPivot = true
|
||||||
|
|
||||||
options.customChartComponent.$mount()
|
options.customChartComponent.$mount()
|
||||||
|
|
||||||
return $(options.customChartComponent.$el)
|
return $(options.customChartComponent.$el)
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ import ClipboardIcon from '@/components/svg/clipboard'
|
|||||||
import cIo from '@/lib/utils/clipboardIo'
|
import cIo from '@/lib/utils/clipboardIo'
|
||||||
import loadingDialog from '@/components/LoadingDialog'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DataView',
|
name: 'DataView',
|
||||||
@@ -123,6 +124,11 @@ export default {
|
|||||||
dataToCopy: null
|
dataToCopy: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
plotlyInPivot () {
|
||||||
|
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
mode () {
|
mode () {
|
||||||
this.$emit('update')
|
this.$emit('update')
|
||||||
@@ -148,6 +154,7 @@ export default {
|
|||||||
*/
|
*/
|
||||||
await time.sleep(0)
|
await time.sleep(0)
|
||||||
this.$refs.viewComponent.saveAsPng()
|
this.$refs.viewComponent.saveAsPng()
|
||||||
|
this.exportSignal('png')
|
||||||
},
|
},
|
||||||
getOptionsForSave () {
|
getOptionsForSave () {
|
||||||
return this.$refs.viewComponent.getOptionsForSave()
|
return this.$refs.viewComponent.getOptionsForSave()
|
||||||
@@ -178,6 +185,7 @@ export default {
|
|||||||
async copyToClipboard () {
|
async copyToClipboard () {
|
||||||
cIo.copyImage(this.dataToCopy)
|
cIo.copyImage(this.dataToCopy)
|
||||||
this.$modal.hide('prepareCopy')
|
this.$modal.hide('prepareCopy')
|
||||||
|
this.exportSignal('clipboard')
|
||||||
},
|
},
|
||||||
cancelCopy () {
|
cancelCopy () {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
@@ -186,9 +194,26 @@ export default {
|
|||||||
|
|
||||||
saveAsSvg () {
|
saveAsSvg () {
|
||||||
this.$refs.viewComponent.saveAsSvg()
|
this.$refs.viewComponent.saveAsSvg()
|
||||||
|
this.exportSignal('svg')
|
||||||
},
|
},
|
||||||
saveAsHtml () {
|
saveAsHtml () {
|
||||||
this.$refs.viewComponent.saveAsHtml()
|
this.$refs.viewComponent.saveAsHtml()
|
||||||
|
this.exportSignal('html')
|
||||||
|
},
|
||||||
|
exportSignal (to) {
|
||||||
|
const eventLabels = { type: to }
|
||||||
|
|
||||||
|
if (this.mode === 'chart' || this.plotlyInPivot) {
|
||||||
|
eventLabels.pivot = this.plotlyInPivot
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send(
|
||||||
|
this.mode === 'chart' || this.plotlyInPivot
|
||||||
|
? 'viz_plotly.export'
|
||||||
|
: 'viz_pivot.export',
|
||||||
|
null,
|
||||||
|
eventLabels
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import fIo from '@/lib/utils/fileIo'
|
|||||||
import cIo from '@/lib/utils/clipboardIo'
|
import cIo from '@/lib/utils/clipboardIo'
|
||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import loadingDialog from '@/components/LoadingDialog'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RunResult',
|
name: 'RunResult',
|
||||||
@@ -117,10 +118,24 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
exportToCsv () {
|
exportToCsv () {
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send('resultset.export',
|
||||||
|
this.result.values[this.result.columns[0]].length,
|
||||||
|
{ to: 'csv' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
|
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
|
||||||
},
|
},
|
||||||
|
|
||||||
async prepareCopy () {
|
async prepareCopy () {
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send('resultset.export',
|
||||||
|
this.result.values[this.result.columns[0]].length,
|
||||||
|
{ to: 'clipboard' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if ('ClipboardItem' in window) {
|
if ('ClipboardItem' in window) {
|
||||||
this.preparingCopy = true
|
this.preparingCopy = true
|
||||||
this.$modal.show('prepareCSVCopy')
|
this.$modal.show('prepareCSVCopy')
|
||||||
|
|||||||
@@ -3,14 +3,20 @@ import 'codemirror/addon/hint/show-hint.js'
|
|||||||
import 'codemirror/addon/hint/sql-hint.js'
|
import 'codemirror/addon/hint/sql-hint.js'
|
||||||
import store from '@/store'
|
import store from '@/store'
|
||||||
|
|
||||||
|
function _getHintText (hint) {
|
||||||
|
return typeof hint === 'string' ? hint : hint.text
|
||||||
|
}
|
||||||
export function getHints (cm, options) {
|
export function getHints (cm, options) {
|
||||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
|
||||||
const result = CM.hint.sql(cm, options)
|
const result = CM.hint.sql(cm, options)
|
||||||
|
|
||||||
// Don't show the hint if there is only one option
|
// Don't show the hint if there is only one option
|
||||||
// and the token is already completed with this option
|
// and the replacingText is already equals to this option
|
||||||
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
|
const replacedText = cm.getRange(result.from, result.to).toUpperCase()
|
||||||
|
if (result.list.length === 1 &&
|
||||||
|
_getHintText(result.list[0]).toUpperCase() === replacedText) {
|
||||||
result.list = []
|
result.list = []
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,44 +3,45 @@
|
|||||||
<splitpanes
|
<splitpanes
|
||||||
class="query-results-splitter"
|
class="query-results-splitter"
|
||||||
horizontal
|
horizontal
|
||||||
:before="{ size: 50, max: 100 }"
|
:before="{ size: topPaneSize, max: 100 }"
|
||||||
:after="{ size: 50, max: 100 }"
|
:after="{ size: 100 - topPaneSize, max: 100 }"
|
||||||
|
:default="{ before: 50, after: 50 }"
|
||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<div :id="'above-' + tabIndex" class="above" />
|
<div :id="'above-' + tab.id" class="above" />
|
||||||
</template>
|
</template>
|
||||||
<template #right-pane>
|
<template #right-pane>
|
||||||
<div :id="'bottom-'+ tabIndex" ref="bottomPane" class="bottomPane" />
|
<div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
|
||||||
</template>
|
</template>
|
||||||
</splitpanes>
|
</splitpanes>
|
||||||
|
|
||||||
<div :id="'hidden-'+ tabIndex" class="hidden-part" />
|
<div :id="'hidden-'+ tab.id" class="hidden-part" />
|
||||||
|
|
||||||
<teleport :to="`#${layout.sqlEditor}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.sqlEditor}-${tab.id}`">
|
||||||
<sql-editor
|
<sql-editor
|
||||||
ref="sqlEditor"
|
ref="sqlEditor"
|
||||||
v-model="query"
|
v-model="tab.query"
|
||||||
:is-getting-results="isGettingResults"
|
:is-getting-results="tab.isGettingResults"
|
||||||
@switchTo="onSwitchView('sqlEditor', $event)"
|
@switchTo="onSwitchView('sqlEditor', $event)"
|
||||||
@run="execute"
|
@run="tab.execute()"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|
||||||
<teleport :to="`#${layout.table}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
||||||
<run-result
|
<run-result
|
||||||
:result="result"
|
:result="tab.result"
|
||||||
:is-getting-results="isGettingResults"
|
:is-getting-results="tab.isGettingResults"
|
||||||
:error="error"
|
:error="tab.error"
|
||||||
:time="time"
|
:time="tab.time"
|
||||||
@switchTo="onSwitchView('table', $event)"
|
@switchTo="onSwitchView('table', $event)"
|
||||||
/>
|
/>
|
||||||
</teleport>
|
</teleport>
|
||||||
|
|
||||||
<teleport :to="`#${layout.dataView}-${tabIndex}`">
|
<teleport :to="`#${tab.layout.dataView}-${tab.id}`">
|
||||||
<data-view
|
<data-view
|
||||||
:data-source="(result && result.values) || null"
|
:data-source="(tab.result && tab.result.values) || null"
|
||||||
:init-options="initViewOptions"
|
:init-options="tab.viewOptions"
|
||||||
:init-mode="initViewType"
|
:init-mode="tab.viewType"
|
||||||
ref="dataView"
|
ref="dataView"
|
||||||
@switchTo="onSwitchView('dataView', $event)"
|
@switchTo="onSwitchView('dataView', $event)"
|
||||||
@update="onDataViewUpdate"
|
@update="onDataViewUpdate"
|
||||||
@@ -54,14 +55,15 @@ import Splitpanes from '@/components/Splitpanes'
|
|||||||
import SqlEditor from './SqlEditor'
|
import SqlEditor from './SqlEditor'
|
||||||
import DataView from './DataView'
|
import DataView from './DataView'
|
||||||
import RunResult from './RunResult'
|
import RunResult from './RunResult'
|
||||||
import time from '@/lib/utils/time'
|
|
||||||
import Teleport from 'vue2-teleport'
|
import Teleport from 'vue2-teleport'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Tab',
|
name: 'Tab',
|
||||||
props: [
|
props: {
|
||||||
'id', 'initName', 'initQuery', 'initViewOptions', 'tabIndex', 'isPredefined', 'initViewType'
|
tab: Object
|
||||||
],
|
},
|
||||||
components: {
|
components: {
|
||||||
SqlEditor,
|
SqlEditor,
|
||||||
DataView,
|
DataView,
|
||||||
@@ -71,21 +73,14 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
query: this.initQuery,
|
topPaneSize: this.tab.maximize
|
||||||
result: null,
|
? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
|
||||||
isGettingResults: false,
|
: 50
|
||||||
error: null,
|
|
||||||
time: 0,
|
|
||||||
layout: {
|
|
||||||
sqlEditor: 'above',
|
|
||||||
table: 'bottom',
|
|
||||||
dataView: 'hidden'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isActive () {
|
isActive () {
|
||||||
return this.id === this.$store.state.currentTabId
|
return this.tab.id === this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
@@ -93,42 +88,34 @@ export default {
|
|||||||
immediate: true,
|
immediate: true,
|
||||||
async handler () {
|
async handler () {
|
||||||
if (this.isActive) {
|
if (this.isActive) {
|
||||||
this.$store.commit('setCurrentTab', this)
|
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
this.$refs.sqlEditor.focus()
|
this.$refs.sqlEditor.focus()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
query () {
|
'tab.query' () {
|
||||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
this.$store.commit('updateTab', {
|
||||||
|
tab: this.tab,
|
||||||
|
newValues: { isSaved: false }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
mounted () {
|
||||||
|
this.tab.dataView = this.$refs.dataView
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSwitchView (from, to) {
|
onSwitchView (from, to) {
|
||||||
const fromPosition = this.layout[from]
|
const fromPosition = this.tab.layout[from]
|
||||||
this.layout[from] = this.layout[to]
|
this.tab.layout[from] = this.tab.layout[to]
|
||||||
this.layout[to] = fromPosition
|
this.tab.layout[to] = fromPosition
|
||||||
|
|
||||||
|
events.send('inquiry.panel', null, { panel: to })
|
||||||
},
|
},
|
||||||
onDataViewUpdate () {
|
onDataViewUpdate () {
|
||||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
this.$store.commit('updateTab', {
|
||||||
},
|
tab: this.tab,
|
||||||
async execute () {
|
newValues: { isSaved: false }
|
||||||
this.isGettingResults = true
|
})
|
||||||
this.result = null
|
|
||||||
this.error = null
|
|
||||||
const state = this.$store.state
|
|
||||||
try {
|
|
||||||
const start = new Date()
|
|
||||||
this.result = await state.db.execute(this.query + ';')
|
|
||||||
this.time = time.getPeriod(start, new Date())
|
|
||||||
} catch (err) {
|
|
||||||
this.error = {
|
|
||||||
type: 'error',
|
|
||||||
message: err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
state.db.refreshSchema()
|
|
||||||
this.isGettingResults = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
v-for="(tab, index) in tabs"
|
v-for="(tab, index) in tabs"
|
||||||
:key="index"
|
:key="index"
|
||||||
@click="selectTab(tab.id)"
|
@click="selectTab(tab.id)"
|
||||||
:class="[{'tab-selected': (tab.id === selectedIndex)}, 'tab']"
|
:class="[{'tab-selected': (tab.id === selectedTabId)}, 'tab']"
|
||||||
>
|
>
|
||||||
<div class="tab-name">
|
<div class="tab-name">
|
||||||
<span v-show="!tab.isSaved" class="star">*</span>
|
<span v-show="!tab.isSaved" class="star">*</span>
|
||||||
@@ -13,20 +13,14 @@
|
|||||||
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(index)"/>
|
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(tab)"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<tab
|
<tab
|
||||||
v-for="(tab, index) in tabs"
|
v-for="tab in tabs"
|
||||||
:key="tab.id"
|
:key="tab.id"
|
||||||
:id="tab.id"
|
:tab="tab"
|
||||||
:init-name="tab.name"
|
|
||||||
:init-query="tab.query"
|
|
||||||
:init-view-options="tab.viewOptions"
|
|
||||||
:init-view-type="tab.viewType"
|
|
||||||
:is-predefined="tab.isPredefined"
|
|
||||||
:tab-index="index"
|
|
||||||
/>
|
/>
|
||||||
<div v-show="tabs.length === 0" id="start-guide">
|
<div v-show="tabs.length === 0" id="start-guide">
|
||||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
||||||
@@ -38,25 +32,25 @@
|
|||||||
<modal name="close-warn" classes="dialog" height="auto">
|
<modal name="close-warn" classes="dialog" height="auto">
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
Close tab {{
|
Close tab {{
|
||||||
closingTabIndex !== null
|
closingTab !== null
|
||||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||||
: ''
|
: ''
|
||||||
}}
|
}}
|
||||||
<close-icon @click="$modal.hide('close-warn')"/>
|
<close-icon @click="$modal.hide('close-warn')"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
You have unsaved changes. Save changes in {{
|
You have unsaved changes. Save changes in {{
|
||||||
closingTabIndex !== null
|
closingTab !== null
|
||||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||||
: ''
|
: ''
|
||||||
}} before closing?
|
}} before closing?
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button class="secondary" @click="closeTab(closingTabIndex)">
|
<button class="secondary" @click="closeTab(closingTab)">
|
||||||
Close without saving
|
Close without saving
|
||||||
</button>
|
</button>
|
||||||
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
|
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
|
||||||
<button class="primary" @click="saveAndClose(closingTabIndex)">Save and close</button>
|
<button class="primary" @click="saveAndClose(closingTab)">Save and close</button>
|
||||||
</div>
|
</div>
|
||||||
</modal>
|
</modal>
|
||||||
</div>
|
</div>
|
||||||
@@ -73,14 +67,14 @@ export default {
|
|||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
closingTabIndex: null
|
closingTab: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
tabs () {
|
tabs () {
|
||||||
return this.$store.state.tabs
|
return this.$store.state.tabs
|
||||||
},
|
},
|
||||||
selectedIndex () {
|
selectedTabId () {
|
||||||
return this.$store.state.currentTabId
|
return this.$store.state.currentTabId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -97,25 +91,24 @@ export default {
|
|||||||
selectTab (id) {
|
selectTab (id) {
|
||||||
this.$store.commit('setCurrentTabId', id)
|
this.$store.commit('setCurrentTabId', id)
|
||||||
},
|
},
|
||||||
beforeCloseTab (index) {
|
beforeCloseTab (tab) {
|
||||||
this.closingTabIndex = index
|
this.closingTab = tab
|
||||||
if (!this.tabs[index].isSaved) {
|
if (!tab.isSaved) {
|
||||||
this.$modal.show('close-warn')
|
this.$modal.show('close-warn')
|
||||||
} else {
|
} else {
|
||||||
this.closeTab(index)
|
this.closeTab(tab)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
closeTab (index) {
|
closeTab (tab) {
|
||||||
this.$modal.hide('close-warn')
|
this.$modal.hide('close-warn')
|
||||||
this.closingTabIndex = null
|
this.$store.commit('deleteTab', tab)
|
||||||
this.$store.commit('deleteTab', index)
|
|
||||||
},
|
},
|
||||||
saveAndClose (index) {
|
saveAndClose (tab) {
|
||||||
this.$root.$on('inquirySaved', () => {
|
this.$root.$on('inquirySaved', () => {
|
||||||
this.closeTab(index)
|
this.closeTab(tab)
|
||||||
this.$root.$off('inquirySaved')
|
this.$root.$off('inquirySaved')
|
||||||
})
|
})
|
||||||
this.selectTab(this.tabs[index].id)
|
this.selectTab(tab.id)
|
||||||
this.$modal.hide('close-warn')
|
this.$modal.hide('close-warn')
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
this.$root.$emit('saveInquiry')
|
this.$root.$emit('saveInquiry')
|
||||||
|
|||||||
@@ -2,8 +2,9 @@
|
|||||||
<div>
|
<div>
|
||||||
<splitpanes
|
<splitpanes
|
||||||
class="schema-tabs-splitter"
|
class="schema-tabs-splitter"
|
||||||
:before="{ size: 20, max: 30 }"
|
:before="{ size: schemaWidth, max: 30 }"
|
||||||
:after="{ size: 80, max: 100 }"
|
:after="{ size: 100 - schemaWidth, max: 100 }"
|
||||||
|
:default="{ before: 20, after: 80 }"
|
||||||
>
|
>
|
||||||
<template #left-pane>
|
<template #left-pane>
|
||||||
<schema/>
|
<schema/>
|
||||||
@@ -19,6 +20,7 @@
|
|||||||
import Splitpanes from '@/components/Splitpanes'
|
import Splitpanes from '@/components/Splitpanes'
|
||||||
import Schema from './Schema'
|
import Schema from './Schema'
|
||||||
import Tabs from './Tabs'
|
import Tabs from './Tabs'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Workspace',
|
name: 'Workspace',
|
||||||
@@ -27,9 +29,14 @@ export default {
|
|||||||
Splitpanes,
|
Splitpanes,
|
||||||
Tabs
|
Tabs
|
||||||
},
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
schemaWidth: this.$route.query.hide_schema === '1' ? 0 : 20
|
||||||
|
}
|
||||||
|
},
|
||||||
async beforeCreate () {
|
async beforeCreate () {
|
||||||
const schema = this.$store.state.db.schema
|
const schema = this.$store.state.db.schema
|
||||||
if (!schema || schema.length === 0) {
|
if ((!schema || schema.length === 0) && this.$store.state.tabs.length === 0) {
|
||||||
const stmt = [
|
const stmt = [
|
||||||
'/*',
|
'/*',
|
||||||
' * Your database is empty. In order to start building charts',
|
' * Your database is empty. In order to start building charts',
|
||||||
@@ -49,6 +56,8 @@ export default {
|
|||||||
|
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
this.$store.commit('setCurrentTabId', tabId)
|
this.$store.commit('setCurrentTabId', tabId)
|
||||||
|
|
||||||
|
events.send('inquiry.create', null, { auto: true })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ describe('Logs.vue', () => {
|
|||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
messages.push({ type: 'error', message: 'msg 5' })
|
messages.push({ type: 'error', message: 'msg 5' })
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
const height = wrapper.find('.logs-container').element.scrollHeight
|
const height = wrapper.find('.logs-container').element.scrollHeight
|
||||||
expect(wrapper.find('.logs-container').element.scrollTop)
|
expect(wrapper.find('.logs-container').element.scrollTop)
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ describe('Splitpanes.vue', () => {
|
|||||||
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%')
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('toggles correctly', async () => {
|
it('toggles correctly - no maximized initially', async () => {
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = shallowMount(Splitpanes, {
|
const wrapper = shallowMount(Splitpanes, {
|
||||||
slots: {
|
slots: {
|
||||||
@@ -70,6 +70,64 @@ describe('Splitpanes.vue', () => {
|
|||||||
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%')
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('toggles correctly - with maximized initially', async () => {
|
||||||
|
// mount the component
|
||||||
|
let wrapper = shallowMount(Splitpanes, {
|
||||||
|
slots: {
|
||||||
|
leftPane: '<div />',
|
||||||
|
rightPane: '<div />'
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
before: { size: 0, max: 100 },
|
||||||
|
after: { size: 100, max: 100 },
|
||||||
|
default: { before: 20, after: 80 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
|
||||||
|
|
||||||
|
wrapper = shallowMount(Splitpanes, {
|
||||||
|
slots: {
|
||||||
|
leftPane: '<div />',
|
||||||
|
rightPane: '<div />'
|
||||||
|
},
|
||||||
|
propsData: {
|
||||||
|
before: { size: 100, max: 100 },
|
||||||
|
after: { size: 0, max: 100 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
|
||||||
|
|
||||||
|
await wrapper.find('.toggle-btn').trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
|
||||||
|
|
||||||
|
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
|
||||||
|
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
|
||||||
|
})
|
||||||
|
|
||||||
it('drag - vertical', async () => {
|
it('drag - vertical', async () => {
|
||||||
const root = document.createElement('div')
|
const root = document.createElement('div')
|
||||||
const place = document.createElement('div')
|
const place = document.createElement('div')
|
||||||
|
|||||||
@@ -116,6 +116,33 @@ describe('csv.js', () => {
|
|||||||
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
await expect(csv.parse(file)).to.be.rejectedWith(err)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('parse rejects when getResult failed', async () => {
|
||||||
|
let err
|
||||||
|
try {
|
||||||
|
new Date('invalid date').toISOString()
|
||||||
|
} catch (e) {
|
||||||
|
err = e // get error message, it's different depending on browser
|
||||||
|
}
|
||||||
|
sinon.stub(Papa, 'parse').callsFake((file, config) => {
|
||||||
|
config.complete({
|
||||||
|
data: [
|
||||||
|
[1, new Date('invalid date')],
|
||||||
|
[2, new Date('2023-05-05T15:30:00Z')]
|
||||||
|
],
|
||||||
|
errors: [],
|
||||||
|
meta: {
|
||||||
|
delimiter: ',',
|
||||||
|
linebreak: '\n',
|
||||||
|
aborted: false,
|
||||||
|
truncated: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const file = {}
|
||||||
|
await expect(csv.parse(file)).to.be.rejectedWith(err.message)
|
||||||
|
})
|
||||||
|
|
||||||
it('prepareForExport', () => {
|
it('prepareForExport', () => {
|
||||||
const resultSet = {
|
const resultSet = {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ describe('database.js', () => {
|
|||||||
expect(schema[0].columns[0].type).to.equal('N/A')
|
expect(schema[0].columns[0].type).to.equal('N/A')
|
||||||
|
|
||||||
expect(schema[0].columns[1].name).to.equal('col2')
|
expect(schema[0].columns[1].name).to.equal('col2')
|
||||||
expect(schema[0].columns[1].type).to.equal('integer')
|
expect(schema[0].columns[1].type).to.equal('INTEGER')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('creates schema with view', async () => {
|
it('creates schema with view', async () => {
|
||||||
@@ -68,12 +68,12 @@ describe('database.js', () => {
|
|||||||
|
|
||||||
expect(schema[0].columns[1]).to.eql({
|
expect(schema[0].columns[1]).to.eql({
|
||||||
name: 'col2',
|
name: 'col2',
|
||||||
type: 'integer'
|
type: 'INTEGER'
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(schema[1].columns).to.eql([{
|
expect(schema[1].columns).to.eql([{
|
||||||
name: 'amount',
|
name: 'amount',
|
||||||
type: 'integer'
|
type: 'INTEGER'
|
||||||
}])
|
}])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -160,39 +160,39 @@ describe('SQLite extensions', function () {
|
|||||||
it('supports transitive_closure', async function () {
|
it('supports transitive_closure', async function () {
|
||||||
const actual = await db.execute(`
|
const actual = await db.execute(`
|
||||||
CREATE TABLE node(
|
CREATE TABLE node(
|
||||||
node_id INTEGER NOT NULL PRIMARY KEY,
|
node_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
parent_id INTEGER,
|
parent_id INTEGER,
|
||||||
name VARCHAR(127),
|
name VARCHAR(127),
|
||||||
FOREIGN KEY (parent_id) REFERENCES node(node_id)
|
FOREIGN KEY (parent_id) REFERENCES node(node_id)
|
||||||
);
|
|
||||||
CREATE INDEX node_parent_id_idx ON node(parent_id);
|
|
||||||
|
|
||||||
CREATE VIRTUAL TABLE node_closure USING transitive_closure(
|
|
||||||
tablename = "node",
|
|
||||||
idcolumn = "node_id",
|
|
||||||
parentcolumn = "parent_id"
|
|
||||||
);
|
);
|
||||||
|
CREATE INDEX node_parent_id_idx ON node(parent_id);
|
||||||
|
|
||||||
INSERT INTO node VALUES
|
CREATE VIRTUAL TABLE node_closure USING transitive_closure(
|
||||||
(1, NULL, 'tests'),
|
tablename = "node",
|
||||||
(2, 1, 'lib'),
|
idcolumn = "node_id",
|
||||||
(3, 2, 'database'),
|
parentcolumn = "parent_id"
|
||||||
(4, 2, 'utils'),
|
);
|
||||||
(5, 2, 'storedQueries.spec.js'),
|
|
||||||
(6, 3, '_sql.spec.js'),
|
|
||||||
(7, 3, '_statements.spec.js'),
|
|
||||||
(8, 3, 'database.spec.js'),
|
|
||||||
(9, 3, 'sqliteExtensions.spec.js'),
|
|
||||||
(10, 4, 'fileIo.spec.js'),
|
|
||||||
(11, 4, 'time.spec.js');
|
|
||||||
|
|
||||||
SELECT name
|
INSERT INTO node VALUES
|
||||||
FROM node
|
(1, NULL, 'tests'),
|
||||||
WHERE node_id IN (
|
(2, 1, 'lib'),
|
||||||
SELECT nc.id
|
(3, 2, 'database'),
|
||||||
FROM node_closure AS nc
|
(4, 2, 'utils'),
|
||||||
WHERE nc.root = 2 AND nc.depth = 2
|
(5, 2, 'storedQueries.spec.js'),
|
||||||
);
|
(6, 3, '_sql.spec.js'),
|
||||||
|
(7, 3, '_statements.spec.js'),
|
||||||
|
(8, 3, 'database.spec.js'),
|
||||||
|
(9, 3, 'sqliteExtensions.spec.js'),
|
||||||
|
(10, 4, 'fileIo.spec.js'),
|
||||||
|
(11, 4, 'time.spec.js');
|
||||||
|
|
||||||
|
SELECT name
|
||||||
|
FROM node
|
||||||
|
WHERE node_id IN (
|
||||||
|
SELECT nc.id
|
||||||
|
FROM node_closure AS nc
|
||||||
|
WHERE nc.root = 2 AND nc.depth = 2
|
||||||
|
);
|
||||||
`)
|
`)
|
||||||
expect(actual.values).to.eql({
|
expect(actual.values).to.eql({
|
||||||
name: [
|
name: [
|
||||||
@@ -293,7 +293,7 @@ describe('SQLite extensions', function () {
|
|||||||
|
|
||||||
it('supports decimal', async function () {
|
it('supports decimal', async function () {
|
||||||
const actual = await db.execute(`
|
const actual = await db.execute(`
|
||||||
select
|
SELECT
|
||||||
decimal_add(decimal('0.1'), decimal('0.2')) "add",
|
decimal_add(decimal('0.1'), decimal('0.2')) "add",
|
||||||
decimal_sub(0.2, 0.1) sub,
|
decimal_sub(0.2, 0.1) sub,
|
||||||
decimal_mul(power(2, 69), 2) mul,
|
decimal_mul(power(2, 69), 2) mul,
|
||||||
@@ -430,4 +430,29 @@ describe('SQLite extensions', function () {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('supports pearson', async function () {
|
||||||
|
const actual = await db.execute(`
|
||||||
|
CREATE TABLE dataset(x REAL, y REAL, z REAL);
|
||||||
|
INSERT INTO dataset VALUES
|
||||||
|
(5,3,3.2), (5,6,4.3), (5,9,5.4),
|
||||||
|
(10,3,4), (10,6,3.8), (10,9,3.6),
|
||||||
|
(15,3,4.8), (15,6,4), (15,9,3.5);
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
pearson(x, x) xx,
|
||||||
|
pearson(x, y) xy,
|
||||||
|
abs(-0.12666 - pearson(x, z)) < 0.00001 xz,
|
||||||
|
pearson(y, x) yx,
|
||||||
|
pearson(y, y) yy,
|
||||||
|
abs(0.10555 - pearson(y, z)) < 0.00001 yz,
|
||||||
|
abs(-0.12666 - pearson(z, x)) < 0.00001 zx,
|
||||||
|
abs(0.10555 - pearson(z, y)) < 0.00001 zy,
|
||||||
|
pearson(z, z) zz
|
||||||
|
FROM dataset;
|
||||||
|
`)
|
||||||
|
expect(actual.values).to.eql({
|
||||||
|
xx: [1], xy: [0], xz: [1], yx: [0], yy: [1], yz: [1], zx: [1], zy: [1], zz: [1]
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -87,14 +87,14 @@ describe('storedInquiries.js', () => {
|
|||||||
|
|
||||||
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
|
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: 'foo'
|
name: 'foo'
|
||||||
}
|
}
|
||||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
|
expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
|
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: null,
|
name: null,
|
||||||
tempName: 'Untitled'
|
tempName: 'Untitled'
|
||||||
}
|
}
|
||||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
|
expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
|
||||||
@@ -102,7 +102,7 @@ describe('storedInquiries.js', () => {
|
|||||||
|
|
||||||
it('isTabNeedName returns true when the inquiry is predefined', () => {
|
it('isTabNeedName returns true when the inquiry is predefined', () => {
|
||||||
const tab = {
|
const tab = {
|
||||||
initName: 'foo',
|
name: 'foo',
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,14 +351,13 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: null,
|
name: null,
|
||||||
$refs: {
|
dataView: {
|
||||||
dataView: {
|
getOptionsForSave () {
|
||||||
getOptionsForSave () {
|
return ['chart']
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
const value = storedInquiries.save(tab, 'foo')
|
const value = storedInquiries.save(tab, 'foo')
|
||||||
expect(value.id).to.equal(tab.id)
|
expect(value.id).to.equal(tab.id)
|
||||||
@@ -376,19 +375,18 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: null,
|
name: null,
|
||||||
$refs: {
|
dataView: {
|
||||||
dataView: {
|
getOptionsForSave () {
|
||||||
getOptionsForSave () {
|
return ['chart']
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const first = storedInquiries.save(tab, 'foo')
|
const first = storedInquiries.save(tab, 'foo')
|
||||||
|
|
||||||
tab.initName = 'foo'
|
tab.name = 'foo'
|
||||||
tab.query = 'select * from foo'
|
tab.query = 'select * from foo'
|
||||||
storedInquiries.save(tab)
|
storedInquiries.save(tab)
|
||||||
const inquiries = storedInquiries.getStoredInquiries()
|
const inquiries = storedInquiries.getStoredInquiries()
|
||||||
@@ -409,12 +407,10 @@ describe('storedInquiries.js', () => {
|
|||||||
query: 'select * from foo',
|
query: 'select * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: [],
|
viewOptions: [],
|
||||||
initName: 'foo predefined',
|
name: 'foo predefined',
|
||||||
$refs: {
|
dataView: {
|
||||||
dataView: {
|
getOptionsForSave () {
|
||||||
getOptionsForSave () {
|
return ['chart']
|
||||||
return ['chart']
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isPredefined: true
|
isPredefined: true
|
||||||
|
|||||||
189
tests/lib/tab.spec.js
Normal file
189
tests/lib/tab.spec.js
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import Tab from '@/lib/tab.js'
|
||||||
|
|
||||||
|
describe('tab.js', () => {
|
||||||
|
it('Creates a tab for new inquiry', () => {
|
||||||
|
const state = {
|
||||||
|
untitledLastIndex: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state)
|
||||||
|
expect(newTab).to.include({
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled 5',
|
||||||
|
query: undefined,
|
||||||
|
viewOptions: undefined,
|
||||||
|
isPredefined: undefined,
|
||||||
|
viewType: 'chart',
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: false,
|
||||||
|
state: state
|
||||||
|
})
|
||||||
|
expect(newTab.layout).to.include({
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
})
|
||||||
|
expect(newTab.id).to.have.lengthOf(21)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Creates a tab for existing inquiry', () => {
|
||||||
|
const state = {
|
||||||
|
untitledLastIndex: 5
|
||||||
|
}
|
||||||
|
|
||||||
|
const inquiry = {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
viewType: 'pivot',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, inquiry)
|
||||||
|
expect(newTab).to.include({
|
||||||
|
id: 'qwerty',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
tempName: 'Foo inquiry',
|
||||||
|
query: 'SELECT * from foo',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
isPredefined: undefined,
|
||||||
|
viewType: 'pivot',
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true,
|
||||||
|
state: state
|
||||||
|
})
|
||||||
|
expect(newTab.layout).to.include({
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Set isGettingResults true when execute', async () => {
|
||||||
|
let resolveQuering
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().returns(new Promise(resolve => {
|
||||||
|
resolveQuering = resolve
|
||||||
|
})),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(newTab.isGettingResults).to.equal(false)
|
||||||
|
newTab.execute()
|
||||||
|
expect(newTab.isGettingResults).to.equal(true)
|
||||||
|
resolveQuering()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates result with query execution result', async () => {
|
||||||
|
const result = {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [1, 2],
|
||||||
|
name: ['Harry', 'Drako']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().resolves(result),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(newTab.isGettingResults).to.equal(false)
|
||||||
|
expect(newTab.result).to.eql(result)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates error with query execution error', async () => {
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().rejects(new Error('No such table')),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(newTab.error.type).to.eql('error')
|
||||||
|
expect(newTab.error.message.toString()).to.equal('Error: No such table')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Updates schema after query execution', async () => {
|
||||||
|
const result = {
|
||||||
|
columns: ['id', 'name'],
|
||||||
|
values: {
|
||||||
|
id: [],
|
||||||
|
name: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mock store state
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1,
|
||||||
|
dbName: 'fooDb',
|
||||||
|
db: {
|
||||||
|
execute: sinon.stub().resolves(result),
|
||||||
|
refreshSchema: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = new Tab(state, {
|
||||||
|
id: 'qwerty',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'cart',
|
||||||
|
viewOptions: 'this is view options object',
|
||||||
|
name: 'Foo inquiry',
|
||||||
|
createdAt: '2022-12-05T18:30:30'
|
||||||
|
})
|
||||||
|
|
||||||
|
await newTab.execute()
|
||||||
|
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -29,12 +29,12 @@ describe('time.js', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sleep resolves after n ms', async () => {
|
it('sleep resolves after n ms', async () => {
|
||||||
let before = performance.now()
|
let before = Date.now()
|
||||||
await time.sleep(10)
|
await time.sleep(10)
|
||||||
expect(performance.now() - before).to.be.least(10)
|
expect(Date.now() - before).to.be.least(10)
|
||||||
|
|
||||||
before = performance.now()
|
before = Date.now()
|
||||||
await time.sleep(30)
|
await time.sleep(30)
|
||||||
expect(performance.now() - before).to.be.least(30)
|
expect(Date.now() - before).to.be.least(30)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ describe('actions', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let id = await addTab({ state })
|
let id = await addTab({ state })
|
||||||
expect(state.tabs[0]).to.eql({
|
expect(state.tabs[0]).to.include({
|
||||||
id: id,
|
id: id,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled',
|
tempName: 'Untitled',
|
||||||
@@ -22,7 +22,7 @@ describe('actions', () => {
|
|||||||
expect(state.untitledLastIndex).to.equal(1)
|
expect(state.untitledLastIndex).to.equal(1)
|
||||||
|
|
||||||
id = await addTab({ state })
|
id = await addTab({ state })
|
||||||
expect(state.tabs[1]).to.eql({
|
expect(state.tabs[1]).to.include({
|
||||||
id: id,
|
id: id,
|
||||||
name: null,
|
name: null,
|
||||||
tempName: 'Untitled 1',
|
tempName: 'Untitled 1',
|
||||||
@@ -41,14 +41,13 @@ describe('actions', () => {
|
|||||||
const tab = {
|
const tab = {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'test',
|
name: 'test',
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
query: 'SELECT * from foo',
|
||||||
viewType: 'chart',
|
viewType: 'chart',
|
||||||
viewOptions: {},
|
viewOptions: 'an object with view options',
|
||||||
isSaved: true
|
isSaved: true
|
||||||
}
|
}
|
||||||
await addTab({ state }, tab)
|
await addTab({ state }, tab)
|
||||||
expect(state.tabs[0]).to.eql(tab)
|
expect(state.tabs[0]).to.include(tab)
|
||||||
expect(state.untitledLastIndex).to.equal(0)
|
expect(state.untitledLastIndex).to.equal(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ const {
|
|||||||
updateTab,
|
updateTab,
|
||||||
deleteTab,
|
deleteTab,
|
||||||
setCurrentTabId,
|
setCurrentTabId,
|
||||||
setCurrentTab,
|
|
||||||
updatePredefinedInquiries,
|
updatePredefinedInquiries,
|
||||||
setDb
|
setDb,
|
||||||
|
setLoadingPredefinedInquiries,
|
||||||
|
setPredefinedInquiriesLoaded
|
||||||
} = mutations
|
} = mutations
|
||||||
|
|
||||||
describe('mutations', () => {
|
describe('mutations', () => {
|
||||||
@@ -35,8 +36,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: false
|
isPredefined: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
@@ -49,7 +49,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs[0]).to.eql({
|
expect(state.tabs[0]).to.eql({
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
@@ -73,8 +73,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'new test',
|
name: 'new test',
|
||||||
query: 'SELECT * from bar',
|
query: 'SELECT * from bar',
|
||||||
@@ -88,7 +87,7 @@ describe('mutations', () => {
|
|||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
expect(state.tabs[0].id).to.equal(2)
|
expect(state.tabs[0].id).to.equal(2)
|
||||||
@@ -109,8 +108,7 @@ describe('mutations', () => {
|
|||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'new test'
|
name: 'new test'
|
||||||
}
|
}
|
||||||
@@ -119,7 +117,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[0].name).to.equal('new test')
|
expect(state.tabs[0].name).to.equal('new test')
|
||||||
@@ -139,8 +137,7 @@ describe('mutations', () => {
|
|||||||
isPredefined: true
|
isPredefined: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const newTab = {
|
const newValues = {
|
||||||
index: 0,
|
|
||||||
isSaved: false
|
isSaved: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,7 +145,7 @@ describe('mutations', () => {
|
|||||||
tabs: [tab]
|
tabs: [tab]
|
||||||
}
|
}
|
||||||
|
|
||||||
updateTab(state, newTab)
|
updateTab(state, { tab, newValues })
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[0].name).to.equal('test')
|
expect(state.tabs[0].name).to.equal('test')
|
||||||
@@ -179,13 +176,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 0)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(2)
|
expect(state.tabs[0].id).to.equal(2)
|
||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
|
expect(state.currentTab).to.eql(tab2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, last', () => {
|
it('deleteTab - opened, last', () => {
|
||||||
@@ -211,13 +210,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 1)
|
deleteTab(state, tab2)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.currentTabId).to.equal(1)
|
expect(state.currentTabId).to.equal(1)
|
||||||
|
expect(state.currentTab).to.eql(tab1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, in the middle', () => {
|
it('deleteTab - opened, in the middle', () => {
|
||||||
@@ -253,14 +254,16 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2, tab3],
|
tabs: [tab1, tab2, tab3],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 1)
|
deleteTab(state, tab2)
|
||||||
expect(state.tabs).to.have.lengthOf(2)
|
expect(state.tabs).to.have.lengthOf(2)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[1].id).to.equal(3)
|
expect(state.tabs[1].id).to.equal(3)
|
||||||
expect(state.currentTabId).to.equal(3)
|
expect(state.currentTabId).to.equal(3)
|
||||||
|
expect(state.currentTab).to.eql(tab3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, single', () => {
|
it('deleteTab - opened, single', () => {
|
||||||
@@ -276,48 +279,19 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1],
|
tabs: [tab1],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, 0)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(0)
|
expect(state.tabs).to.have.lengthOf(0)
|
||||||
expect(state.currentTabId).to.equal(null)
|
expect(state.currentTabId).to.equal(null)
|
||||||
})
|
expect(state.currentTab).to.equal(null)
|
||||||
|
|
||||||
it('deleteTab - not opened', () => {
|
|
||||||
const tab1 = {
|
|
||||||
id: 1,
|
|
||||||
name: 'foo',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from foo',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const tab2 = {
|
|
||||||
id: 2,
|
|
||||||
name: 'bar',
|
|
||||||
tempName: null,
|
|
||||||
query: 'SELECT * from bar',
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: {},
|
|
||||||
isSaved: true
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
tabs: [tab1, tab2],
|
|
||||||
currentTabId: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
deleteTab(state, 1)
|
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
|
||||||
expect(state.currentTabId).to.equal(1)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setCurrentTabId', () => {
|
it('setCurrentTabId', () => {
|
||||||
const state = {
|
const state = {
|
||||||
|
tabs: [{ id: 1 }, { id: 2 }],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,15 +299,6 @@ describe('mutations', () => {
|
|||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setCurrentTab', () => {
|
|
||||||
const state = {
|
|
||||||
currentTab: { id: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrentTab(state, { id: 2 })
|
|
||||||
expect(state.currentTab).to.eql({ id: 2 })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('updatePredefinedInquiries - single', () => {
|
it('updatePredefinedInquiries - single', () => {
|
||||||
const inquiry = {
|
const inquiry = {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -377,4 +342,22 @@ describe('mutations', () => {
|
|||||||
updatePredefinedInquiries(state, inquiries)
|
updatePredefinedInquiries(state, inquiries)
|
||||||
expect(state.predefinedInquiries).to.eql(inquiries)
|
expect(state.predefinedInquiries).to.eql(inquiries)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('setLoadingPredefinedInquiries', () => {
|
||||||
|
const state = {
|
||||||
|
loadingPredefinedInquiries: false
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoadingPredefinedInquiries(state, true)
|
||||||
|
expect(state.loadingPredefinedInquiries).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('setPredefinedInquiriesLoaded', () => {
|
||||||
|
const state = {
|
||||||
|
predefinedInquiriesLoaded: false
|
||||||
|
}
|
||||||
|
|
||||||
|
setPredefinedInquiriesLoaded(state, true)
|
||||||
|
expect(state.predefinedInquiriesLoaded).to.equal(true)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
147
tests/views/LoadView.spec.js
Normal file
147
tests/views/LoadView.spec.js
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import sinon from 'sinon'
|
||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import Vuex from 'vuex'
|
||||||
|
import LoadView from '@/views/LoadView'
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import realMutations from '@/store/mutations'
|
||||||
|
import realActions from '@/store/actions'
|
||||||
|
import flushPromises from 'flush-promises'
|
||||||
|
import Tab from '@/lib/tab'
|
||||||
|
|
||||||
|
describe('LoadView.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
sinon.restore()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Loads db and inquiries and redirects to workspace if no errors', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const mutations = {
|
||||||
|
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
|
||||||
|
setDb: sinon.stub().callsFake(realMutations.setDb)
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
addTab: sinon.stub().callsFake(realActions.addTab)
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations, actions })
|
||||||
|
const $route = {
|
||||||
|
path: '/workspace',
|
||||||
|
query: {
|
||||||
|
data_url: 'https://my-url/test.db',
|
||||||
|
data_format: 'sqlite',
|
||||||
|
inquiry_url: 'https://my-url/test_inquiries.json',
|
||||||
|
inquiry_id: [1],
|
||||||
|
maximize: 'dataView'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $router = { push: sinon.stub() }
|
||||||
|
|
||||||
|
const readFile = sinon.stub(fu, 'readFile')
|
||||||
|
const dataRes = new Response()
|
||||||
|
dataRes.blob = sinon.stub().resolves({})
|
||||||
|
readFile.onCall(0).returns(Promise.resolve(dataRes))
|
||||||
|
|
||||||
|
const inquiriesRes = new Response()
|
||||||
|
inquiriesRes.json = sinon.stub().resolves({
|
||||||
|
version: 2,
|
||||||
|
inquiries: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
|
||||||
|
})
|
||||||
|
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
|
||||||
|
const db = {
|
||||||
|
loadDb: sinon.stub().resolves()
|
||||||
|
}
|
||||||
|
sinon.stub(database, 'getNewDatabase').returns(db)
|
||||||
|
Tab.prototype.execute = sinon.stub()
|
||||||
|
|
||||||
|
const wrapper = mount(LoadView, {
|
||||||
|
store,
|
||||||
|
mocks: { $route, $router },
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// DB file is read
|
||||||
|
expect(fu.readFile.firstCall.args[0]).to.equal('https://my-url/test.db')
|
||||||
|
|
||||||
|
// Db is loaded
|
||||||
|
expect(db.loadDb.firstCall.args[0]).to.equal(await dataRes.blob.returnValues[0])
|
||||||
|
|
||||||
|
// Inquiries file is read
|
||||||
|
expect(fu.readFile.secondCall.args[0])
|
||||||
|
.to.equal('https://my-url/test_inquiries.json')
|
||||||
|
|
||||||
|
// Tab for inquiry is created
|
||||||
|
expect(actions.addTab.calledOnce).to.equal(true)
|
||||||
|
expect(actions.addTab.firstCall.args[1]).eql({
|
||||||
|
id: undefined,
|
||||||
|
name: 'foo',
|
||||||
|
layout: {
|
||||||
|
dataView: 'bottom',
|
||||||
|
sqlEditor: 'hidden',
|
||||||
|
table: 'above'
|
||||||
|
},
|
||||||
|
maximize: 'dataView'
|
||||||
|
})
|
||||||
|
const executedTab = Tab.prototype.execute.firstCall.thisValue
|
||||||
|
expect(executedTab.tempName).to.equal('foo')
|
||||||
|
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(false)
|
||||||
|
expect($router.push.called).to.equal(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Doesn\'t redirect and show the button if there is an error', async () => {
|
||||||
|
const state = {
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const mutations = {
|
||||||
|
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
|
||||||
|
setDb: sinon.stub().callsFake(realMutations.setDb)
|
||||||
|
}
|
||||||
|
const actions = {
|
||||||
|
addTab: sinon.stub().callsFake(realActions.addTab)
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations, actions })
|
||||||
|
const $route = {
|
||||||
|
path: '/workspace',
|
||||||
|
query: {
|
||||||
|
data_url: 'https://my-url/test.db',
|
||||||
|
data_format: 'sqlite',
|
||||||
|
inquiry_url: 'https://my-url/test_inquiries.json',
|
||||||
|
inquiry_id: [1],
|
||||||
|
maximize: 'dataView'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const $router = { push: sinon.stub() }
|
||||||
|
|
||||||
|
const readFile = sinon.stub(fu, 'readFile')
|
||||||
|
const dataRes = new Response()
|
||||||
|
dataRes.blob = sinon.stub().rejects(new Error('Something is wrong'))
|
||||||
|
readFile.onCall(0).returns(Promise.resolve(dataRes))
|
||||||
|
|
||||||
|
const inquiriesRes = new Response()
|
||||||
|
inquiriesRes.json = sinon.stub().resolves({
|
||||||
|
version: 2,
|
||||||
|
inquiries: [{ id: 1 }]
|
||||||
|
})
|
||||||
|
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
|
||||||
|
sinon.stub(database, 'getNewDatabase').returns({
|
||||||
|
loadDb: sinon.stub().resolves()
|
||||||
|
})
|
||||||
|
|
||||||
|
const wrapper = mount(LoadView, {
|
||||||
|
store,
|
||||||
|
mocks: { $route, $router },
|
||||||
|
stubs: ['router-link']
|
||||||
|
})
|
||||||
|
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(true)
|
||||||
|
expect($router.push.called).to.equal(false)
|
||||||
|
expect(wrapper.find('#logs').text()).to.include('Something is wrong')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,7 +19,9 @@ describe('Inquiries.vue', () => {
|
|||||||
predefinedInquiries: []
|
predefinedInquiries: []
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
updatePredefinedInquiries: sinon.stub()
|
setPredefinedInquiriesLoaded: sinon.stub(),
|
||||||
|
updatePredefinedInquiries: sinon.stub(),
|
||||||
|
setLoadingPredefinedInquiries: sinon.stub()
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
const wrapper = shallowMount(Inquiries, { store })
|
const wrapper = shallowMount(Inquiries, { store })
|
||||||
@@ -327,6 +329,7 @@ describe('Inquiries.vue', () => {
|
|||||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
|
tabs: [],
|
||||||
predefinedInquiries: []
|
predefinedInquiries: []
|
||||||
}
|
}
|
||||||
const actions = { addTab: sinon.stub().resolves(1) }
|
const actions = { addTab: sinon.stub().resolves(1) }
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('MainMenu.vue', () => {
|
|||||||
it('Save is not visible if there is no tabs', () => {
|
it('Save is not visible if there is no tabs', () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: null,
|
currentTab: null,
|
||||||
tabs: [{}],
|
tabs: [],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -62,13 +62,15 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Save is disabled if current tab.isSaved is true', async () => {
|
it('Save is disabled if current tab.isSaved is true', async () => {
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -83,17 +85,19 @@ describe('MainMenu.vue', () => {
|
|||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||||
|
|
||||||
await vm.$set(state.tabs[0], 'isSaved', true)
|
await vm.$set(state.tabs[0], 'isSaved', true)
|
||||||
|
await vm.$nextTick()
|
||||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab', async () => {
|
it('Creates a tab', async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const newInquiryId = 1
|
const newInquiryId = 1
|
||||||
@@ -121,13 +125,14 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Creates a tab and redirects to workspace', async () => {
|
it('Creates a tab and redirects to workspace', async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const newInquiryId = 1
|
const newInquiryId = 1
|
||||||
@@ -156,13 +161,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
||||||
async () => {
|
async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -201,13 +207,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"',
|
||||||
async () => {
|
async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -245,13 +252,14 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Ctrl B calls createNewInquiry', async () => {
|
it('Ctrl B calls createNewInquiry', async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -280,13 +288,14 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace',
|
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace',
|
||||||
async () => {
|
async () => {
|
||||||
|
const tab = {
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state })
|
const store = new Vuex.Store({ state })
|
||||||
@@ -325,13 +334,16 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
it('Saves the inquiry when no need the new name',
|
it('Saves the inquiry when no need the new name',
|
||||||
async () => {
|
async () => {
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ id: 1, name: 'foo', isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -364,13 +376,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab,
|
||||||
name: 'foo',
|
newValues: {
|
||||||
id: 1,
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
id: 1,
|
||||||
viewType: 'chart',
|
query: 'SELECT * FROM foo',
|
||||||
viewOptions: [],
|
viewType: 'chart',
|
||||||
isSaved: true
|
viewOptions: [],
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -378,13 +392,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -424,13 +442,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Saves the inquiry with a new name', async () => {
|
it('Saves the inquiry with a new name', async () => {
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -475,13 +497,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab: tab,
|
||||||
name: 'foo',
|
newValues: {
|
||||||
id: 1,
|
name: 'foo',
|
||||||
query: 'SELECT * FROM foo',
|
id: 1,
|
||||||
viewType: 'chart',
|
query: 'SELECT * FROM foo',
|
||||||
viewOptions: [],
|
viewType: 'chart',
|
||||||
isSaved: true
|
viewOptions: [],
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -489,23 +513,26 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Saves a predefined inquiry with a new name', async () => {
|
it('Saves a predefined inquiry with a new name', async () => {
|
||||||
const state = {
|
const tab = {
|
||||||
currentTab: {
|
id: 1,
|
||||||
query: 'SELECT * FROM foo',
|
name: 'foo',
|
||||||
execute: sinon.stub(),
|
query: 'SELECT * FROM foo',
|
||||||
tabIndex: 0,
|
execute: sinon.stub(),
|
||||||
isPredefined: true,
|
isPredefined: true,
|
||||||
result: {
|
result: {
|
||||||
columns: ['id', 'name'],
|
columns: ['id', 'name'],
|
||||||
values: [
|
values: [
|
||||||
[1, 'Harry Potter'],
|
[1, 'Harry Potter'],
|
||||||
[2, 'Drako Malfoy']
|
[2, 'Drako Malfoy']
|
||||||
]
|
]
|
||||||
},
|
|
||||||
viewType: 'chart',
|
|
||||||
viewOptions: []
|
|
||||||
},
|
},
|
||||||
tabs: [{ id: 1, name: 'foo', isSaved: false, isPredefined: true }],
|
viewType: 'chart',
|
||||||
|
viewOptions: [],
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
|
const state = {
|
||||||
|
currentTab: tab,
|
||||||
|
tabs: [tab],
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
@@ -553,13 +580,15 @@ describe('MainMenu.vue', () => {
|
|||||||
|
|
||||||
// check that the tab was updated
|
// check that the tab was updated
|
||||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||||
index: 0,
|
tab,
|
||||||
name: 'bar',
|
newValues: {
|
||||||
id: 2,
|
name: 'bar',
|
||||||
query: 'SELECT * FROM foo',
|
id: 2,
|
||||||
viewType: 'chart',
|
query: 'SELECT * FROM foo',
|
||||||
viewOptions: [],
|
viewType: 'chart',
|
||||||
isSaved: true
|
viewOptions: [],
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
}))).to.equal(true)
|
}))).to.equal(true)
|
||||||
|
|
||||||
// check that 'inquirySaved' event was triggered on $root
|
// check that 'inquirySaved' event was triggered on $root
|
||||||
@@ -580,13 +609,17 @@ describe('MainMenu.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('Cancel saving', async () => {
|
it('Cancel saving', async () => {
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
execute: sinon.stub(),
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
const state = {
|
const state = {
|
||||||
currentTab: {
|
currentTab: tab,
|
||||||
query: 'SELECT * FROM foo',
|
tabs: [tab],
|
||||||
execute: sinon.stub(),
|
|
||||||
tabIndex: 0
|
|
||||||
},
|
|
||||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
|
||||||
db: {}
|
db: {}
|
||||||
}
|
}
|
||||||
const mutations = {
|
const mutations = {
|
||||||
|
|||||||
@@ -461,6 +461,7 @@ describe('Pivot.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('saveAsPng - table', async () => {
|
it('saveAsPng - table', async () => {
|
||||||
|
sinon.stub(pivotHelper, 'getPivotCanvas').returns(document.createElement('canvas'))
|
||||||
sinon.stub(HTMLCanvasElement.prototype, 'toDataURL').returns('canvas data url')
|
sinon.stub(HTMLCanvasElement.prototype, 'toDataURL').returns('canvas data url')
|
||||||
sinon.stub(fIo, 'downloadFromUrl')
|
sinon.stub(fIo, 'downloadFromUrl')
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect } from 'chai'
|
import { expect } from 'chai'
|
||||||
import { _getDataSources, getPivotCanvas, getPivotHtml }
|
import { _getDataSources, getPivotCanvas, getPivotHtml }
|
||||||
from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
|
from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
|
||||||
|
|
||||||
describe('pivotHelper.js', () => {
|
describe('pivotHelper.js', () => {
|
||||||
it('_getDataSources returns data sources', () => {
|
it('_getDataSources returns data sources', () => {
|
||||||
|
|||||||
@@ -138,15 +138,37 @@ describe('hint.js', () => {
|
|||||||
'getHints returns [ ] if there is only one option and token is completed with this option',
|
'getHints returns [ ] if there is only one option and token is completed with this option',
|
||||||
() => {
|
() => {
|
||||||
// mock CM.hint.sql and editor
|
// mock CM.hint.sql and editor
|
||||||
sinon.stub(CM.hint, 'sql').returns({ list: [{ text: 'SELECT' }] })
|
sinon.stub(CM.hint, 'sql').returns({
|
||||||
|
list: [{ text: 'SELECT' }],
|
||||||
|
from: null, // from/to doesn't metter because getRange is mocked
|
||||||
|
to: null
|
||||||
|
})
|
||||||
|
|
||||||
const editor = {
|
const editor = {
|
||||||
getTokenAt () {
|
getRange () {
|
||||||
return {
|
return 'select'
|
||||||
string: 'select',
|
}
|
||||||
type: 'keyword'
|
}
|
||||||
}
|
|
||||||
},
|
const hints = getHints(editor, {})
|
||||||
getCursor: sinon.stub()
|
expect(hints.list).to.eql([])
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
it(
|
||||||
|
'getHints returns [ ] if there is only one string option and token ' +
|
||||||
|
'is completed with this option',
|
||||||
|
() => {
|
||||||
|
// mock CM.hint.sql and editor
|
||||||
|
sinon.stub(CM.hint, 'sql').returns({
|
||||||
|
list: ['house.name'],
|
||||||
|
from: null, // from/to doesn't metter because getRange is mocked
|
||||||
|
to: null
|
||||||
|
})
|
||||||
|
const editor = {
|
||||||
|
getRange () {
|
||||||
|
return 'house.name'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hints = getHints(editor, {})
|
const hints = getHints(editor, {})
|
||||||
@@ -160,15 +182,11 @@ describe('hint.js', () => {
|
|||||||
{ text: 'SELECT' },
|
{ text: 'SELECT' },
|
||||||
{ text: 'ST' }
|
{ text: 'ST' }
|
||||||
]
|
]
|
||||||
sinon.stub(CM.hint, 'sql').returns({ list })
|
sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
|
||||||
const editor = {
|
const editor = {
|
||||||
getTokenAt () {
|
getRange () {
|
||||||
return {
|
return 'se'
|
||||||
string: 'se',
|
}
|
||||||
type: 'keyword'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCursor: sinon.stub()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hints = getHints(editor, {})
|
const hints = getHints(editor, {})
|
||||||
@@ -182,15 +200,11 @@ describe('hint.js', () => {
|
|||||||
() => {
|
() => {
|
||||||
// mock CM.hint.sql and editor
|
// mock CM.hint.sql and editor
|
||||||
const list = [{ text: 'SELECT' }]
|
const list = [{ text: 'SELECT' }]
|
||||||
sinon.stub(CM.hint, 'sql').returns({ list })
|
sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
|
||||||
const editor = {
|
const editor = {
|
||||||
getTokenAt () {
|
getRange () {
|
||||||
return {
|
return 'sele'
|
||||||
string: 'sele',
|
}
|
||||||
type: 'keyword'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getCursor: sinon.stub()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const hints = getHints(editor, {})
|
const hints = getHints(editor, {})
|
||||||
|
|||||||
@@ -31,13 +31,23 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab: {
|
||||||
initName: 'foo',
|
id: 1,
|
||||||
initQuery: 'SELECT * FROM foo',
|
name: 'foo',
|
||||||
initViewType: 'chart',
|
query: 'SELECT * FROM foo',
|
||||||
initViewOptions: [],
|
viewType: 'chart',
|
||||||
tabIndex: 0,
|
viewOptions: {},
|
||||||
isPredefined: false
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -60,7 +70,23 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1
|
tab: {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
||||||
@@ -79,40 +105,51 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1
|
tab: {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
expect(wrapper.find('.tab-content-container').isVisible()).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Calls setCurrentTab when becomes active', async () => {
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
currentTabId: 0
|
|
||||||
}
|
|
||||||
sinon.spy(mutations, 'setCurrentTab')
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tab, {
|
|
||||||
store,
|
|
||||||
stubs: ['chart'],
|
|
||||||
propsData: {
|
|
||||||
id: 1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
state.currentTabId = 1
|
|
||||||
await wrapper.vm.$nextTick()
|
|
||||||
expect(mutations.setCurrentTab.calledOnceWith(state, wrapper.vm)).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Update tab state when a query is changed', async () => {
|
it('Update tab state when a query is changed', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true }
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
@@ -124,13 +161,7 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab: state.tabs[0]
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100')
|
await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100')
|
||||||
@@ -141,7 +172,24 @@ describe('Tab.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'SELECT * FROM foo', chart: [], isSaved: true }
|
{
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 1
|
currentTabId: 1
|
||||||
}
|
}
|
||||||
@@ -153,13 +201,7 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab: state.tabs[0]
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
await wrapper.findComponent({ name: 'DataView' }).vm.$emit('update')
|
await wrapper.findComponent({ name: 'DataView' }).vm.$emit('update')
|
||||||
@@ -169,29 +211,38 @@ describe('Tab.vue', () => {
|
|||||||
it('Shows .result-in-progress message when executing query', async () => {
|
it('Shows .result-in-progress message when executing query', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
|
||||||
execute () { return new Promise(() => {}) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
// mount the component
|
// mount the component
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.execute()
|
tab.isGettingResults = true
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').isVisible()).to.equal(true)
|
expect(wrapper.find('.run-result-panel .result-in-progress').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
@@ -199,30 +250,42 @@ describe('Tab.vue', () => {
|
|||||||
it('Shows error when executing query ends with error', async () => {
|
it('Shows error when executing query ends with error', async () => {
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
|
||||||
execute: sinon.stub().rejects(new Error('There is no table foo')),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
// mount the component
|
// mount the component
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
tab.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: 'There is no table foo'
|
||||||
|
}
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
||||||
@@ -239,11 +302,26 @@ describe('Tab.vue', () => {
|
|||||||
}
|
}
|
||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {
|
}
|
||||||
execute: sinon.stub().resolves(result),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
const tab = {
|
||||||
}
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
@@ -253,83 +331,50 @@ describe('Tab.vue', () => {
|
|||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
tab.result = result
|
||||||
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-before').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
expect(wrapper.find('.run-result-panel .result-in-progress').exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
||||||
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
expect(wrapper.findComponent({ name: 'SqlTable' }).vm.dataSet).to.eql(result)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('Updates schema after query execution', async () => {
|
|
||||||
const result = {
|
|
||||||
columns: ['id', 'name'],
|
|
||||||
values: {
|
|
||||||
id: [],
|
|
||||||
name: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// mock store state
|
|
||||||
const state = {
|
|
||||||
currentTabId: 1,
|
|
||||||
dbName: 'fooDb',
|
|
||||||
db: {
|
|
||||||
execute: sinon.stub().resolves(result),
|
|
||||||
refreshSchema: sinon.stub().resolves()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
|
||||||
|
|
||||||
// mount the component
|
|
||||||
const wrapper = mount(Tab, {
|
|
||||||
store,
|
|
||||||
stubs: ['chart'],
|
|
||||||
propsData: {
|
|
||||||
id: 1,
|
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await wrapper.vm.execute()
|
|
||||||
expect(state.db.refreshSchema.calledOnce).to.equal(true)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('Switches views', async () => {
|
it('Switches views', async () => {
|
||||||
const state = {
|
const state = {
|
||||||
currentTabId: 1,
|
currentTabId: 1
|
||||||
db: {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = new Vuex.Store({ state, mutations })
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
const wrapper = mount(Tab, {
|
const wrapper = mount(Tab, {
|
||||||
attachTo: place,
|
attachTo: place,
|
||||||
store,
|
store,
|
||||||
stubs: ['chart'],
|
stubs: ['chart'],
|
||||||
propsData: {
|
propsData: {
|
||||||
id: 1,
|
tab
|
||||||
initName: 'foo',
|
|
||||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
|
||||||
initViewOptions: [],
|
|
||||||
initViewType: 'chart',
|
|
||||||
tabIndex: 0,
|
|
||||||
isPredefined: false
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -361,4 +406,119 @@ describe('Tab.vue', () => {
|
|||||||
expect(wrapper.find('.above .sql-editor-panel').exists()).to.equal(true)
|
expect(wrapper.find('.above .sql-editor-panel').exists()).to.equal(true)
|
||||||
expect(wrapper.find('.bottomPane .run-result-panel').exists()).to.equal(true)
|
expect(wrapper.find('.bottomPane .run-result-panel').exists()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Maximize top panel if maximized panel is above', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
maximize: 'sqlEditor',
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.above').element.parentElement.style.height)
|
||||||
|
.to.equal('100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Maximize bottom panel if maximized panel is below', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
maximize: 'table',
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.bottomPane').element.parentElement.style.height)
|
||||||
|
.to.equal('100%')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('Panel size is 50 is nothing to maximize', () => {
|
||||||
|
const state = {
|
||||||
|
currentTabId: 1
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, mutations })
|
||||||
|
const tab = {
|
||||||
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isPredefined: false,
|
||||||
|
result: null,
|
||||||
|
isGettingResults: false,
|
||||||
|
error: null,
|
||||||
|
time: 0,
|
||||||
|
isSaved: true
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapper = mount(Tab, {
|
||||||
|
attachTo: place,
|
||||||
|
store,
|
||||||
|
stubs: ['chart'],
|
||||||
|
propsData: {
|
||||||
|
tab
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.find('.above').element.parentElement.style.height)
|
||||||
|
.to.equal('50%')
|
||||||
|
expect(wrapper.find('.bottomPane').element.parentElement.style.height)
|
||||||
|
.to.equal('50%')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -94,8 +94,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -125,8 +150,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -166,8 +216,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
@@ -211,8 +286,33 @@ describe('Tabs.vue', () => {
|
|||||||
// mock store state
|
// mock store state
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [
|
tabs: [
|
||||||
{ id: 1, name: 'foo', query: 'select * from foo', chart: [], isSaved: true },
|
{
|
||||||
{ id: 2, name: null, tempName: 'Untitled', query: '', chart: [], isSaved: false }
|
id: 1,
|
||||||
|
name: 'foo',
|
||||||
|
query: 'select * from foo',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
name: null,
|
||||||
|
tempName: 'Untitled',
|
||||||
|
query: '',
|
||||||
|
viewType: 'chart',
|
||||||
|
viewOptions: {},
|
||||||
|
layout: {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
},
|
||||||
|
isSaved: false
|
||||||
|
}
|
||||||
],
|
],
|
||||||
currentTabId: 2
|
currentTabId: 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ describe('Workspace.vue', () => {
|
|||||||
tabs: []
|
tabs: []
|
||||||
}
|
}
|
||||||
const store = new Vuex.Store({ state, actions, mutations })
|
const store = new Vuex.Store({ state, actions, mutations })
|
||||||
|
const $route = { path: '/workspace', query: {} }
|
||||||
mount(Workspace, {
|
mount(Workspace, {
|
||||||
store,
|
store,
|
||||||
stubs: ['router-link']
|
stubs: ['router-link'],
|
||||||
|
mocks: { $route }
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(state.tabs[0].query).to.include('Your database is empty.')
|
expect(state.tabs[0].query).to.include('Your database is empty.')
|
||||||
@@ -24,4 +26,20 @@ describe('Workspace.vue', () => {
|
|||||||
expect(state.tabs[0].viewOptions).to.equal(undefined)
|
expect(state.tabs[0].viewOptions).to.equal(undefined)
|
||||||
expect(state.tabs[0].isSaved).to.equal(false)
|
expect(state.tabs[0].isSaved).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('Collapse schema if hide_schema is 1', () => {
|
||||||
|
const state = {
|
||||||
|
db: {},
|
||||||
|
tabs: []
|
||||||
|
}
|
||||||
|
const store = new Vuex.Store({ state, actions, mutations })
|
||||||
|
const $route = { path: '/workspace', query: { hide_schema: '1' } }
|
||||||
|
const vm = mount(Workspace, {
|
||||||
|
store,
|
||||||
|
stubs: ['router-link'],
|
||||||
|
mocks: { $route }
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(vm.find('#schema-container').element.offsetWidth).to.equal(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -41,5 +41,12 @@ module.exports = {
|
|||||||
.end()
|
.end()
|
||||||
|
|
||||||
config.module.rule('js').exclude.add(/worker\.js$/)
|
config.module.rule('js').exclude.add(/worker\.js$/)
|
||||||
|
|
||||||
|
config
|
||||||
|
.plugin('html')
|
||||||
|
.tap(args => {
|
||||||
|
args[0].minify = false
|
||||||
|
return args
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user