mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
52 Commits
0.21.1
...
3a05b27400
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a05b27400 | ||
|
|
108d96a753 | ||
|
|
f55a8caa92 | ||
|
|
87f9f9eb01 | ||
|
|
d6408bdd85 | ||
|
|
378b9fb580 | ||
|
|
244ba9eb08 | ||
|
|
53e5194295 | ||
|
|
04274ef19a | ||
|
|
3893a66f4e | ||
|
|
1b6b7c71e9 | ||
|
|
3f6427ff0e | ||
|
|
a2464d839f | ||
|
|
316e603c3c | ||
|
|
88466eca5e | ||
|
|
5123e39a60 | ||
|
|
4c8401f32f | ||
|
|
d949629ee4 | ||
|
|
7a18e415c8 | ||
|
|
878689b3f7 | ||
|
|
42f040975d | ||
|
|
78e9ca2120 | ||
|
|
96af391f20 | ||
|
|
f58b62eb0c | ||
|
|
b17040d3ef | ||
|
|
bc6154b9ad | ||
|
|
3aea8c951b | ||
|
|
1e982a1196 | ||
|
|
6ecbde7fd3 | ||
|
|
5ee881432a | ||
|
|
735e4ec7f6 | ||
|
|
07d31dbfe9 | ||
|
|
ac1f7de62c | ||
|
|
96877de532 | ||
|
|
b60fc28e47 | ||
|
|
bec3d9c737 | ||
|
|
8aac7af481 | ||
|
|
6982204e68 | ||
|
|
41e0ae7332 | ||
|
|
ebb5af4f10 | ||
|
|
ae26358b25 | ||
|
|
d9ee702b8e | ||
|
|
446045fa55 | ||
|
|
1a9d1b308b | ||
|
|
014ecf145e | ||
|
|
0044d82b6f | ||
|
|
998e8d66f7 | ||
|
|
db3dbdf993 | ||
|
|
4e13a16e33 | ||
|
|
9c0103fd05 | ||
|
|
e4b117ffb9 | ||
|
|
6320f818cb |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -14,10 +14,10 @@ jobs:
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 16.x
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
run: npm install -g npm@8
|
||||
|
||||
- name: npm install and build
|
||||
run: |
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -11,13 +11,13 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
node-version: 16.x
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
run: npm install -g npm@8
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
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.22-buster
|
||||
|
||||
RUN set -ex; \
|
||||
apt update; \
|
||||
apt install -y chromium firefox-esr; \
|
||||
npm install -g npm@7
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
COPY lib lib
|
||||
RUN npm install
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN set -ex; \
|
||||
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
|
||||
|
||||
RUN npm run lint -- --no-fix && npm run test
|
||||
@@ -4,11 +4,12 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
|
||||
of SQLite databases, CSV, JSON or NDJSON files.
|
||||
|
||||
With sqliteviz you can:
|
||||
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
||||
- import a CSV file into a SQLite database and visualize imported data
|
||||
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
|
||||
- export result set to CSV file
|
||||
- manage inquiries and run them against different databases
|
||||
- import/export inquiries from/to a JSON file
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM emscripten/emsdk:2.0.24
|
||||
FROM emscripten/emsdk:3.0.1
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
|
||||
@@ -43,6 +43,10 @@ SQLite [miscellaneous extensions][3] included:
|
||||
SQLite 3rd party extensions included:
|
||||
|
||||
1. [pivot_vtab][5] -- a pivot virtual table
|
||||
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||
(which is part of [squib][20])
|
||||
3. [sqlitelua][22] -- a virtual table `luafunctions` which allows to define custom scalar,
|
||||
aggregate and table-valued functions in Lua
|
||||
|
||||
To ease the step to have working clone locally, the build is committed into
|
||||
the repository.
|
||||
@@ -99,3 +103,6 @@ described in [this message from SQLite Forum][12]:
|
||||
[17]: https://sqlite.org/contrib/
|
||||
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
|
||||
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
|
||||
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
||||
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c
|
||||
[22]: https://github.com/kev82/sqlitelua
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
FROM node:14-bullseye
|
||||
FROM node:20.14-bookworm
|
||||
|
||||
RUN set -ex; \
|
||||
echo 'deb http://deb.debian.org/debian unstable main' \
|
||||
> /etc/apt/sources.list.d/unstable.list; \
|
||||
apt-get update; \
|
||||
apt-get install -y -t unstable firefox; \
|
||||
apt-get install -y firefox-esr; \
|
||||
apt-get install -y chromium
|
||||
|
||||
WORKDIR /tmp/build
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
# SQLite WebAssembly build micro-benchmark
|
||||
|
||||
This directory contains a micro-benchmark for evaluating SQLite
|
||||
WebAssembly builds performance on typical SQL queries, run from
|
||||
`make.sh` script. It can also serve as a smoke test.
|
||||
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
|
||||
builds performance on read and write SQL queries, run from `make.sh` script. If
|
||||
the script has permission to `nice` processes and [Procpath][1] is installed,
|
||||
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
|
||||
processes running inside the benchmark containers. It can also serve as a smoke
|
||||
test (e.g. for memory leaks).
|
||||
|
||||
The benchmark operates on a set of SQLite WebAssembly builds expected
|
||||
in `lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs
|
||||
the benchmark in Firefox and Chromium using Karma in the container.
|
||||
The benchmark operates on a set of SQLite WebAssembly builds expected in
|
||||
`lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
|
||||
benchmark in Firefox and Chromium using Karma in the container.
|
||||
|
||||
After successful run, the benchmark result of each build is contained
|
||||
in `build-$NAME-result.json`. The JSON result files can be analysed
|
||||
using `result-analysis.ipynb` Jupyter notebook.
|
||||
After successful run, the benchmark produces the following per each build:
|
||||
|
||||
- `build-$NAME-result.json`
|
||||
- `build-$NAME.sqlite` (if Procpath is installed)
|
||||
- `build-$NAME.svg` (if Procpath is installed)
|
||||
|
||||
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
|
||||
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
|
||||
run, then Firefox run per container).
|
||||
|
||||
[1]: https://pypi.org/project/Procpath/
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
cleanup () {
|
||||
rm -rf lib/dist $flag_file
|
||||
rm -rf lib/dist "$renice_flag_file"
|
||||
docker rm -f sqljs-benchmark-run 2> /dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
@@ -11,34 +12,36 @@ if [ ! -f sample.csv ]; then
|
||||
| gunzip -c > sample.csv
|
||||
fi
|
||||
|
||||
PLAYBOOK=procpath/karma_docker.procpath
|
||||
|
||||
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
|
||||
test_ni=$(nice -n -1 nice)
|
||||
if [ $test_ni == -1 ]; then
|
||||
flag_file=$(mktemp)
|
||||
test_ni=$(nice -n -5 nice)
|
||||
if [ $test_ni == -5 ]; then
|
||||
renice_flag_file=$(mktemp)
|
||||
fi
|
||||
(
|
||||
while [ -f $flag_file ]; do
|
||||
root_pid=$(
|
||||
docker ps -f status=running -f name='^sqljs-benchmark-' -q \
|
||||
| xargs -r -I{} -- docker inspect -f '{{.State.Pid}}' {}
|
||||
)
|
||||
if [ ! -z $root_pid ]; then
|
||||
procpath query -d $'\n' "$..children[?(@.stat.pid == $root_pid)]..pid" \
|
||||
| xargs -I{} -- renice -n -1 -p {} > /dev/null
|
||||
fi
|
||||
sleep 1
|
||||
done &
|
||||
)
|
||||
{
|
||||
while [ -f $renice_flag_file ]; do
|
||||
procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
|
||||
done
|
||||
} &
|
||||
|
||||
shopt -s nullglob
|
||||
for d in lib/build-* ; do
|
||||
rm -rf lib/dist
|
||||
cp -r $d lib/dist
|
||||
sample_name=$(basename $d)
|
||||
|
||||
name=$(basename $d)
|
||||
docker build -t sqliteviz/sqljs-benchmark:$name .
|
||||
docker rm sqljs-benchmark-$name 2> /dev/null || true
|
||||
docker run -it --cpus 2 --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
|
||||
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
|
||||
docker rm sqljs-benchmark-$name
|
||||
docker build -t sqliteviz/sqljs-benchmark .
|
||||
docker rm sqljs-benchmark-run 2> /dev/null || true
|
||||
docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
|
||||
{
|
||||
rm -f ${sample_name}.sqlite
|
||||
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
|
||||
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
|
||||
-o plot_file=${sample_name}.svg track:plot
|
||||
} &
|
||||
|
||||
docker attach sqljs-benchmark-run
|
||||
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
|
||||
docker rm sqljs-benchmark-run
|
||||
done
|
||||
|
||||
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
|
||||
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 = (
|
||||
'-O2',
|
||||
# SQLite configuration
|
||||
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
||||
@@ -13,26 +15,27 @@ cflags = (
|
||||
'-DSQLITE_ENABLE_FTS3',
|
||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||
'-DSQLITE_ENABLE_FTS5',
|
||||
'-DSQLITE_ENABLE_JSON1',
|
||||
'-DSQLITE_ENABLE_NORMALIZE',
|
||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||
'-DSQLITE_OMIT_DEPRECATED',
|
||||
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||
'-DSQLITE_OMIT_SHARED_CACHE',
|
||||
'-DSQLITE_THREADSAFE=0',
|
||||
# Compile-time optimisation
|
||||
'-Os', # reduces the code size about in half comparing to -O2
|
||||
'-flto',
|
||||
'-Isrc', '-Isrc/lua',
|
||||
)
|
||||
emflags = (
|
||||
# Base
|
||||
'--memory-init-file', '0',
|
||||
'-s', 'RESERVED_FUNCTION_POINTERS=64',
|
||||
'-s', 'ALLOW_TABLE_GROWTH=1',
|
||||
'-s', 'SINGLE_FILE=0',
|
||||
# WASM
|
||||
'-s', 'WASM=1',
|
||||
'-s', 'ALLOW_MEMORY_GROWTH=1',
|
||||
# Optimisation
|
||||
'-s', 'INLINING_LIMIT=50',
|
||||
'-O3',
|
||||
'-s', 'ENVIRONMENT=web,worker',
|
||||
# Link-time optimisation
|
||||
'-Os',
|
||||
'-flto',
|
||||
# sql.js
|
||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||
@@ -50,22 +53,32 @@ def build(src: Path, dst: Path):
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'sqlite3.c',
|
||||
'-o', out / 'sqlite3.bc',
|
||||
'-o', out / 'sqlite3.o',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for extension-functions.c')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-c', src / 'extension-functions.c',
|
||||
'-o', out / 'extension-functions.bc',
|
||||
'-o', out / 'extension-functions.o',
|
||||
])
|
||||
logging.info('Building LLVM bitcode for SQLite Lua extension')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*cflags,
|
||||
'-shared',
|
||||
*(src / 'lua').glob('*.c'),
|
||||
*(src / 'sqlitelua').glob('*.c'),
|
||||
'-o', out / 'sqlitelua.o',
|
||||
])
|
||||
|
||||
logging.info('Building WASM from bitcode')
|
||||
subprocess.check_call([
|
||||
'emcc',
|
||||
*emflags,
|
||||
out / 'sqlite3.bc',
|
||||
out / 'extension-functions.bc',
|
||||
out / 'sqlite3.o',
|
||||
out / 'extension-functions.o',
|
||||
out / 'sqlitelua.o',
|
||||
'-o', out / 'sql-wasm.js',
|
||||
])
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import zipfile
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from urllib import request
|
||||
|
||||
|
||||
amalgamation_url = 'https://sqlite.org/2022/sqlite-amalgamation-3390000.zip'
|
||||
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
|
||||
|
||||
# Extension-functions
|
||||
# ===================
|
||||
@@ -28,10 +30,17 @@ extension_urls = (
|
||||
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
|
||||
# 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'),
|
||||
# Third-party extension with own dependencies
|
||||
# ===========================================
|
||||
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
|
||||
)
|
||||
|
||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip'
|
||||
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
|
||||
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
|
||||
|
||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||
|
||||
|
||||
def _generate_extra_init_c_function(init_function_names):
|
||||
@@ -58,6 +67,38 @@ def _get_amalgamation(tgt: Path):
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_lua(tgt: Path):
|
||||
# Library definitions from lua/Makefile
|
||||
lib_str = '''
|
||||
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||
ltm.o lundump.o lvm.o lzio.o
|
||||
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
|
||||
LUA_O= lua.o
|
||||
'''
|
||||
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
|
||||
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
|
||||
|
||||
logging.info('Downloading and extracting Lua %s', lua_url)
|
||||
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
|
||||
(tgt / 'lua').mkdir()
|
||||
for tarinfo in archive:
|
||||
tarpath = Path(tarinfo.name)
|
||||
if tarpath.match('src/*') and tarpath.stem in lib_names:
|
||||
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
|
||||
|
||||
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
|
||||
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
|
||||
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||
(tgt / 'sqlitelua').mkdir()
|
||||
for zpath in (archive_root_dir / 'src').iterdir():
|
||||
if zpath.name != 'main.c':
|
||||
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
|
||||
shutil.copyfileobj(fr, fw)
|
||||
|
||||
|
||||
def _get_contrib_functions(tgt: Path):
|
||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||
|
||||
@@ -69,6 +110,7 @@ def _get_extensions(tgt: Path):
|
||||
for url, init_fn in extension_urls:
|
||||
logging.info('Downloading and appending to amalgamation %s', url)
|
||||
with request.urlopen(url) as resp:
|
||||
f.write(b'\n')
|
||||
shutil.copyfileobj(resp, f)
|
||||
init_functions.append(init_fn)
|
||||
|
||||
@@ -89,6 +131,7 @@ def _get_sqljs(tgt: Path):
|
||||
def configure(tgt: Path):
|
||||
_get_amalgamation(tgt)
|
||||
_get_contrib_functions(tgt)
|
||||
_get_lua(tgt)
|
||||
_get_extensions(tgt)
|
||||
_get_sqljs(tgt)
|
||||
|
||||
|
||||
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.
7078
package-lock.json
generated
7078
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.21.1",
|
||||
"version": "0.25.1",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -10,19 +10,19 @@
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"codemirror": "^5.65.18",
|
||||
"core-js": "^3.6.5",
|
||||
"dataurl-to-blob": "^0.0.1",
|
||||
"html2canvas": "^1.1.4",
|
||||
"jquery": "^3.6.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"papaparse": "^5.3.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"pivottable": "^2.23.0",
|
||||
"plotly.js": "^1.58.4",
|
||||
"plotly.js": "^2.35.2",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.45.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"react": "^16.14.0",
|
||||
"react-chart-editor": "^0.46.1",
|
||||
"react-dom": "^16.14.0",
|
||||
"sql.js": "file:./lib/sql-js",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
@@ -51,6 +51,7 @@
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"flush-promises": "^1.0.2",
|
||||
"karma": "^3.1.4",
|
||||
"karma-firefox-launcher": "^2.1.0",
|
||||
"karma-webpack": "^4.0.2",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
|
||||
23
src/App.vue
23
src/App.vue
@@ -4,6 +4,29 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
|
||||
export default {
|
||||
created () {
|
||||
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||
},
|
||||
computed: {
|
||||
inquiries () {
|
||||
return this.$store.state.inquiries
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inquiries: {
|
||||
deep: true,
|
||||
handler () {
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@font-face {
|
||||
font-family: "Open Sans";
|
||||
|
||||
@@ -107,3 +107,9 @@ table.sqliteviz-table {
|
||||
font-size: 11px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-table tbody td[data-isNull="true"],
|
||||
.sqliteviz-table tbody td[data-isBlob="true"] {
|
||||
color: var(--color-text-light-2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
:clickToClose="false"
|
||||
>
|
||||
<div class="dialog-header">
|
||||
CSV import
|
||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
||||
{{ typeName }} import
|
||||
<close-icon @click="cancelImport" :disabled="disableDialog"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<text-field
|
||||
@@ -18,15 +18,15 @@
|
||||
width="484px"
|
||||
:disabled="disableDialog"
|
||||
:error-msg="tableNameError"
|
||||
id="csv-table-name"
|
||||
id="csv-json-table-name"
|
||||
/>
|
||||
<div class="chars">
|
||||
<div v-if="!isJson && !isNdJson" class="chars">
|
||||
<delimiter-selector
|
||||
v-model="delimiter"
|
||||
width="210px"
|
||||
class="char-input"
|
||||
@input="previewCsv"
|
||||
:disabled="disableDialog"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
label="Quote char"
|
||||
@@ -36,6 +36,7 @@
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="quote-char"
|
||||
@input="preview"
|
||||
/>
|
||||
<text-field
|
||||
label="Escape char"
|
||||
@@ -49,52 +50,52 @@
|
||||
:disabled="disableDialog"
|
||||
class="char-input"
|
||||
id="escape-char"
|
||||
@input="preview"
|
||||
/>
|
||||
</div>
|
||||
<check-box
|
||||
@click="header = $event"
|
||||
:init="true"
|
||||
v-if="!isJson && !isNdJson"
|
||||
:init="header"
|
||||
label="Use first row as column headers"
|
||||
:disabled="disableDialog"
|
||||
@click="changeHeaderDisplaying"
|
||||
/>
|
||||
<sql-table
|
||||
v-if="previewData
|
||||
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
|
||||
"
|
||||
v-if="previewData && previewData.rowCount > 0"
|
||||
:data-set="previewData"
|
||||
class="preview-table"
|
||||
:preview="true"
|
||||
class="preview-table"
|
||||
/>
|
||||
<div v-else class="no-data">No data</div>
|
||||
<logs
|
||||
class="import-csv-errors"
|
||||
:messages="importCsvMessages"
|
||||
class="import-errors"
|
||||
:messages="importMessages"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button
|
||||
class="secondary"
|
||||
:disabled="disableDialog"
|
||||
@click="cancelCsvImport"
|
||||
id="csv-cancel"
|
||||
@click="cancelImport"
|
||||
id="import-cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
v-show="!importCsvCompleted"
|
||||
v-show="!importCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="loadFromCsv(file)"
|
||||
id="csv-import"
|
||||
:disabled="disableDialog || disableImport"
|
||||
@click="loadToDb(file)"
|
||||
id="import-start"
|
||||
>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
v-show="importCsvCompleted"
|
||||
v-show="importCompleted"
|
||||
class="primary"
|
||||
:disabled="disableDialog"
|
||||
@click="finish"
|
||||
id="csv-finish"
|
||||
id="import-finish"
|
||||
>
|
||||
Finish
|
||||
</button>
|
||||
@@ -115,7 +116,7 @@ import fIo from '@/lib/utils/fileIo'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
name: 'CsvImport',
|
||||
name: 'CsvJsonImport',
|
||||
components: {
|
||||
CloseIcon,
|
||||
TextField,
|
||||
@@ -124,33 +125,50 @@ export default {
|
||||
SqlTable,
|
||||
Logs
|
||||
},
|
||||
props: ['file', 'db', 'dialogName'],
|
||||
props: {
|
||||
file: File,
|
||||
db: Object,
|
||||
dialogName: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
disableDialog: false,
|
||||
disableImport: false,
|
||||
tableName: '',
|
||||
delimiter: '',
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: true,
|
||||
importCsvCompleted: false,
|
||||
importCsvMessages: [],
|
||||
importCompleted: false,
|
||||
importMessages: [],
|
||||
previewData: null,
|
||||
addedTable: null,
|
||||
tableNameError: ''
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isJson () {
|
||||
return fIo.isJSON(this.file)
|
||||
},
|
||||
isNdJson () {
|
||||
return fIo.isNDJSON(this.file)
|
||||
},
|
||||
typeName () {
|
||||
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
quoteChar () {
|
||||
this.previewCsv()
|
||||
isJson () {
|
||||
if (this.isJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
|
||||
escapeChar () {
|
||||
this.previewCsv()
|
||||
},
|
||||
|
||||
header () {
|
||||
this.previewCsv()
|
||||
isNdJson () {
|
||||
if (this.isNdJson) {
|
||||
this.delimiter = '\u001E'
|
||||
this.header = false
|
||||
}
|
||||
},
|
||||
tableName: time.debounce(function () {
|
||||
this.tableNameError = ''
|
||||
@@ -164,7 +182,11 @@ export default {
|
||||
}, 400)
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
changeHeaderDisplaying (e) {
|
||||
this.header = e
|
||||
this.preview()
|
||||
},
|
||||
cancelImport () {
|
||||
if (!this.disableDialog) {
|
||||
if (this.addedTable) {
|
||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||
@@ -175,14 +197,15 @@ export default {
|
||||
}
|
||||
},
|
||||
reset () {
|
||||
this.header = true
|
||||
this.header = !this.isJson && !this.isNdJson
|
||||
this.quoteChar = '"'
|
||||
this.escapeChar = '"'
|
||||
this.delimiter = ''
|
||||
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
|
||||
this.tableName = ''
|
||||
this.disableDialog = false
|
||||
this.importCsvCompleted = false
|
||||
this.importCsvMessages = []
|
||||
this.disableImport = false
|
||||
this.importCompleted = false
|
||||
this.importMessages = []
|
||||
this.previewData = null
|
||||
this.addedTable = null
|
||||
this.tableNameError = ''
|
||||
@@ -191,39 +214,69 @@ export default {
|
||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||
this.$modal.show(this.dialogName)
|
||||
},
|
||||
async previewCsv () {
|
||||
this.importCsvCompleted = false
|
||||
async preview () {
|
||||
this.disableImport = false
|
||||
if (!this.file) {
|
||||
return
|
||||
}
|
||||
this.importCompleted = false
|
||||
const config = {
|
||||
preview: 3,
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
try {
|
||||
const start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(this.file)
|
||||
: await csv.parse(this.file, config)
|
||||
const end = new Date()
|
||||
this.previewData = parseResult.data
|
||||
this.previewData.rowCount = parseResult.rowCount
|
||||
this.delimiter = parseResult.delimiter
|
||||
|
||||
// In parseResult.messages we can get parse errors
|
||||
this.importCsvMessages = parseResult.messages || []
|
||||
this.importMessages = parseResult.messages || []
|
||||
|
||||
if (this.previewData.rowCount === 0) {
|
||||
this.disableImport = true
|
||||
this.importMessages.push({
|
||||
type: 'info',
|
||||
message: 'No rows to import.'
|
||||
})
|
||||
}
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
this.importCsvMessages.push({
|
||||
this.importMessages.push({
|
||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
this.importCsvMessages = [{
|
||||
console.error(err)
|
||||
this.importMessages = [{
|
||||
message: err,
|
||||
type: 'error'
|
||||
}]
|
||||
}
|
||||
},
|
||||
async loadFromCsv (file) {
|
||||
async getJsonParseResult (file) {
|
||||
const jsonContent = await fIo.getFileContent(file)
|
||||
const isEmpty = !jsonContent.trim()
|
||||
return {
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: { doc: !isEmpty ? [jsonContent] : [] }
|
||||
},
|
||||
hasErrors: false,
|
||||
messages: [],
|
||||
rowCount: +(!isEmpty)
|
||||
}
|
||||
},
|
||||
async loadToDb (file) {
|
||||
if (!this.tableName) {
|
||||
this.tableNameError = "Table name can't be empty"
|
||||
return
|
||||
@@ -234,17 +287,18 @@ export default {
|
||||
quoteChar: this.quoteChar || '"',
|
||||
escapeChar: this.escapeChar,
|
||||
header: this.header,
|
||||
delimiter: this.delimiter
|
||||
delimiter: this.delimiter,
|
||||
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||
}
|
||||
const parseCsvMsg = {
|
||||
message: 'Parsing CSV...',
|
||||
const parsingMsg = {
|
||||
message: `Parsing ${this.typeName}...`,
|
||||
type: 'info'
|
||||
}
|
||||
this.importCsvMessages.push(parseCsvMsg)
|
||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
||||
this.importMessages.push(parsingMsg)
|
||||
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
|
||||
|
||||
const importMsg = {
|
||||
message: 'Importing CSV into a SQLite database...',
|
||||
message: `Importing ${this.typeName} into a SQLite database...`,
|
||||
type: 'info'
|
||||
}
|
||||
let importLoadingIndicator = null
|
||||
@@ -256,27 +310,30 @@ export default {
|
||||
|
||||
try {
|
||||
let start = new Date()
|
||||
const parseResult = await csv.parse(this.file, config)
|
||||
const parseResult = this.isJson
|
||||
? await this.getJsonParseResult(file)
|
||||
: await csv.parse(this.file, config)
|
||||
|
||||
let end = new Date()
|
||||
|
||||
if (!parseResult.hasErrors) {
|
||||
const rowCount = parseResult.rowCount
|
||||
let period = time.getPeriod(start, end)
|
||||
parseCsvMsg.type = 'success'
|
||||
parsingMsg.type = 'success'
|
||||
|
||||
if (parseResult.messages.length > 0) {
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||
} else {
|
||||
// Inform about csv parsing success
|
||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
// Inform about parsing success
|
||||
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||
}
|
||||
|
||||
// Loading indicator for csv parsing is not needed anymore
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
// Loading indicator for parsing is not needed anymore
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
|
||||
// Add info about import start
|
||||
this.importCsvMessages.push(importMsg)
|
||||
this.importMessages.push(importMsg)
|
||||
|
||||
// Show import progress after 1 second
|
||||
importLoadingIndicator = setTimeout(() => {
|
||||
@@ -291,52 +348,108 @@ export default {
|
||||
this.addedTable = this.tableName
|
||||
// Inform about import success
|
||||
period = time.getPeriod(start, end)
|
||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
||||
importMsg.message = `Importing ${this.typeName} ` +
|
||||
`into a SQLite database is completed in ${period}.`
|
||||
importMsg.type = 'success'
|
||||
|
||||
// Loading indicator for import is not needed anymore
|
||||
clearTimeout(importLoadingIndicator)
|
||||
|
||||
this.importCsvCompleted = true
|
||||
this.importCompleted = true
|
||||
} else {
|
||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
||||
parseCsvMsg.type = 'info'
|
||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
||||
parsingMsg.message = 'Parsing ended with errors.'
|
||||
parsingMsg.type = 'info'
|
||||
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||
}
|
||||
} catch (err) {
|
||||
if (parseCsvMsg.type === 'loading') {
|
||||
parseCsvMsg.type = 'info'
|
||||
console.error(err)
|
||||
if (parsingMsg.type === 'loading') {
|
||||
parsingMsg.type = 'info'
|
||||
}
|
||||
|
||||
if (importMsg.type === 'loading') {
|
||||
importMsg.type = 'info'
|
||||
}
|
||||
|
||||
this.importCsvMessages.push({
|
||||
this.importMessages.push({
|
||||
message: err,
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
clearTimeout(parseCsvLoadingIndicator)
|
||||
clearTimeout(parsingLoadingIndicator)
|
||||
clearTimeout(importLoadingIndicator)
|
||||
this.db.deleteProgressCounter(progressCounterId)
|
||||
this.disableDialog = false
|
||||
},
|
||||
async finish () {
|
||||
this.$modal.hide(this.dialogName)
|
||||
const stmt = [
|
||||
const stmt = this.getQueryExample()
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCompleted = false
|
||||
this.$emit('finish')
|
||||
events.send('inquiry.create', null, { auto: true })
|
||||
},
|
||||
getQueryExample () {
|
||||
return this.isNdJson ? this.getNdJsonQueryExample()
|
||||
: this.isJson ? this.getJsonQueryExample()
|
||||
: [
|
||||
'/*',
|
||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||
' * You can run this SQL query to make all CSV records available for charting.',
|
||||
' */',
|
||||
`SELECT * FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||
this.$store.commit('setCurrentTabId', tabId)
|
||||
this.importCsvCompleted = false
|
||||
this.$emit('finish')
|
||||
events.send('inquiry.create', null, { auto: true })
|
||||
},
|
||||
getNdJsonQueryExample () {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
`SELECT doc->>'${firstKey}'`,
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
},
|
||||
getJsonQueryExample () {
|
||||
try {
|
||||
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||
const firstKey = Object.keys(firstRowJson)[0]
|
||||
return [
|
||||
'/*',
|
||||
` * Your JSON file has been imported into ${this.addedTable} table.`,
|
||||
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||
'and make them available for charting.',
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`,
|
||||
`JOIN json_each(doc, '$.${firstKey}')`
|
||||
].join('\n')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return [
|
||||
'/*',
|
||||
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||
' */',
|
||||
'SELECT *',
|
||||
`FROM "${this.addedTable}"`
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,10 +460,14 @@ export default {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
#csv-json-table-name {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.chars {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
margin: 24px 0 20px;
|
||||
margin: 0 0 20px;
|
||||
}
|
||||
.char-input {
|
||||
margin-right: 44px;
|
||||
@@ -359,7 +476,7 @@ export default {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.import-csv-errors {
|
||||
.import-errors {
|
||||
height: 136px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -10,7 +10,8 @@
|
||||
@click="browse"
|
||||
>
|
||||
<div class="text">
|
||||
Drop the database or CSV file here or click to choose a file from your computer.
|
||||
Drop the database, CSV, JSON or NDJSON file here
|
||||
or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,13 +42,13 @@
|
||||
</div>
|
||||
<div id="error" class="error"></div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
<!--Parse csv or json dialog -->
|
||||
<csv-json-import
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="newDb"
|
||||
dialog-name="importFromCsv"
|
||||
@cancel="cancelCsvImport"
|
||||
dialog-name="importFromCsvJson"
|
||||
@cancel="cancelImport"
|
||||
@finish="finish"
|
||||
/>
|
||||
</div>
|
||||
@@ -57,7 +58,7 @@
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||
import database from '@/lib/database'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
@@ -79,7 +80,7 @@ export default {
|
||||
},
|
||||
components: {
|
||||
ChangeDbIcon,
|
||||
CsvImport
|
||||
CsvJsonImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -102,7 +103,7 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
cancelCsvImport () {
|
||||
cancelImport () {
|
||||
if (this.newDb) {
|
||||
this.newDb.shutDown()
|
||||
this.newDb = null
|
||||
@@ -128,21 +129,22 @@ export default {
|
||||
if (fIo.isDatabase(file)) {
|
||||
this.loadDb(file)
|
||||
} else {
|
||||
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||
events.send('database.import', file.size, {
|
||||
from: 'csv',
|
||||
from: isJson ? 'json' : 'csv',
|
||||
new_db: true
|
||||
})
|
||||
|
||||
this.file = file
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
||||
.then(csvImport.open)
|
||||
const csvJsonImportModal = this.$refs.addCsvJson
|
||||
csvJsonImportModal.reset()
|
||||
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
|
||||
.then(csvJsonImportModal.open)
|
||||
}
|
||||
},
|
||||
browse () {
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||
.then(this.checkFile)
|
||||
},
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
:class="['icon-btn', { active }, { disabled }]"
|
||||
<button
|
||||
:class="['icon-btn', { active }]"
|
||||
:disabled="disabled"
|
||||
@click="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
@@ -12,7 +13,7 @@
|
||||
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -38,11 +39,12 @@ export default {
|
||||
box-sizing: border-box;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
.icon-btn:hover {
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -56,12 +58,12 @@ export default {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.disabled.icon-btn .icon >>> path,
|
||||
.disabled.icon-btn .icon >>> circle {
|
||||
.icon-btn:disabled .icon >>> path,
|
||||
.icon-btn:disabled .icon >>> circle {
|
||||
fill: var(--color-border);
|
||||
}
|
||||
|
||||
.disabled.icon-btn {
|
||||
.icon-btn:disabled {
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -75,14 +75,23 @@ export default {
|
||||
props: {
|
||||
horizontal: { type: Boolean, default: false },
|
||||
before: { type: Object },
|
||||
after: { type: Object }
|
||||
after: { type: Object },
|
||||
default: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
before: 50,
|
||||
after: 50
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
container: null,
|
||||
paneBefore: this.before,
|
||||
paneAfter: this.after,
|
||||
beforeMinimising: {
|
||||
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
|
||||
before: this.before.size,
|
||||
after: this.after.size
|
||||
},
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
ref="table-container"
|
||||
@scroll="onScrollTable"
|
||||
>
|
||||
<table ref="table" class="sqliteviz-table">
|
||||
<table
|
||||
ref="table"
|
||||
class="sqliteviz-table"
|
||||
tabindex="0"
|
||||
@keydown="onTableKeydown"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||
@@ -28,9 +33,18 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||
<td v-for="(col, colIndex) in columns" :key="colIndex">
|
||||
<td
|
||||
v-for="(col, colIndex) in columns"
|
||||
:data-col="colIndex"
|
||||
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
|
||||
:data-isNull="isNull(getCellValue(col, rowIndex))"
|
||||
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
|
||||
:key="colIndex"
|
||||
:aria-selected="false"
|
||||
@click="onCellClick"
|
||||
>
|
||||
<div class="cell-data" :style="cellStyle">
|
||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
||||
{{ getCellText(col, rowIndex) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -44,7 +58,11 @@
|
||||
<span v-if="preview">for preview</span>
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
<pager
|
||||
v-show="pageCount > 1"
|
||||
:page-count="pageCount"
|
||||
v-model="currentPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -57,19 +75,25 @@ export default {
|
||||
components: { Pager },
|
||||
props: {
|
||||
dataSet: Object,
|
||||
time: String,
|
||||
time: [String, Number],
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
preview: Boolean
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
preview: Boolean,
|
||||
selectedCellCoordinates: Object
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
tableWidth: null,
|
||||
currentPage: 1,
|
||||
resizeObserver: null
|
||||
currentPage: this.page,
|
||||
resizeObserver: null,
|
||||
selectedCellElement: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -99,7 +123,40 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
|
||||
if (this.selectedCellCoordinates) {
|
||||
const { row, col } = this.selectedCellCoordinates
|
||||
const cell = this.$refs.table
|
||||
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
|
||||
if (cell) {
|
||||
this.selectCell(cell)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isBlob (value) {
|
||||
return value && ArrayBuffer.isView(value)
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null
|
||||
},
|
||||
getCellValue (col, rowIndex) {
|
||||
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
|
||||
},
|
||||
getCellText (col, rowIndex) {
|
||||
const value = this.getCellValue(col, rowIndex)
|
||||
if (this.isNull(value)) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (this.isBlob(value)) {
|
||||
return 'BLOB'
|
||||
}
|
||||
return value
|
||||
},
|
||||
calculateHeadersWidth () {
|
||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||
this.$nextTick(() => {
|
||||
@@ -110,18 +167,95 @@ export default {
|
||||
},
|
||||
onScrollTable () {
|
||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||
this.resizeObserver.observe(this.$refs.table)
|
||||
this.calculateHeadersWidth()
|
||||
onTableKeydown (e) {
|
||||
const keyCodeMap = {
|
||||
37: 'left',
|
||||
39: 'right',
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
}
|
||||
|
||||
if (
|
||||
!this.selectedCellElement ||
|
||||
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||
},
|
||||
onCellClick (e) {
|
||||
this.selectCell(e.target.closest('td'), false)
|
||||
},
|
||||
selectCell (cell, scrollTo = true) {
|
||||
if (!cell) {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
this.selectedCellElement = cell
|
||||
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
cell.ariaSelected = 'true'
|
||||
this.selectedCellElement = cell
|
||||
} else {
|
||||
cell.ariaSelected = 'false'
|
||||
this.selectedCellElement = null
|
||||
}
|
||||
|
||||
if (this.selectedCellElement && scrollTo) {
|
||||
this.selectedCellElement.scrollIntoView()
|
||||
}
|
||||
|
||||
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||
},
|
||||
moveFocusInTable (initialCell, direction) {
|
||||
const currentRowIndex = +initialCell.dataset.row
|
||||
const currentColIndex = +initialCell.dataset.col
|
||||
let newRowIndex, newColIndex
|
||||
|
||||
if (direction === 'right') {
|
||||
if (currentColIndex === this.columns.length - 1) {
|
||||
newRowIndex = currentRowIndex + 1
|
||||
newColIndex = 0
|
||||
} else {
|
||||
newRowIndex = currentRowIndex
|
||||
newColIndex = currentColIndex + 1
|
||||
}
|
||||
} else if (direction === 'left') {
|
||||
if (currentColIndex === 0) {
|
||||
newRowIndex = currentRowIndex - 1
|
||||
newColIndex = this.columns.length - 1
|
||||
} else {
|
||||
newRowIndex = currentRowIndex
|
||||
newColIndex = currentColIndex - 1
|
||||
}
|
||||
} else if (direction === 'up') {
|
||||
newRowIndex = currentRowIndex - 1
|
||||
newColIndex = currentColIndex
|
||||
} else if (direction === 'down') {
|
||||
newRowIndex = currentRowIndex + 1
|
||||
newColIndex = currentColIndex
|
||||
}
|
||||
|
||||
const newCell = this.$refs.table
|
||||
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
|
||||
if (newCell) {
|
||||
this.selectCell(newCell)
|
||||
}
|
||||
}
|
||||
},
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.table)
|
||||
},
|
||||
watch: {
|
||||
currentPageData: 'calculateHeadersWidth',
|
||||
currentPageData () {
|
||||
this.calculateHeadersWidth()
|
||||
this.selectCell(null)
|
||||
},
|
||||
dataSet () {
|
||||
this.currentPage = 1
|
||||
}
|
||||
@@ -130,4 +264,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table.sqliteviz-table:focus {
|
||||
outline: none;
|
||||
}
|
||||
.sqliteviz-table tbody td:hover {
|
||||
background-color: var(--color-bg-light-3);
|
||||
}
|
||||
.sqliteviz-table tbody td[aria-selected="true"] {
|
||||
box-shadow:inset 0 0 0 1px var(--color-accent);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
</defs>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
Add new table from CSV
|
||||
Add new table from CSV, JSON or NDJSON
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
20
src/components/svg/arrow.vue
Normal file
20
src/components/svg/arrow.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<svg
|
||||
width="28"
|
||||
height="27"
|
||||
viewBox="0 0 28 27"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
|
||||
13.5L16.35 6.75L17.9475 8.33625Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
}
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
Load another database or CSV
|
||||
Load another database, CSV, JSON or NDJSON
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
26
src/components/svg/edgeArrow.vue
Normal file
26
src/components/svg/edgeArrow.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<svg
|
||||
width="27"
|
||||
height="27"
|
||||
viewBox="0 0 27 27"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
|
||||
13.5L15.7499 6.75L17.3474 8.33625Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
|
||||
fill="#506784"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
}
|
||||
</script>
|
||||
53
src/components/svg/row.vue
Normal file
53
src/components/svg/row.vue
Normal file
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<svg
|
||||
width="19"
|
||||
height="19"
|
||||
viewBox="0 0 19 19"
|
||||
fill="none"
|
||||
>
|
||||
<g clip-path="url(#clip0_2130_5292)">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
|
||||
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
|
||||
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
|
||||
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
|
||||
6.30371L16.853 6.30371C17.6815 6.30371 18.353 6.97528 18.353
|
||||
7.80371L18.353 11.3794C18.353 12.2078 17.6815 12.8794 16.853
|
||||
12.8794L15.353 12.8794L15.353 14.3111C15.353 15.0153 14.7603 15.5916
|
||||
14.0358 15.5916L4.67027 15.5916C3.94579 15.5916 3.35303 15.0153 3.35303
|
||||
14.3111L3.35303 12.8794L1.85303 12.8794C1.0246 12.8794 0.353027 12.2078
|
||||
0.353027 11.3794L0.353027 7.80371C0.353027 6.97528 1.0246 6.30371
|
||||
1.85303 6.30371L3.35303 6.30371L3.35303 4.87201C3.35303 4.16349 3.94139
|
||||
3.59155 4.67027 3.59155L14.0358 3.59155C14.7604 3.59155 15.353 4.16117
|
||||
15.353 4.87201L15.353 6.30371ZM14.0315 6.30371L14.0315 4.87086L11.887
|
||||
4.87086L11.887 6.30371L12.8428 6.30371L14.0315 6.30371ZM10.387
|
||||
6.30371L10.387 4.87086L8.26685 4.87086L8.26685 6.30371L10.387
|
||||
6.30371ZM6.76685 6.30371L6.76685 4.87086L4.67027 4.87086L4.67027
|
||||
6.30371L6.76685 6.30371ZM4.67027 12.8794L4.67027 14.3121L6.76685
|
||||
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
|
||||
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
|
||||
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2130_5292">
|
||||
<rect
|
||||
width="18"
|
||||
height="18"
|
||||
fill="white"
|
||||
transform="translate(0.353027 18.5916) rotate(-90)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'RowIcon'
|
||||
}
|
||||
</script>
|
||||
50
src/components/svg/viewCellValue.vue
Normal file
50
src/components/svg/viewCellValue.vue
Normal file
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<svg
|
||||
width="19"
|
||||
height="19"
|
||||
viewBox="0 0 19 19"
|
||||
fill="none"
|
||||
>
|
||||
<g clip-path="url(#clip0_2131_6054)">
|
||||
<path
|
||||
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
|
||||
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
|
||||
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
|
||||
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
|
||||
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
|
||||
11.5846H3.53784Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
|
||||
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
|
||||
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
|
||||
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
|
||||
17.659 17.802C18.0027 17.5708 18.0939 17.1048 17.8628 16.7611L15.6884
|
||||
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
|
||||
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
|
||||
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_2131_6054">
|
||||
<rect
|
||||
width="18"
|
||||
height="18"
|
||||
fill="white"
|
||||
transform="translate(0.5 18.5916) rotate(-90)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ViewCellValueIcon'
|
||||
}
|
||||
</script>
|
||||
@@ -7,9 +7,9 @@ const hintsByCode = {
|
||||
}
|
||||
|
||||
export default {
|
||||
getResult (source) {
|
||||
getResult (source, columns) {
|
||||
const result = {
|
||||
columns: []
|
||||
columns: columns || []
|
||||
}
|
||||
const values = {}
|
||||
if (source.meta.fields) {
|
||||
@@ -24,8 +24,18 @@ export default {
|
||||
return value
|
||||
})
|
||||
})
|
||||
} else if (columns) {
|
||||
columns.forEach((col, i) => {
|
||||
values[col] = source.data.map(row => {
|
||||
let value = row[i]
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
return value
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i <= source.data[0].length - 1; i++) {
|
||||
for (let i = 0; source.data[0] && i <= source.data[0].length - 1; i++) {
|
||||
const colName = `col${i + 1}`
|
||||
result.columns.push(colName)
|
||||
values[colName] = source.data.map(row => {
|
||||
@@ -73,8 +83,10 @@ export default {
|
||||
comments: false,
|
||||
step: undefined,
|
||||
complete: results => {
|
||||
const res = {
|
||||
data: this.getResult(results),
|
||||
let res
|
||||
try {
|
||||
res = {
|
||||
data: this.getResult(results, config.columns),
|
||||
delimiter: results.meta.delimiter,
|
||||
hasErrors: false,
|
||||
rowCount: results.data.length
|
||||
@@ -85,9 +97,12 @@ export default {
|
||||
msg.hint = hintsByCode[msg.code]
|
||||
return msg
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
resolve(res)
|
||||
},
|
||||
error: (error, file) => {
|
||||
error: error => {
|
||||
reject(error)
|
||||
},
|
||||
download: false,
|
||||
|
||||
@@ -8,11 +8,11 @@ function _getDataSourcesFromSqlResult (sqlResult) {
|
||||
if (!sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const dataSources = {}
|
||||
sqlResult.columns.forEach((column, index) => {
|
||||
dataSorces[column] = sqlResult.values.map(row => row[index])
|
||||
dataSources[column] = sqlResult.values.map(row => row[index])
|
||||
})
|
||||
return dataSorces
|
||||
return dataSources
|
||||
}
|
||||
|
||||
export default class Sql {
|
||||
|
||||
@@ -77,7 +77,7 @@ class 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',
|
||||
|
||||
@@ -33,40 +33,7 @@ export default {
|
||||
},
|
||||
|
||||
isTabNeedName (inquiryTab) {
|
||||
const isFromScratch = !inquiryTab.initName
|
||||
return inquiryTab.isPredefined || isFromScratch
|
||||
},
|
||||
|
||||
save (inquiryTab, newName) {
|
||||
const value = {
|
||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||
query: inquiryTab.query,
|
||||
viewType: inquiryTab.$refs.dataView.mode,
|
||||
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(),
|
||||
name: newName || inquiryTab.initName
|
||||
}
|
||||
|
||||
// Get inquiries from local storage
|
||||
const myInquiries = this.getStoredInquiries()
|
||||
|
||||
// Set createdAt
|
||||
if (newName) {
|
||||
value.createdAt = new Date()
|
||||
} else {
|
||||
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
|
||||
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||
}
|
||||
|
||||
// Insert in inquiries list
|
||||
if (newName) {
|
||||
myInquiries.push(value)
|
||||
} else {
|
||||
myInquiries[inquiryIndex] = value
|
||||
}
|
||||
|
||||
// Save to local storage
|
||||
this.updateStorage(myInquiries)
|
||||
return value
|
||||
return inquiryTab.isPredefined || !inquiryTab.name
|
||||
},
|
||||
|
||||
updateStorage (inquiries) {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import Lib from 'plotly.js/src/lib'
|
||||
import dataUrlToBlob from 'dataurl-to-blob'
|
||||
|
||||
export default {
|
||||
async copyCsv (str) {
|
||||
async copyText (str, notifyMessage) {
|
||||
await navigator.clipboard.writeText(str)
|
||||
Lib.notifier('CSV copied to clipboard successfully', 'long')
|
||||
if (notifyMessage) {
|
||||
Lib.notifier(notifyMessage, 'long')
|
||||
}
|
||||
},
|
||||
|
||||
async copyImage (source) {
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export default {
|
||||
isJSON (file) {
|
||||
return file && file.type === 'application/json'
|
||||
},
|
||||
isNDJSON (file) {
|
||||
return file && file.name.endsWith('.ndjson')
|
||||
},
|
||||
isDatabase (file) {
|
||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||
return file.type
|
||||
@@ -51,16 +57,17 @@ export default {
|
||||
},
|
||||
|
||||
importFile () {
|
||||
const reader = new FileReader()
|
||||
|
||||
return this.getFileFromUser('.json')
|
||||
.then(file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onload = e => {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
return this.getFileContent(file)
|
||||
})
|
||||
},
|
||||
|
||||
getFileContent (file) {
|
||||
const reader = new FileReader()
|
||||
return new Promise(resolve => {
|
||||
reader.onload = e => resolve(e.target.result)
|
||||
reader.readAsText(file)
|
||||
})
|
||||
},
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import Workspace from '@/views/Main/Workspace'
|
||||
import Inquiries from '@/views/Main/Inquiries'
|
||||
import Welcome from '@/views/Welcome'
|
||||
import Main from '@/views/Main'
|
||||
import LoadView from '@/views/LoadView'
|
||||
import store from '@/store'
|
||||
import database from '@/lib/database'
|
||||
|
||||
@@ -31,6 +32,11 @@ const routes = [
|
||||
component: Inquiries
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/load',
|
||||
name: 'Load',
|
||||
component: LoadView
|
||||
}
|
||||
]
|
||||
|
||||
@@ -39,7 +45,7 @@ const router = new VueRouter({
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
if (!store.state.db) {
|
||||
if (!store.state.db && to.name !== 'Load') {
|
||||
const newDb = database.getNewDatabase()
|
||||
await newDb.loadDb()
|
||||
store.commit('setDb', newDb)
|
||||
|
||||
@@ -1,32 +1,82 @@
|
||||
import Tab from '@/lib/tab'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export default {
|
||||
async addTab ({ state }, data) {
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// 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)) {
|
||||
async addTab ({ state }, inquiry = {}) {
|
||||
// add new tab only if it was not already opened
|
||||
if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
|
||||
const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
|
||||
state.tabs.push(tab)
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return tab.id
|
||||
}
|
||||
|
||||
return inquiry.id
|
||||
},
|
||||
async saveInquiry ({ state }, { inquiryTab, newName }) {
|
||||
const value = {
|
||||
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||
query: inquiryTab.query,
|
||||
viewType: inquiryTab.dataView.mode,
|
||||
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||
name: newName || inquiryTab.name
|
||||
}
|
||||
|
||||
// Get inquiries from local storage
|
||||
const myInquiries = state.inquiries
|
||||
|
||||
// Set createdAt
|
||||
if (newName) {
|
||||
value.createdAt = new Date()
|
||||
} else {
|
||||
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
|
||||
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||
}
|
||||
|
||||
// Insert in inquiries list
|
||||
if (newName) {
|
||||
myInquiries.push(value)
|
||||
} else {
|
||||
myInquiries.splice(inquiryIndex, 1, value)
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
addInquiry ({ state }, newInquiry) {
|
||||
state.inquiries.push(newInquiry)
|
||||
},
|
||||
deleteInquiries ({ state, commit }, inquiryIdSet) {
|
||||
state.inquiries = state.inquiries.filter(
|
||||
inquiry => !inquiryIdSet.has(inquiry.id)
|
||||
)
|
||||
|
||||
// Close deleted inquiries if it was opened
|
||||
const tabs = state.tabs
|
||||
let i = tabs.length - 1
|
||||
while (i > -1) {
|
||||
if (inquiryIdSet.has(tabs[i].id)) {
|
||||
commit('deleteTab', tabs[i])
|
||||
}
|
||||
i--
|
||||
}
|
||||
},
|
||||
renameInquiry ({ state, commit }, { inquiryId, newName }) {
|
||||
const renamingInquiry = state.inquiries
|
||||
.find(inquiry => inquiry.id === inquiryId)
|
||||
|
||||
renamingInquiry.name = newName
|
||||
|
||||
// update tab, if renamed inquiry is opened
|
||||
const tab = state.tabs.find(tab => tab.id === renamingInquiry.id)
|
||||
if (tab) {
|
||||
commit('updateTab', {
|
||||
tab,
|
||||
newValues: {
|
||||
name: newName
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import Vue from 'vue'
|
||||
|
||||
export default {
|
||||
setDb (state, db) {
|
||||
if (state.db) {
|
||||
@@ -8,8 +6,8 @@ export default {
|
||||
state.db = db
|
||||
},
|
||||
|
||||
updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) {
|
||||
const tab = state.tabs[index]
|
||||
updateTab (state, { tab, newValues }) {
|
||||
const { name, id, query, viewType, viewOptions, isSaved } = newValues
|
||||
const oldId = tab.id
|
||||
|
||||
if (id && state.currentTabId === oldId) {
|
||||
@@ -26,32 +24,44 @@ export default {
|
||||
// Saved inquiry is not predefined
|
||||
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 (state.tabs[index].id === state.currentTabId) {
|
||||
if (tab.id === state.currentTabId) {
|
||||
if (index < state.tabs.length - 1) {
|
||||
state.currentTabId = state.tabs[index + 1].id
|
||||
} else if (index > 0) {
|
||||
state.currentTabId = state.tabs[index - 1].id
|
||||
} else {
|
||||
state.currentTabId = null
|
||||
state.currentTab = null
|
||||
state.untitledLastIndex = 0
|
||||
}
|
||||
state.currentTab = state.currentTabId
|
||||
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||
: null
|
||||
}
|
||||
state.tabs.splice(index, 1)
|
||||
},
|
||||
setCurrentTabId (state, id) {
|
||||
try {
|
||||
state.currentTabId = id
|
||||
},
|
||||
setCurrentTab (state, tab) {
|
||||
state.currentTab = tab
|
||||
state.currentTab = state.tabs.find(tab => tab.id === id)
|
||||
} catch (e) {
|
||||
console.error('Can\'t open a tab id:' + id)
|
||||
}
|
||||
},
|
||||
updatePredefinedInquiries (state, inquiries) {
|
||||
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
||||
},
|
||||
setLoadingPredefinedInquiries (state, value) {
|
||||
state.loadingPredefinedInquiries = value
|
||||
},
|
||||
setPredefinedInquiriesLoaded (state, value) {
|
||||
state.predefinedInquiriesLoaded = value
|
||||
},
|
||||
setInquiries (state, value) {
|
||||
state.inquiries = value
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ export default {
|
||||
currentTab: null,
|
||||
currentTabId: null,
|
||||
untitledLastIndex: 0,
|
||||
inquiries: [],
|
||||
predefinedInquiries: [],
|
||||
loadingPredefinedInquiries: false,
|
||||
predefinedInquiriesLoaded: false,
|
||||
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>
|
||||
@@ -1,11 +1,18 @@
|
||||
<template>
|
||||
<div>
|
||||
<div id="my-inquiries-container">
|
||||
<div id="start-guide" v-if="allInquiries.length === 0">
|
||||
You don't have saved inquiries so far.
|
||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
||||
the one from scratch or
|
||||
<span @click="importInquiries" class="link">import</span> from a file.
|
||||
</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-toolbar">
|
||||
<div id="toolbar-buttons">
|
||||
@@ -157,6 +164,7 @@ import DeleteIcon from './svg/delete'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
import TextField from '@/components/TextField'
|
||||
import CheckBox from '@/components/CheckBox'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
|
||||
@@ -169,12 +177,12 @@ export default {
|
||||
DeleteIcon,
|
||||
CloseIcon,
|
||||
TextField,
|
||||
CheckBox
|
||||
CheckBox,
|
||||
LoadingIndicator
|
||||
},
|
||||
mixins: [tooltipMixin],
|
||||
data () {
|
||||
return {
|
||||
inquiries: [],
|
||||
filter: null,
|
||||
newName: null,
|
||||
processedInquiryId: null,
|
||||
@@ -189,6 +197,9 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
inquiries () {
|
||||
return this.$store.state.inquiries
|
||||
},
|
||||
predefinedInquiries () {
|
||||
return this.$store.state.predefinedInquiries.map(inquiry => {
|
||||
inquiry.isPredefined = true
|
||||
@@ -248,15 +259,20 @@ export default {
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
storedInquiries.readPredefinedInquiries()
|
||||
.then(inquiries => {
|
||||
async created () {
|
||||
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)
|
||||
})
|
||||
.catch(console.error)
|
||||
.finally(() => {
|
||||
this.inquiries = storedInquiries.getStoredInquiries()
|
||||
})
|
||||
this.$store.commit('setPredefinedInquiriesLoaded', true)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
this.$store.commit('setLoadingPredefinedInquiries', false)
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
|
||||
@@ -315,29 +331,17 @@ export default {
|
||||
this.errorMsg = "Inquiry name can't be empty"
|
||||
return
|
||||
}
|
||||
const processedInquiry = this.inquiries[this.processedInquiryIndex]
|
||||
processedInquiry.name = this.newName
|
||||
this.$set(this.inquiries, this.processedInquiryIndex, processedInquiry)
|
||||
|
||||
// update inquiries in local storage
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
|
||||
// update tab, if renamed inquiry is opened
|
||||
const tabIndex = this.findTabIndex(processedInquiry.id)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('updateTab', {
|
||||
index: tabIndex,
|
||||
name: this.newName,
|
||||
id: processedInquiry.id
|
||||
this.$store.dispatch('renameInquiry', {
|
||||
inquiryId: this.processedInquiryId,
|
||||
newName: this.newName
|
||||
})
|
||||
}
|
||||
|
||||
// hide dialog
|
||||
this.$modal.hide('rename')
|
||||
},
|
||||
duplicateInquiry (index) {
|
||||
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
|
||||
this.inquiries.push(newInquiry)
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
this.$store.dispatch('addInquiry', newInquiry)
|
||||
},
|
||||
showDeleteDialog (idsSet) {
|
||||
this.deleteGroup = idsSet.size > 1
|
||||
@@ -349,39 +353,19 @@ export default {
|
||||
deleteInquiry () {
|
||||
this.$modal.hide('delete')
|
||||
if (!this.deleteGroup) {
|
||||
this.inquiries.splice(this.processedInquiryIndex, 1)
|
||||
|
||||
// Close deleted inquiry tab if it was opened
|
||||
const tabIndex = this.findTabIndex(this.processedInquiryId)
|
||||
if (tabIndex >= 0) {
|
||||
this.$store.commit('deleteTab', tabIndex)
|
||||
}
|
||||
this.$store.dispatch('deleteInquiries', new Set().add(this.processedInquiryId))
|
||||
|
||||
// Clear checkbox
|
||||
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
|
||||
this.selectedInquiriesIds.delete(this.processedInquiryId)
|
||||
}
|
||||
} else {
|
||||
this.inquiries = this.inquiries.filter(
|
||||
inquiry => !this.selectedInquiriesIds.has(inquiry.id)
|
||||
)
|
||||
|
||||
// Close deleted inquiries if it was opened
|
||||
const tabs = this.$store.state.tabs
|
||||
for (let i = tabs.length - 1; i >= 0; i--) {
|
||||
if (this.selectedInquiriesIds.has(tabs[i].id)) {
|
||||
this.$store.commit('deleteTab', i)
|
||||
}
|
||||
}
|
||||
this.$store.dispatch('deleteInquiries', this.selectedInquiriesIds)
|
||||
|
||||
// Clear checkboxes
|
||||
this.selectedInquiriesIds.clear()
|
||||
}
|
||||
this.selectedInquiriesCount = this.selectedInquiriesIds.size
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
},
|
||||
findTabIndex (id) {
|
||||
return this.$store.state.tabs.findIndex(tab => tab.id === id)
|
||||
},
|
||||
exportToFile (inquiryList, fileName) {
|
||||
storedInquiries.export(inquiryList, fileName)
|
||||
@@ -397,8 +381,7 @@ export default {
|
||||
importInquiries () {
|
||||
storedInquiries.importInquiries()
|
||||
.then(importedInquiries => {
|
||||
this.inquiries = this.inquiries.concat(importedInquiries)
|
||||
storedInquiries.updateStorage(this.inquiries)
|
||||
this.$store.commit('setInquiries', this.inquiries.concat(importedInquiries))
|
||||
})
|
||||
},
|
||||
|
||||
@@ -441,6 +424,21 @@ export default {
|
||||
</script>
|
||||
|
||||
<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 {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
|
||||
@@ -80,19 +80,10 @@ export default {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isSaved () {
|
||||
if (!this.currentInquiry) {
|
||||
return false
|
||||
}
|
||||
const tabIndex = this.currentInquiry.tabIndex
|
||||
const tab = this.$store.state.tabs[tabIndex]
|
||||
return tab && tab.isSaved
|
||||
return this.currentInquiry && this.currentInquiry.isSaved
|
||||
},
|
||||
isPredefined () {
|
||||
if (this.currentInquiry) {
|
||||
return this.currentInquiry.isPredefined
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return this.currentInquiry && this.currentInquiry.isPredefined
|
||||
},
|
||||
runDisabled () {
|
||||
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
|
||||
@@ -131,7 +122,7 @@ export default {
|
||||
this.saveInquiry()
|
||||
}
|
||||
},
|
||||
saveInquiry () {
|
||||
async saveInquiry () {
|
||||
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
|
||||
if (isNeedName && !this.name) {
|
||||
this.errorMsg = 'Inquiry name can\'t be empty'
|
||||
@@ -141,17 +132,22 @@ export default {
|
||||
const tabView = this.currentInquiry.view
|
||||
|
||||
// Save inquiry
|
||||
const value = storedInquiries.save(this.currentInquiry, this.name)
|
||||
const value = await this.$store.dispatch('saveInquiry', {
|
||||
inquiryTab: this.currentInquiry,
|
||||
newName: this.name
|
||||
})
|
||||
|
||||
// Update tab in store
|
||||
this.$store.commit('updateTab', {
|
||||
index: this.currentInquiry.tabIndex,
|
||||
tab: this.currentInquiry,
|
||||
newValues: {
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
viewType: value.viewType,
|
||||
viewOptions: value.viewOptions,
|
||||
isSaved: true
|
||||
}
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
</div>
|
||||
<db-uploader id="db-edit" type="small" />
|
||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||
<add-table-icon @click="addCsv"/>
|
||||
<add-table-icon @click="addCsvJson"/>
|
||||
</div>
|
||||
<div v-show="schemaVisible" class="schema">
|
||||
<table-description
|
||||
@@ -21,12 +21,12 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!--Parse csv dialog -->
|
||||
<csv-import
|
||||
ref="addCsv"
|
||||
<!--Parse csv or json dialog -->
|
||||
<csv-json-import
|
||||
ref="addCsvJson"
|
||||
:file="file"
|
||||
:db="$store.state.db"
|
||||
dialog-name="addCsv"
|
||||
dialog-name="addCsvJson"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -40,7 +40,7 @@ import TreeChevron from '@/components/svg/treeChevron'
|
||||
import DbUploader from '@/components/DbUploader'
|
||||
import ExportIcon from '@/components/svg/export'
|
||||
import AddTableIcon from '@/components/svg/addTable'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
|
||||
export default {
|
||||
name: 'Schema',
|
||||
@@ -51,7 +51,7 @@ export default {
|
||||
DbUploader,
|
||||
ExportIcon,
|
||||
AddTableIcon,
|
||||
CsvImport
|
||||
CsvJsonImport
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
@@ -80,16 +80,17 @@ export default {
|
||||
exportToFile () {
|
||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||
},
|
||||
async addCsv () {
|
||||
this.file = await fIo.getFileFromUser('.csv')
|
||||
async addCsvJson () {
|
||||
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
|
||||
await this.$nextTick()
|
||||
const csvImport = this.$refs.addCsv
|
||||
csvImport.reset()
|
||||
await csvImport.previewCsv()
|
||||
csvImport.open()
|
||||
const csvJsonImportModal = this.$refs.addCsvJson
|
||||
csvJsonImportModal.reset()
|
||||
await csvJsonImportModal.preview()
|
||||
csvJsonImportModal.open()
|
||||
|
||||
const isJson = fIo.isJSON(this.file) || fIo.isNDJSON(this.file)
|
||||
events.send('database.import', this.file.size, {
|
||||
from: 'csv',
|
||||
from: isJson ? 'json' : 'csv',
|
||||
new_db: false
|
||||
})
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
<script>
|
||||
import plotly from 'plotly.js'
|
||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
||||
import 'react-chart-editor/lib/react-chart-editor.css'
|
||||
|
||||
import PlotlyEditor from 'react-chart-editor'
|
||||
import chartHelper from '@/lib/chartHelper'
|
||||
@@ -66,7 +66,8 @@ export default {
|
||||
notifyOnLogging: 1
|
||||
})
|
||||
this.$watch(
|
||||
() => this.state.data.map(trace => `${trace.type}-${trace.mode}`)
|
||||
() => this.state && this.state.data && this.state.data
|
||||
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
|
||||
.join(','),
|
||||
(value) => {
|
||||
events.send('viz_plotly.render', null, {
|
||||
@@ -76,6 +77,7 @@ export default {
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
this.$emit('update:importToSvgEnabled', true)
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
|
||||
@@ -41,7 +41,6 @@
|
||||
>
|
||||
<png-icon />
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!importToSvgEnabled"
|
||||
tooltip="Save as SVG"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="record-navigator">
|
||||
<icon-button
|
||||
:disabled="value === 0"
|
||||
tooltip="First row"
|
||||
tooltip-position="top-left"
|
||||
class="first"
|
||||
@click="$emit('input', 0)"
|
||||
>
|
||||
<edge-arrow-icon :disabled="false" />
|
||||
</icon-button>
|
||||
<icon-button
|
||||
:disabled="value === 0"
|
||||
tooltip="Previous row"
|
||||
tooltip-position="top-left"
|
||||
class="prev"
|
||||
@click="$emit('input', value - 1)"
|
||||
>
|
||||
<arrow-icon :disabled="false" />
|
||||
</icon-button>
|
||||
<icon-button
|
||||
:disabled="value === total - 1"
|
||||
tooltip="Next row"
|
||||
tooltip-position="top-left"
|
||||
class="next"
|
||||
@click="$emit('input', value + 1)"
|
||||
>
|
||||
<arrow-icon :disabled="false" />
|
||||
</icon-button>
|
||||
<icon-button
|
||||
:disabled="value === total - 1"
|
||||
tooltip="Last row"
|
||||
tooltip-position="top-left"
|
||||
class="last"
|
||||
@click="$emit('input', total - 1)"
|
||||
>
|
||||
<edge-arrow-icon :disabled="false" />
|
||||
</icon-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from '@/components/IconButton'
|
||||
import ArrowIcon from '@/components/svg/arrow'
|
||||
import EdgeArrowIcon from '@/components/svg/edgeArrow'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconButton,
|
||||
ArrowIcon,
|
||||
EdgeArrowIcon
|
||||
},
|
||||
props: {
|
||||
value: Number,
|
||||
total: Number
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.record-navigator {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.record-navigator .next,
|
||||
.record-navigator .last {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
</style>
|
||||
221
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
221
src/views/Main/Workspace/Tabs/Tab/RunResult/Record/index.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="record-view">
|
||||
<div class="table-container">
|
||||
<table
|
||||
ref="table"
|
||||
class="sqliteviz-table"
|
||||
tabindex="0"
|
||||
@keydown="onTableKeydown"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th/>
|
||||
<th>
|
||||
<div class="cell-data">
|
||||
Row #{{ currentRowIndex + 1 }}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(col, index) in columns" :key="index">
|
||||
<th class="column-cell">{{ col }}</th>
|
||||
<td
|
||||
:data-col="index"
|
||||
:data-row="currentRowIndex"
|
||||
:data-isNull="isNull(getCellValue(col))"
|
||||
:data-isBlob="isBlob(getCellValue(col))"
|
||||
:key="index"
|
||||
:aria-selected="false"
|
||||
@click="onCellClick"
|
||||
>
|
||||
<div class="cell-data">
|
||||
{{ getCellText(col) }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<div class="table-footer-count">
|
||||
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
|
||||
<span v-if="time">in {{ time }}</span>
|
||||
</div>
|
||||
|
||||
<row-navigator v-model="currentRowIndex" :total="rowCount"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import RowNavigator from './RowNavigator.vue'
|
||||
|
||||
export default {
|
||||
components: { RowNavigator },
|
||||
props: {
|
||||
dataSet: Object,
|
||||
time: String,
|
||||
rowIndex: { type: Number, default: 0 },
|
||||
selectedColumnIndex: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
selectedCellElement: null,
|
||||
currentRowIndex: this.rowIndex
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return this.dataSet.columns
|
||||
},
|
||||
rowCount () {
|
||||
return this.dataSet.values[this.columns[0]].length
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
const col = this.selectedColumnIndex
|
||||
const row = this.currentRowIndex
|
||||
const cell = this.$refs.table
|
||||
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
|
||||
if (cell) {
|
||||
this.selectCell(cell)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
async currentRowIndex () {
|
||||
await this.$nextTick()
|
||||
if (this.selectedCellElement) {
|
||||
const previouslySelected = this.selectedCellElement
|
||||
this.selectCell(null)
|
||||
this.selectCell(previouslySelected)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
isBlob (value) {
|
||||
return value && ArrayBuffer.isView(value)
|
||||
},
|
||||
isNull (value) {
|
||||
return value === null
|
||||
},
|
||||
getCellValue (col) {
|
||||
return this.dataSet.values[col][this.currentRowIndex]
|
||||
},
|
||||
getCellText (col) {
|
||||
const value = this.getCellValue(col)
|
||||
if (this.isNull(value)) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (this.isBlob(value)) {
|
||||
return 'BLOB'
|
||||
}
|
||||
return value
|
||||
},
|
||||
onTableKeydown (e) {
|
||||
const keyCodeMap = {
|
||||
38: 'up',
|
||||
40: 'down'
|
||||
}
|
||||
|
||||
if (
|
||||
!this.selectedCellElement ||
|
||||
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||
) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
|
||||
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||
},
|
||||
onCellClick (e) {
|
||||
this.selectCell(e.target.closest('td'), false)
|
||||
},
|
||||
selectCell (cell, scrollTo = true) {
|
||||
if (!cell) {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
this.selectedCellElement = cell
|
||||
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||
if (this.selectedCellElement) {
|
||||
this.selectedCellElement.ariaSelected = 'false'
|
||||
}
|
||||
cell.ariaSelected = 'true'
|
||||
this.selectedCellElement = cell
|
||||
} else {
|
||||
cell.ariaSelected = 'false'
|
||||
this.selectedCellElement = null
|
||||
}
|
||||
|
||||
if (this.selectedCellElement && scrollTo) {
|
||||
this.selectedCellElement.scrollIntoView()
|
||||
this.selectedCellElement.closest('.table-container').scrollTo({ left: 0 })
|
||||
}
|
||||
|
||||
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||
},
|
||||
moveFocusInTable (initialCell, direction) {
|
||||
const currentColIndex = +initialCell.dataset.col
|
||||
const newColIndex = direction === 'up'
|
||||
? currentColIndex - 1
|
||||
: currentColIndex + 1
|
||||
|
||||
const newCell = this.$refs.table
|
||||
.querySelector(`td[data-col="${newColIndex}"][data-row="${this.currentRowIndex}"]`)
|
||||
if (newCell) {
|
||||
this.selectCell(newCell)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
table.sqliteviz-table:focus {
|
||||
outline: none;
|
||||
}
|
||||
.sqliteviz-table tbody td:hover {
|
||||
background-color: var(--color-bg-light-3);
|
||||
}
|
||||
.sqliteviz-table tbody td[aria-selected="true"] {
|
||||
box-shadow: inset 0 0 0 1px var(--color-accent);
|
||||
}
|
||||
|
||||
table.sqliteviz-table {
|
||||
margin-top: 0;
|
||||
}
|
||||
.sqliteviz-table thead tr th {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
.sqliteviz-table tbody tr th {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-bg-dark);
|
||||
color: var(--color-text-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
align-items: center;
|
||||
}
|
||||
.record-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.column-cell {
|
||||
max-width: 150px;
|
||||
}
|
||||
</style>
|
||||
207
src/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer.vue
Normal file
207
src/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<div class="value-viewer">
|
||||
<div class="value-viewer-toolbar">
|
||||
<button
|
||||
v-for="format in formats"
|
||||
:key="format.value"
|
||||
type="button"
|
||||
:aria-selected="currentFormat === format.value"
|
||||
:class="format.value"
|
||||
@click="currentFormat = format.value"
|
||||
>
|
||||
{{ format.text }}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="copy"
|
||||
@click="copyToClipboard"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
<div class="value-body">
|
||||
<codemirror
|
||||
v-if="currentFormat === 'json' && formattedJson"
|
||||
:value="formattedJson"
|
||||
:options="cmOptions"
|
||||
class="json-value"
|
||||
/>
|
||||
<pre
|
||||
v-if="currentFormat === 'text'"
|
||||
:class="['text-value', { 'meta-value': isNull || isBlob }]"
|
||||
>{{ cellText }}</pre>
|
||||
<logs
|
||||
v-if="messages && messages.length > 0"
|
||||
:messages="messages"
|
||||
class="messages"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { codemirror } from 'vue-codemirror'
|
||||
import 'codemirror/lib/codemirror.css'
|
||||
import 'codemirror/mode/javascript/javascript.js'
|
||||
import 'codemirror/addon/fold/foldcode.js'
|
||||
import 'codemirror/addon/fold/foldgutter.js'
|
||||
import 'codemirror/addon/fold/foldgutter.css'
|
||||
import 'codemirror/addon/fold/brace-fold.js'
|
||||
import 'codemirror/theme/neo.css'
|
||||
import cIo from '@/lib/utils/clipboardIo'
|
||||
import Logs from '@/components/Logs'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
codemirror,
|
||||
Logs
|
||||
},
|
||||
props: {
|
||||
cellValue: [String, Number, Uint8Array]
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
formats: [
|
||||
{ text: 'Text', value: 'text' },
|
||||
{ text: 'JSON', value: 'json' }
|
||||
],
|
||||
currentFormat: 'text',
|
||||
cmOptions: {
|
||||
tabSize: 4,
|
||||
mode: { name: 'javascript', json: true },
|
||||
theme: 'neo',
|
||||
lineNumbers: true,
|
||||
line: true,
|
||||
foldGutter: true,
|
||||
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
|
||||
readOnly: true
|
||||
},
|
||||
formattedJson: '',
|
||||
messages: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isBlob () {
|
||||
return this.cellValue && ArrayBuffer.isView(this.cellValue)
|
||||
},
|
||||
isNull () {
|
||||
return this.cellValue === null
|
||||
},
|
||||
cellText () {
|
||||
const value = this.cellValue
|
||||
if (this.isNull) {
|
||||
return 'NULL'
|
||||
}
|
||||
if (this.isBlob) {
|
||||
return 'BLOB'
|
||||
}
|
||||
return value
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
currentFormat () {
|
||||
this.messages = []
|
||||
this.formattedJson = ''
|
||||
if (this.currentFormat === 'json') {
|
||||
this.formatJson(this.cellValue)
|
||||
}
|
||||
},
|
||||
cellValue () {
|
||||
this.messages = []
|
||||
if (this.currentFormat === 'json') {
|
||||
this.formatJson(this.cellValue)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
formatJson (jsonStr) {
|
||||
try {
|
||||
this.formattedJson = JSON.stringify(
|
||||
JSON.parse(jsonStr), null, 4
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
this.formattedJson = ''
|
||||
this.messages = [{
|
||||
type: 'error',
|
||||
message: 'Can\'t parse JSON.'
|
||||
}]
|
||||
}
|
||||
},
|
||||
copyToClipboard () {
|
||||
cIo.copyText(this.currentFormat === 'json'
|
||||
? this.formattedJson
|
||||
: this.cellValue,
|
||||
'The value is copied to clipboard.'
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.value-viewer {
|
||||
background-color: var(--color-white);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.value-viewer-toolbar {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
.value-body {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
.text-value {
|
||||
padding: 0 8px;
|
||||
margin: 0;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.json-value {
|
||||
margin-top: -4px;
|
||||
}
|
||||
|
||||
.text-value.meta-value {
|
||||
font-style: italic;
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.messages {
|
||||
margin: 0 8px;
|
||||
}
|
||||
|
||||
.value-viewer-toolbar button {
|
||||
font-size: 10px;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-base);
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.value-viewer-toolbar button:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
}
|
||||
|
||||
.value-viewer-toolbar button[aria-selected="true"] {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
>>> .vue-codemirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
>>> .CodeMirror-cursor {
|
||||
width: 1px;
|
||||
background: var(--color-text-base);
|
||||
}
|
||||
</style>
|
||||
@@ -1,31 +1,31 @@
|
||||
<template>
|
||||
<div class="run-result-panel" ref="runResultPanel">
|
||||
<div class="run-result-panel-content">
|
||||
<div
|
||||
v-show="result === null && !isGettingResults && !error"
|
||||
class="table-preview result-before"
|
||||
<component
|
||||
:is="viewValuePanelVisible ? 'splitpanes':'div'"
|
||||
:before="{ size: 50, max: 100 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
:default="{ before: 50, after: 50 }"
|
||||
class="run-result-panel-content"
|
||||
>
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
||||
<loading-indicator :size="30"/>
|
||||
Fetching results...
|
||||
</div>
|
||||
<div
|
||||
v-show="result === undefined && !isGettingResults && !error"
|
||||
class="table-preview result-empty"
|
||||
>
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<logs v-if="error" :messages="[error]"/>
|
||||
<sql-table
|
||||
v-if="result"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:pageSize="pageSize"
|
||||
class="straight"
|
||||
<template #left-pane>
|
||||
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
|
||||
</template>
|
||||
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
|
||||
<template #right-pane v-if="viewValuePanelVisible">
|
||||
<div class="value-viewer-container">
|
||||
<value-viewer
|
||||
v-show="selectedCell"
|
||||
:cellValue="selectedCell
|
||||
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
|
||||
: ''"
|
||||
/>
|
||||
<div v-show="!selectedCell" class="table-preview">
|
||||
No cell selected to view
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
@@ -44,6 +44,26 @@
|
||||
>
|
||||
<clipboard-icon/>
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
tooltip="View record"
|
||||
tooltip-position="top-left"
|
||||
:active="viewRecord"
|
||||
@click="toggleViewRecord"
|
||||
>
|
||||
<row-icon/>
|
||||
</icon-button>
|
||||
|
||||
<icon-button
|
||||
:disabled="!result"
|
||||
tooltip="View value"
|
||||
tooltip-position="top-left"
|
||||
:active="viewValuePanelVisible"
|
||||
@click="toggleViewValuePanel"
|
||||
>
|
||||
<view-cell-value-icon/>
|
||||
</icon-button>
|
||||
</side-tool-bar>
|
||||
|
||||
<loading-dialog
|
||||
@@ -56,6 +76,48 @@
|
||||
@action="copyToClipboard"
|
||||
@cancel="cancelCopy"
|
||||
/>
|
||||
|
||||
<teleport :to="resultSetTeleportTarget">
|
||||
<div>
|
||||
<div
|
||||
v-show="result === null && !isGettingResults && !error"
|
||||
class="table-preview result-before"
|
||||
>
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
||||
<loading-indicator :size="30"/>
|
||||
Fetching results...
|
||||
</div>
|
||||
<div
|
||||
v-show="result === undefined && !isGettingResults && !error"
|
||||
class="table-preview result-empty"
|
||||
>
|
||||
No rows retrieved according to your query
|
||||
</div>
|
||||
<logs v-if="error" :messages="[error]"/>
|
||||
<sql-table
|
||||
v-if="result && !viewRecord"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:pageSize="pageSize"
|
||||
:page="defaultPage"
|
||||
:selected-cell-coordinates="defaultSelectedCell"
|
||||
class="straight"
|
||||
@updateSelectedCell="onUpdateSelectedCell"
|
||||
/>
|
||||
|
||||
<record
|
||||
ref="recordView"
|
||||
v-if="result && viewRecord"
|
||||
:data-set="result"
|
||||
:time="time"
|
||||
:selected-column-index="selectedCell ? +selectedCell.dataset.col : 0"
|
||||
:rowIndex="selectedCell ? +selectedCell.dataset.row : 0"
|
||||
@updateSelectedCell="onUpdateSelectedCell"
|
||||
/>
|
||||
</div>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -63,9 +125,12 @@
|
||||
import Logs from '@/components/Logs'
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
import SideToolBar from './SideToolBar'
|
||||
import SideToolBar from '../SideToolBar'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ExportToCsvIcon from '@/components/svg/exportToCsv'
|
||||
import ClipboardIcon from '@/components/svg/clipboard'
|
||||
import ViewCellValueIcon from '@/components/svg/viewCellValue'
|
||||
import RowIcon from '@/components/svg/row'
|
||||
import IconButton from '@/components/IconButton'
|
||||
import csv from '@/lib/csv'
|
||||
import fIo from '@/lib/utils/fileIo'
|
||||
@@ -73,16 +138,30 @@ import cIo from '@/lib/utils/clipboardIo'
|
||||
import time from '@/lib/utils/time'
|
||||
import loadingDialog from '@/components/LoadingDialog'
|
||||
import events from '@/lib/utils/events'
|
||||
import Teleport from 'vue2-teleport'
|
||||
import ValueViewer from './ValueViewer'
|
||||
import Record from './Record/index.vue'
|
||||
|
||||
export default {
|
||||
name: 'RunResult',
|
||||
props: ['result', 'isGettingResults', 'error', 'time'],
|
||||
props: {
|
||||
tab: Object,
|
||||
result: Object,
|
||||
isGettingResults: Boolean,
|
||||
error: Object,
|
||||
time: [String, Number]
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resizeObserver: null,
|
||||
pageSize: 20,
|
||||
preparingCopy: false,
|
||||
dataToCopy: null
|
||||
dataToCopy: null,
|
||||
viewValuePanelVisible: false,
|
||||
selectedCell: null,
|
||||
viewRecord: false,
|
||||
defaultPage: 1,
|
||||
defaultSelectedCell: null
|
||||
}
|
||||
},
|
||||
components: {
|
||||
@@ -93,7 +172,23 @@ export default {
|
||||
ExportToCsvIcon,
|
||||
IconButton,
|
||||
ClipboardIcon,
|
||||
loadingDialog
|
||||
ViewCellValueIcon,
|
||||
RowIcon,
|
||||
loadingDialog,
|
||||
ValueViewer,
|
||||
Record,
|
||||
Splitpanes,
|
||||
Teleport
|
||||
},
|
||||
computed: {
|
||||
resultSetTeleportTarget () {
|
||||
const base = `#${this.viewValuePanelVisible
|
||||
? 'run-result-left-pane'
|
||||
: 'run-result-result-set'
|
||||
}`
|
||||
const tabIdPostfix = `-${this.tab.id}`
|
||||
return base + tabIdPostfix
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||
@@ -103,6 +198,12 @@ export default {
|
||||
beforeDestroy () {
|
||||
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
||||
},
|
||||
watch: {
|
||||
result () {
|
||||
this.defaultSelectedCell = null
|
||||
this.selectedCell = null
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handleResize () {
|
||||
this.calculatePageSize()
|
||||
@@ -160,13 +261,35 @@ export default {
|
||||
},
|
||||
|
||||
copyToClipboard () {
|
||||
cIo.copyCsv(this.dataToCopy)
|
||||
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
},
|
||||
|
||||
cancelCopy () {
|
||||
this.dataToCopy = null
|
||||
this.$modal.hide('prepareCSVCopy')
|
||||
},
|
||||
|
||||
toggleViewValuePanel () {
|
||||
this.viewValuePanelVisible = !this.viewValuePanelVisible
|
||||
},
|
||||
|
||||
toggleViewRecord () {
|
||||
if (this.viewRecord) {
|
||||
this.defaultSelectedCell = {
|
||||
row: this.$refs.recordView.currentRowIndex,
|
||||
col: this.selectedCell ? +this.selectedCell.dataset.col : 0
|
||||
}
|
||||
this.defaultPage = Math.ceil(
|
||||
(this.$refs.recordView.currentRowIndex + 1) / this.pageSize
|
||||
)
|
||||
}
|
||||
|
||||
this.viewRecord = !this.viewRecord
|
||||
},
|
||||
|
||||
onUpdateSelectedCell (e) {
|
||||
this.selectedCell = e
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -180,12 +303,24 @@ export default {
|
||||
}
|
||||
|
||||
.run-result-panel-content {
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.result-set-container,
|
||||
.result-set-container > div {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.value-viewer-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--color-white);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-preview {
|
||||
position: absolute;
|
||||
@@ -194,6 +329,7 @@ export default {
|
||||
transform: translate(-50%, -50%);
|
||||
color: var(--color-text-base);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result-in-progress {
|
||||
@@ -3,44 +3,46 @@
|
||||
<splitpanes
|
||||
class="query-results-splitter"
|
||||
horizontal
|
||||
:before="{ size: 50, max: 100 }"
|
||||
:after="{ size: 50, max: 100 }"
|
||||
:before="{ size: topPaneSize, max: 100 }"
|
||||
:after="{ size: 100 - topPaneSize, max: 100 }"
|
||||
:default="{ before: 50, after: 50 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<div :id="'above-' + tabIndex" class="above" />
|
||||
<div :id="'above-' + tab.id" class="above" />
|
||||
</template>
|
||||
<template #right-pane>
|
||||
<div :id="'bottom-'+ tabIndex" ref="bottomPane" class="bottomPane" />
|
||||
<div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
|
||||
</template>
|
||||
</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
|
||||
ref="sqlEditor"
|
||||
v-model="query"
|
||||
:is-getting-results="isGettingResults"
|
||||
v-model="tab.query"
|
||||
:is-getting-results="tab.isGettingResults"
|
||||
@switchTo="onSwitchView('sqlEditor', $event)"
|
||||
@run="execute"
|
||||
@run="tab.execute()"
|
||||
/>
|
||||
</teleport>
|
||||
|
||||
<teleport :to="`#${layout.table}-${tabIndex}`">
|
||||
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
||||
<run-result
|
||||
:result="result"
|
||||
:is-getting-results="isGettingResults"
|
||||
:error="error"
|
||||
:time="time"
|
||||
:tab="tab"
|
||||
:result="tab.result"
|
||||
:is-getting-results="tab.isGettingResults"
|
||||
:error="tab.error"
|
||||
:time="tab.time"
|
||||
@switchTo="onSwitchView('table', $event)"
|
||||
/>
|
||||
</teleport>
|
||||
|
||||
<teleport :to="`#${layout.dataView}-${tabIndex}`">
|
||||
<teleport :to="`#${tab.layout.dataView}-${tab.id}`">
|
||||
<data-view
|
||||
:data-source="(result && result.values) || null"
|
||||
:init-options="initViewOptions"
|
||||
:init-mode="initViewType"
|
||||
:data-source="(tab.result && tab.result.values) || null"
|
||||
:init-options="tab.viewOptions"
|
||||
:init-mode="tab.viewType"
|
||||
ref="dataView"
|
||||
@switchTo="onSwitchView('dataView', $event)"
|
||||
@update="onDataViewUpdate"
|
||||
@@ -54,15 +56,15 @@ import Splitpanes from '@/components/Splitpanes'
|
||||
import SqlEditor from './SqlEditor'
|
||||
import DataView from './DataView'
|
||||
import RunResult from './RunResult'
|
||||
import time from '@/lib/utils/time'
|
||||
|
||||
import Teleport from 'vue2-teleport'
|
||||
import events from '@/lib/utils/events'
|
||||
|
||||
export default {
|
||||
name: 'Tab',
|
||||
props: [
|
||||
'id', 'initName', 'initQuery', 'initViewOptions', 'tabIndex', 'isPredefined', 'initViewType'
|
||||
],
|
||||
props: {
|
||||
tab: Object
|
||||
},
|
||||
components: {
|
||||
SqlEditor,
|
||||
DataView,
|
||||
@@ -72,21 +74,14 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
query: this.initQuery,
|
||||
result: null,
|
||||
isGettingResults: false,
|
||||
error: null,
|
||||
time: 0,
|
||||
layout: {
|
||||
sqlEditor: 'above',
|
||||
table: 'bottom',
|
||||
dataView: 'hidden'
|
||||
}
|
||||
topPaneSize: this.tab.maximize
|
||||
? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
|
||||
: 50
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isActive () {
|
||||
return this.id === this.$store.state.currentTabId
|
||||
return this.tab.id === this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
@@ -94,54 +89,34 @@ export default {
|
||||
immediate: true,
|
||||
async handler () {
|
||||
if (this.isActive) {
|
||||
this.$store.commit('setCurrentTab', this)
|
||||
await this.$nextTick()
|
||||
this.$refs.sqlEditor.focus()
|
||||
}
|
||||
}
|
||||
},
|
||||
query () {
|
||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
||||
'tab.query' () {
|
||||
this.$store.commit('updateTab', {
|
||||
tab: this.tab,
|
||||
newValues: { isSaved: false }
|
||||
})
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.tab.dataView = this.$refs.dataView
|
||||
},
|
||||
methods: {
|
||||
onSwitchView (from, to) {
|
||||
const fromPosition = this.layout[from]
|
||||
this.layout[from] = this.layout[to]
|
||||
this.layout[to] = fromPosition
|
||||
const fromPosition = this.tab.layout[from]
|
||||
this.tab.layout[from] = this.tab.layout[to]
|
||||
this.tab.layout[to] = fromPosition
|
||||
|
||||
events.send('inquiry.panel', null, { panel: to })
|
||||
},
|
||||
onDataViewUpdate () {
|
||||
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
|
||||
},
|
||||
async execute () {
|
||||
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())
|
||||
|
||||
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' })
|
||||
}
|
||||
state.db.refreshSchema()
|
||||
this.isGettingResults = false
|
||||
this.$store.commit('updateTab', {
|
||||
tab: this.tab,
|
||||
newValues: { isSaved: false }
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
@click="selectTab(tab.id)"
|
||||
:class="[{'tab-selected': (tab.id === selectedIndex)}, 'tab']"
|
||||
:class="[{'tab-selected': (tab.id === selectedTabId)}, 'tab']"
|
||||
>
|
||||
<div class="tab-name">
|
||||
<span v-show="!tab.isSaved" class="star">*</span>
|
||||
@@ -13,20 +13,14 @@
|
||||
<span v-else class="tab-untitled">{{ tab.tempName }}</span>
|
||||
</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>
|
||||
<tab
|
||||
v-for="(tab, index) in tabs"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
:id="tab.id"
|
||||
: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"
|
||||
:tab="tab"
|
||||
/>
|
||||
<div v-show="tabs.length === 0" id="start-guide">
|
||||
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
|
||||
@@ -38,25 +32,25 @@
|
||||
<modal name="close-warn" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Close tab {{
|
||||
closingTabIndex !== null
|
||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
||||
closingTab !== null
|
||||
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||
: ''
|
||||
}}
|
||||
<close-icon @click="$modal.hide('close-warn')"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
You have unsaved changes. Save changes in {{
|
||||
closingTabIndex !== null
|
||||
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`)
|
||||
closingTab !== null
|
||||
? (closingTab.name || `[${closingTab.tempName}]`)
|
||||
: ''
|
||||
}} before closing?
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="closeTab(closingTabIndex)">
|
||||
<button class="secondary" @click="closeTab(closingTab)">
|
||||
Close without saving
|
||||
</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>
|
||||
</modal>
|
||||
</div>
|
||||
@@ -73,14 +67,14 @@ export default {
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
closingTabIndex: null
|
||||
closingTab: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tabs () {
|
||||
return this.$store.state.tabs
|
||||
},
|
||||
selectedIndex () {
|
||||
selectedTabId () {
|
||||
return this.$store.state.currentTabId
|
||||
}
|
||||
},
|
||||
@@ -97,25 +91,24 @@ export default {
|
||||
selectTab (id) {
|
||||
this.$store.commit('setCurrentTabId', id)
|
||||
},
|
||||
beforeCloseTab (index) {
|
||||
this.closingTabIndex = index
|
||||
if (!this.tabs[index].isSaved) {
|
||||
beforeCloseTab (tab) {
|
||||
this.closingTab = tab
|
||||
if (!tab.isSaved) {
|
||||
this.$modal.show('close-warn')
|
||||
} else {
|
||||
this.closeTab(index)
|
||||
this.closeTab(tab)
|
||||
}
|
||||
},
|
||||
closeTab (index) {
|
||||
closeTab (tab) {
|
||||
this.$modal.hide('close-warn')
|
||||
this.closingTabIndex = null
|
||||
this.$store.commit('deleteTab', index)
|
||||
this.$store.commit('deleteTab', tab)
|
||||
},
|
||||
saveAndClose (index) {
|
||||
saveAndClose (tab) {
|
||||
this.$root.$on('inquirySaved', () => {
|
||||
this.closeTab(index)
|
||||
this.closeTab(tab)
|
||||
this.$root.$off('inquirySaved')
|
||||
})
|
||||
this.selectTab(this.tabs[index].id)
|
||||
this.selectTab(tab.id)
|
||||
this.$modal.hide('close-warn')
|
||||
this.$nextTick(() => {
|
||||
this.$root.$emit('saveInquiry')
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<div>
|
||||
<splitpanes
|
||||
class="schema-tabs-splitter"
|
||||
:before="{ size: 20, max: 30 }"
|
||||
:after="{ size: 80, max: 100 }"
|
||||
:before="{ size: schemaWidth, max: 30 }"
|
||||
:after="{ size: 100 - schemaWidth, max: 100 }"
|
||||
:default="{ before: 20, after: 80 }"
|
||||
>
|
||||
<template #left-pane>
|
||||
<schema/>
|
||||
@@ -28,9 +29,14 @@ export default {
|
||||
Splitpanes,
|
||||
Tabs
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
schemaWidth: this.$route.query.hide_schema === '1' ? 0 : 20
|
||||
}
|
||||
},
|
||||
async beforeCreate () {
|
||||
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 = [
|
||||
'/*',
|
||||
' * Your database is empty. In order to start building charts',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div>
|
||||
<main-menu />
|
||||
<keep-alive include="Workspace">
|
||||
<keep-alive include="Workspace,Inquiries">
|
||||
<router-view id="main-view" />
|
||||
</keep-alive>
|
||||
</div>
|
||||
|
||||
52
tests/App.spec.js
Normal file
52
tests/App.spec.js
Normal file
@@ -0,0 +1,52 @@
|
||||
import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import { shallowMount } from '@vue/test-utils'
|
||||
import Vuex from 'vuex'
|
||||
import App from '@/App'
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import mutations from '@/store/mutations'
|
||||
|
||||
describe('App.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('Gets inquiries', () => {
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
{ id: 1 }, { id: 2 }, { id: 3 }
|
||||
])
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
shallowMount(App, { store, stubs: ['router-view'] })
|
||||
|
||||
expect(state.inquiries).to.eql([{ id: 1 }, { id: 2 }, { id: 3 }])
|
||||
})
|
||||
|
||||
it('Updates inquiries when they change in store', async () => {
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
{ id: 1, name: 'foo' }, { id: 2, name: 'baz' }, { id: 3, name: 'bar' }
|
||||
])
|
||||
sinon.spy(storedInquiries, 'updateStorage')
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const wrapper = shallowMount(App, { store, stubs: ['router-view'] })
|
||||
|
||||
store.state.inquiries.splice(0, 1, { id: 1, name: 'new foo name' })
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(storedInquiries.updateStorage.calledTwice).to.equal(true)
|
||||
|
||||
expect(storedInquiries.updateStorage.args[1][0]).to.eql([
|
||||
{ id: 1, name: 'new foo name' },
|
||||
{ id: 2, name: 'baz' },
|
||||
{ id: 3, name: 'bar' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,10 @@ import { expect } from 'chai'
|
||||
import sinon from 'sinon'
|
||||
import Vuex from 'vuex'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import CsvImport from '@/components/CsvImport'
|
||||
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||
import csv from '@/lib/csv'
|
||||
|
||||
describe('CsvImport.vue', () => {
|
||||
describe('CsvJsonImport.vue', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
@@ -13,7 +13,7 @@ describe('CsvImport.vue', () => {
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = { name: 'my data.csv' }
|
||||
const file = new File([], 'my data.csv')
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
@@ -40,11 +40,11 @@ describe('CsvImport.vue', () => {
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvImport, {
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsv',
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
@@ -74,11 +74,12 @@ describe('CsvImport.vue', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('CSV import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
||||
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
||||
expect(wrapper.find('#escape-char input').element.value).to.equal('"')
|
||||
@@ -93,8 +94,36 @@ describe('CsvImport.vue', () => {
|
||||
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-start').attributes().disabled).to.equal(undefined)
|
||||
})
|
||||
|
||||
it('disables import if no rows found', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col2', 'col1'],
|
||||
values: {
|
||||
col1: [],
|
||||
col2: []
|
||||
}
|
||||
},
|
||||
rowCount: 0,
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(0)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('No rows to import.')
|
||||
expect(wrapper.find('.no-data').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-start').attributes().disabled).to.equal('disabled')
|
||||
})
|
||||
|
||||
it('reparses when parameters changes', async () => {
|
||||
@@ -111,7 +140,7 @@ describe('CsvImport.vue', () => {
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await csv.parse.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -231,20 +260,32 @@ describe('CsvImport.vue', () => {
|
||||
col2: ['foo']
|
||||
}
|
||||
},
|
||||
rowCount: 1
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = resolve
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['col1', 'col2'],
|
||||
values: {
|
||||
col1: [1],
|
||||
col2: ['foo']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing CSV..." in the logs
|
||||
@@ -262,11 +303,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
@@ -306,7 +347,7 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -315,8 +356,8 @@ describe('CsvImport.vue', () => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -329,11 +370,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
@@ -377,12 +418,12 @@ describe('CsvImport.vue', () => {
|
||||
resolveImport = resolve
|
||||
}))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -397,11 +438,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveImport()
|
||||
})
|
||||
|
||||
@@ -440,12 +481,12 @@ describe('CsvImport.vue', () => {
|
||||
}]
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -460,11 +501,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
@@ -501,12 +542,12 @@ describe('CsvImport.vue', () => {
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -525,11 +566,11 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
@@ -570,12 +611,12 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -589,10 +630,10 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('import fails', async () => {
|
||||
@@ -627,12 +668,12 @@ describe('CsvImport.vue', () => {
|
||||
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
@@ -647,10 +688,10 @@ describe('CsvImport.vue', () => {
|
||||
expect(wrapper.find('#quote-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
})
|
||||
|
||||
it('import finish', async () => {
|
||||
@@ -668,19 +709,19 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.previewCsv()
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
await wrapper.find('#import-finish').trigger('click')
|
||||
|
||||
expect(actions.addTab.calledOnce).to.equal(true)
|
||||
await actions.addTab.returnValues[0]
|
||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
@@ -699,47 +740,525 @@ describe('CsvImport.vue', () => {
|
||||
messages: []
|
||||
})
|
||||
|
||||
await wrapper.vm.previewCsv()
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-cancel').trigger('click')
|
||||
await wrapper.find('#import-cancel').trigger('click')
|
||||
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
expect(wrapper.vm.db.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
|
||||
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
||||
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
||||
})
|
||||
|
||||
it('checks table name', async () => {
|
||||
sinon.stub(csv, 'parse').resolves()
|
||||
await wrapper.vm.previewCsv()
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
data: {},
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
wrapper.vm.db.validateTableName = sinon.stub().rejects(new Error('this is a bad table name'))
|
||||
await wrapper.find('#csv-table-name input').setValue('bar')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('bar')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
|
||||
.to.equal('this is a bad table name. Try another table name.')
|
||||
|
||||
await wrapper.find('#csv-table-name input').setValue('')
|
||||
await wrapper.find('#csv-json-table-name input').setValue('')
|
||||
await clock.tick(400)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text()).to.equal('')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text()).to.equal('')
|
||||
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
|
||||
.to.equal("Table name can't be empty")
|
||||
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CsvJsonImport.vue - json', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = new File(
|
||||
[new Blob(
|
||||
[JSON.stringify({ foo: [1, 2, 3] }, null, 2)],
|
||||
{ type: 'application/json' }
|
||||
)],
|
||||
'my data.json',
|
||||
{ type: 'application/json' })
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const db = {
|
||||
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||
addTableFromCsv: sinon.stub().resolves(),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
validateTableName: sinon.stub().resolves(),
|
||||
execute: sinon.stub().resolves(),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('previews', async () => {
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal([
|
||||
'{',
|
||||
' "foo": [',
|
||||
' 1,',
|
||||
' 2,',
|
||||
' 3',
|
||||
' ]',
|
||||
'}'
|
||||
].join('\n')
|
||||
)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before parsing is complete', async () => {
|
||||
const getJsonParseResult = sinon.stub(wrapper.vm, 'getJsonParseResult')
|
||||
getJsonParseResult.onCall(0).returns({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveParsing
|
||||
getJsonParseResult.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing JSON..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.equal('Parsing JSON...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await getJsonParseResult.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await getJsonParseResult.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing JSON into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const getJsonParseResult = sinon.spy(wrapper.vm, 'getJsonParseResult')
|
||||
|
||||
await wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await getJsonParseResult.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('CsvJsonImport.vue - ndjson', () => {
|
||||
let state = {}
|
||||
let actions = {}
|
||||
let mutations = {}
|
||||
let store = {}
|
||||
let clock
|
||||
let wrapper
|
||||
const newTabId = 1
|
||||
const file = new File([], 'my data.ndjson')
|
||||
|
||||
beforeEach(() => {
|
||||
clock = sinon.useFakeTimers()
|
||||
|
||||
// mock store state and mutations
|
||||
state = {}
|
||||
mutations = {
|
||||
setDb: sinon.stub(),
|
||||
setCurrentTabId: sinon.stub()
|
||||
}
|
||||
actions = {
|
||||
addTab: sinon.stub().resolves(newTabId)
|
||||
}
|
||||
store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const db = {
|
||||
sanitizeTableName: sinon.stub().returns('my_data'),
|
||||
addTableFromCsv: sinon.stub().resolves(),
|
||||
createProgressCounter: sinon.stub().returns(1),
|
||||
deleteProgressCounter: sinon.stub(),
|
||||
validateTableName: sinon.stub().resolves(),
|
||||
execute: sinon.stub().resolves(),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
|
||||
// mount the component
|
||||
wrapper = mount(CsvJsonImport, {
|
||||
store,
|
||||
propsData: {
|
||||
file,
|
||||
dialogName: 'addCsvJson',
|
||||
db
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('previews', async () => {
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
await wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
expect(wrapper.find('.dialog-header').text()).to.equal('JSON import')
|
||||
expect(wrapper.find('#csv-json-table-name input').element.value).to.equal('my_data')
|
||||
expect(wrapper.findComponent({ name: 'delimiter-selector' }).exists()).to.equal(false)
|
||||
expect(wrapper.find('#quote-char input').exists()).to.equal(false)
|
||||
expect(wrapper.find('#escape-char input').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'check-box' }).exists()).to.equal(false)
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(1)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equal('{ "foo": [ 1, 2, 3 ] }')
|
||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||
.to.include('Preview parsing is completed in')
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
})
|
||||
|
||||
it('has proper state before parsing is complete', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
let resolveParsing
|
||||
parse.onCall(1).returns(new Promise(resolve => {
|
||||
resolveParsing = () => resolve({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
messages: []
|
||||
})
|
||||
}))
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// "Parsing JSON..." in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(1).text())
|
||||
.to.equal('Parsing JSON...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
await resolveParsing()
|
||||
await parse.returnValues[1]
|
||||
|
||||
// Loading indicator is not shown when parsing is compete
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('has proper state before import is completed', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
let resolveImport = sinon.stub()
|
||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Parsing success in the logs
|
||||
expect(wrapper.findComponent({ name: 'logs' }).findAll('.msg').at(2).text())
|
||||
.to.equal('Importing JSON into a SQLite database...')
|
||||
|
||||
// After 1 second - loading indicator is shown
|
||||
await clock.tick(1000)
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(true)
|
||||
|
||||
// All the dialog controls are disabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(true)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||
|
||||
// After resolving - loading indicator is not shown
|
||||
await resolveImport()
|
||||
await wrapper.vm.db.addTableFromCsv.returnValues[0]
|
||||
expect(
|
||||
wrapper.findComponent({ name: 'logs' }).findComponent({ name: 'LoadingIndicator' }).exists()
|
||||
).to.equal(false)
|
||||
})
|
||||
|
||||
it('import success', async () => {
|
||||
const parse = sinon.stub(csv, 'parse')
|
||||
parse.onCall(0).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 1,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
// we need to separate calles because messages will mutate
|
||||
parse.onCall(1).resolves({
|
||||
delimiter: '|',
|
||||
data: {
|
||||
columns: ['doc'],
|
||||
values: {
|
||||
doc: ['{ "foo": [ 1, 2, 3 ] }']
|
||||
}
|
||||
},
|
||||
rowCount: 2,
|
||||
hasErrors: false,
|
||||
messages: []
|
||||
})
|
||||
|
||||
wrapper.vm.preview()
|
||||
wrapper.vm.open()
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await csv.parse.returnValues[1]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Import success in the logs
|
||||
const logs = wrapper.findComponent({ name: 'logs' }).findAll('.msg')
|
||||
expect(logs).to.have.lengthOf(3)
|
||||
expect(logs.at(2).text()).to.contain('Importing JSON into a SQLite database is completed in')
|
||||
|
||||
// All the dialog controls are enabled
|
||||
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').element.disabled).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(false)
|
||||
expect(wrapper.find('#import-finish').isVisible()).to.equal(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, shallowMount } from '@vue/test-utils'
|
||||
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
||||
import DelimiterSelector from '@/components/CsvJsonImport/DelimiterSelector'
|
||||
|
||||
describe('DelimiterSelector', async () => {
|
||||
it('shows the name of value', async () => {
|
||||
|
||||
@@ -31,7 +31,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it('loads db on click and redirects to /workspace', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -85,7 +85,7 @@ describe('DbUploader.vue', () => {
|
||||
})
|
||||
|
||||
// mock a file dropped by a user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
const dropData = { dataTransfer: new DataTransfer() }
|
||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||
value: [file],
|
||||
@@ -103,7 +103,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it("doesn't redirect if already on /workspace", async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.db' }
|
||||
const file = new File([], 'test.db')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock db loading
|
||||
@@ -136,7 +136,7 @@ describe('DbUploader.vue', () => {
|
||||
|
||||
it('shows parse dialog if gets csv file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
@@ -153,24 +153,92 @@ describe('DbUploader.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
const CsvImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'preview').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
||||
expect(CsvImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('deletes temporary db if CSV import is canceled', async () => {
|
||||
it('shows parse dialog if gets json file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.json', { type: 'application/json' })
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const JsonImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(JsonImport, 'reset')
|
||||
sinon.stub(JsonImport, 'preview').resolves()
|
||||
sinon.stub(JsonImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(JsonImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('shows parse dialog if gets ndjson file', async () => {
|
||||
// mock getting a file from user
|
||||
const file = new File([], 'test.ndjson')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
const $router = { push: sinon.stub() }
|
||||
const $route = { path: '/workspace' }
|
||||
|
||||
// mount the component
|
||||
const wrapper = mount(DbUploader, {
|
||||
attachTo: place,
|
||||
store,
|
||||
mocks: { $router, $route },
|
||||
propsData: {
|
||||
type: 'illustrated'
|
||||
}
|
||||
})
|
||||
|
||||
const JsonImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(JsonImport, 'reset')
|
||||
sinon.stub(JsonImport, 'preview').resolves()
|
||||
sinon.stub(JsonImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.reset.calledOnce).to.equal(true)
|
||||
await wrapper.vm.animationPromise
|
||||
expect(JsonImport.preview.calledOnce).to.equal(true)
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(JsonImport.open.calledOnce).to.equal(true)
|
||||
wrapper.destroy()
|
||||
})
|
||||
|
||||
it('deletes temporary db if import is canceled', async () => {
|
||||
// mock getting a file from user
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||
|
||||
// mock router
|
||||
@@ -186,9 +254,9 @@ describe('DbUploader.vue', () => {
|
||||
}
|
||||
})
|
||||
|
||||
const CsvImport = wrapper.vm.$refs.addCsv
|
||||
const CsvImport = wrapper.vm.$refs.addCsvJson
|
||||
sinon.stub(CsvImport, 'reset')
|
||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
||||
sinon.stub(CsvImport, 'preview').resolves()
|
||||
sinon.stub(CsvImport, 'open')
|
||||
|
||||
await wrapper.find('.drop-area').trigger('click')
|
||||
|
||||
@@ -40,7 +40,7 @@ describe('Splitpanes.vue', () => {
|
||||
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
|
||||
const wrapper = shallowMount(Splitpanes, {
|
||||
slots: {
|
||||
@@ -70,6 +70,64 @@ describe('Splitpanes.vue', () => {
|
||||
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 () => {
|
||||
const root = document.createElement('div')
|
||||
const place = document.createElement('div')
|
||||
|
||||
@@ -28,7 +28,26 @@ describe('csv.js', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('getResult without fields', () => {
|
||||
it('getResult without fields but with columns', () => {
|
||||
const source = {
|
||||
data: [
|
||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||
[2, 'bar', new Date('2021-07-30T14:10:15.717Z')]
|
||||
],
|
||||
meta: {}
|
||||
}
|
||||
const columns = ['id', 'name', 'date']
|
||||
expect(csv.getResult(source, columns)).to.eql({
|
||||
columns: ['id', 'name', 'date'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar'],
|
||||
date: ['2021-06-30T14:10:24.717Z', '2021-07-30T14:10:15.717Z']
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it('getResult without fields and columns', () => {
|
||||
const source = {
|
||||
data: [
|
||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||
@@ -116,6 +135,33 @@ describe('csv.js', () => {
|
||||
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', () => {
|
||||
const resultSet = {
|
||||
columns: ['id', 'name'],
|
||||
|
||||
@@ -293,7 +293,7 @@ describe('SQLite extensions', function () {
|
||||
|
||||
it('supports decimal', async function () {
|
||||
const actual = await db.execute(`
|
||||
select
|
||||
SELECT
|
||||
decimal_add(decimal('0.1'), decimal('0.2')) "add",
|
||||
decimal_sub(0.2, 0.1) sub,
|
||||
decimal_mul(power(2, 69), 2) mul,
|
||||
@@ -430,4 +430,110 @@ 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]
|
||||
})
|
||||
})
|
||||
|
||||
it('supports simple Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_inline', 'return {"arg"}, {"rv"}, "simple", function(arg) return arg + 1 end'),
|
||||
('lua_full', '
|
||||
local input = {"arg"}
|
||||
local output = {"rv"}
|
||||
|
||||
local function func(x)
|
||||
return math.sin(math.pi) + x
|
||||
end
|
||||
|
||||
return input, output, "simple", func
|
||||
');
|
||||
|
||||
SELECT lua_inline(1), lua_full(1) - 1 < 0.000001;
|
||||
`)
|
||||
expect(actual.values).to.eql({ 'lua_inline(1)': [2], 'lua_full(1) - 1 < 0.000001': [1] })
|
||||
})
|
||||
|
||||
it('supports aggregate Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_sum', '
|
||||
local inputs = {"item"}
|
||||
local outputs = {"sum"}
|
||||
|
||||
local function func(item)
|
||||
if aggregate_now(item) then
|
||||
return item
|
||||
end
|
||||
|
||||
local sum = 0
|
||||
while true do
|
||||
if aggregate_now(item) then
|
||||
break
|
||||
end
|
||||
sum = sum + item
|
||||
item = coroutine.yield()
|
||||
end
|
||||
|
||||
return sum
|
||||
end
|
||||
|
||||
return inputs, outputs, "aggregate", func
|
||||
');
|
||||
|
||||
SELECT SUM(value), lua_sum(value) FROM generate_series(1, 10);
|
||||
`)
|
||||
expect(actual.values).to.eql({ 'SUM(value)': [55], 'lua_sum(value)': [55] })
|
||||
})
|
||||
|
||||
it('supports table-valued Lua functions', async function () {
|
||||
const actual = await db.execute(`
|
||||
INSERT INTO
|
||||
luafunctions(name, src)
|
||||
VALUES
|
||||
('lua_match', '
|
||||
local inputs = {"pattern", "s"}
|
||||
local outputs = {"idx", "elm"}
|
||||
|
||||
local function func(pattern, s)
|
||||
local i = 1
|
||||
for k in s:gmatch(pattern) do
|
||||
coroutine.yield(i, k)
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
return inputs, outputs, "table", func
|
||||
');
|
||||
|
||||
SELECT * FROM lua_match('%w+', 'hello world from Lua');
|
||||
`)
|
||||
expect(actual.values).to.eql({ idx: [1, 2, 3, 4], elm: ['hello', 'world', 'from', 'Lua'] })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -87,14 +87,14 @@ describe('storedInquiries.js', () => {
|
||||
|
||||
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
|
||||
const tab = {
|
||||
initName: 'foo'
|
||||
name: 'foo'
|
||||
}
|
||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
|
||||
})
|
||||
|
||||
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
|
||||
const tab = {
|
||||
initName: null,
|
||||
name: null,
|
||||
tempName: 'Untitled'
|
||||
}
|
||||
expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
|
||||
@@ -102,7 +102,7 @@ describe('storedInquiries.js', () => {
|
||||
|
||||
it('isTabNeedName returns true when the inquiry is predefined', () => {
|
||||
const tab = {
|
||||
initName: 'foo',
|
||||
name: 'foo',
|
||||
isPredefined: true
|
||||
}
|
||||
|
||||
@@ -342,91 +342,4 @@ describe('storedInquiries.js', () => {
|
||||
createdAt: '2020-11-03T14:17:49.524Z'
|
||||
}])
|
||||
})
|
||||
|
||||
it('save adds new inquiry in the storage', () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
initName: null,
|
||||
$refs: {
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const value = storedInquiries.save(tab, 'foo')
|
||||
expect(value.id).to.equal(tab.id)
|
||||
expect(value.name).to.equal('foo')
|
||||
expect(value.query).to.equal(tab.query)
|
||||
expect(value.viewOptions).to.eql(['chart'])
|
||||
expect(value).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||
const inquiries = storedInquiries.getStoredInquiries()
|
||||
expect(JSON.stringify(inquiries)).to.equal(JSON.stringify([value]))
|
||||
})
|
||||
|
||||
it('save updates existing inquiry in the storage', () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
initName: null,
|
||||
$refs: {
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const first = storedInquiries.save(tab, 'foo')
|
||||
|
||||
tab.initName = 'foo'
|
||||
tab.query = 'select * from foo'
|
||||
storedInquiries.save(tab)
|
||||
const inquiries = storedInquiries.getStoredInquiries()
|
||||
const second = inquiries[0]
|
||||
expect(inquiries).has.lengthOf(1)
|
||||
expect(second.id).to.equal(first.id)
|
||||
expect(second.name).to.equal(first.name)
|
||||
expect(second.query).to.equal(tab.query)
|
||||
expect(second.viewOptions).to.eql(['chart'])
|
||||
expect(new Date(second.createdAt).getTime()).to.equal(first.createdAt.getTime())
|
||||
})
|
||||
|
||||
it("save adds a new inquiry with new id if it's based on predefined inquiry", () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
initName: 'foo predefined',
|
||||
$refs: {
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
},
|
||||
isPredefined: true
|
||||
}
|
||||
storedInquiries.save(tab, 'foo')
|
||||
|
||||
const inquiries = storedInquiries.getStoredInquiries()
|
||||
expect(inquiries).has.lengthOf(1)
|
||||
expect(inquiries[0]).to.have.property('id').which.not.equal(tab.id)
|
||||
expect(inquiries[0].name).to.equal('foo')
|
||||
expect(inquiries[0].query).to.equal(tab.query)
|
||||
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
||||
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
||||
})
|
||||
})
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
@@ -7,9 +7,9 @@ describe('clipboardIo.js', async () => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('copyCsv', async () => {
|
||||
it('copyText', async () => {
|
||||
sinon.stub(navigator.clipboard, 'writeText').resolves(true)
|
||||
await cIo.copyCsv('id\tname\r\n1\t2')
|
||||
await cIo.copyText('id\tname\r\n1\t2')
|
||||
expect(navigator.clipboard.writeText.calledOnceWith('id\tname\r\n1\t2'))
|
||||
})
|
||||
|
||||
|
||||
@@ -106,10 +106,65 @@ describe('fileIo.js', () => {
|
||||
await expect(fIo.readAsArrayBuffer(blob)).to.be.rejectedWith('Problem parsing input file.')
|
||||
})
|
||||
|
||||
it('isJSON', () => {
|
||||
let file = { type: 'application/json' }
|
||||
expect(fIo.isJSON(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isJSON(file)).to.equal(false)
|
||||
})
|
||||
|
||||
it('isNDJSON', () => {
|
||||
let file = { type: 'application/json', name: 'test.json' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: 'application/x-sqlite3', name: 'test.sqlite3' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.db' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.sqlite3' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(true)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isNDJSON(file)).to.equal(false)
|
||||
})
|
||||
|
||||
it('isDatabase', () => {
|
||||
let file = { type: 'application/vnd.sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
file = { type: 'application/json' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'application/x-sqlite3' }
|
||||
expect(fIo.isDatabase(file)).to.equal(true)
|
||||
|
||||
@@ -125,6 +180,9 @@ describe('fileIo.js', () => {
|
||||
file = { type: '', name: 'test.csv' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: '', name: 'test.ndjson' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
|
||||
file = { type: 'text', name: 'test.db' }
|
||||
expect(fIo.isDatabase(file)).to.equal(false)
|
||||
})
|
||||
|
||||
@@ -29,12 +29,12 @@ describe('time.js', () => {
|
||||
})
|
||||
|
||||
it('sleep resolves after n ms', async () => {
|
||||
let before = Date.now()
|
||||
let before = performance.now()
|
||||
await time.sleep(10)
|
||||
expect(Date.now() - before).to.be.least(10)
|
||||
expect(performance.now() - before).to.be.least(10)
|
||||
|
||||
before = Date.now()
|
||||
before = performance.now()
|
||||
await time.sleep(30)
|
||||
expect(Date.now() - before).to.be.least(30)
|
||||
expect(performance.now() - before).to.be.least(30)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { expect } from 'chai'
|
||||
import actions from '@/store/actions'
|
||||
import sinon from 'sinon'
|
||||
|
||||
const { addTab } = actions
|
||||
const {
|
||||
addTab,
|
||||
addInquiry,
|
||||
deleteInquiries,
|
||||
renameInquiry,
|
||||
saveInquiry
|
||||
} = actions
|
||||
|
||||
describe('actions', () => {
|
||||
it('addTab adds new blank tab', async () => {
|
||||
@@ -11,7 +18,7 @@ describe('actions', () => {
|
||||
}
|
||||
|
||||
let id = await addTab({ state })
|
||||
expect(state.tabs[0]).to.eql({
|
||||
expect(state.tabs[0]).to.include({
|
||||
id: id,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
@@ -22,7 +29,7 @@ describe('actions', () => {
|
||||
expect(state.untitledLastIndex).to.equal(1)
|
||||
|
||||
id = await addTab({ state })
|
||||
expect(state.tabs[1]).to.eql({
|
||||
expect(state.tabs[1]).to.include({
|
||||
id: id,
|
||||
name: null,
|
||||
tempName: 'Untitled 1',
|
||||
@@ -41,14 +48,13 @@ describe('actions', () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'test',
|
||||
tempName: null,
|
||||
query: 'SELECT * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: {},
|
||||
viewOptions: 'an object with view options',
|
||||
isSaved: true
|
||||
}
|
||||
await addTab({ state }, tab)
|
||||
expect(state.tabs[0]).to.eql(tab)
|
||||
expect(state.tabs[0]).to.include(tab)
|
||||
expect(state.untitledLastIndex).to.equal(0)
|
||||
})
|
||||
|
||||
@@ -82,4 +88,156 @@ describe('actions', () => {
|
||||
expect(state.tabs).to.have.lengthOf(2)
|
||||
expect(state.untitledLastIndex).to.equal(0)
|
||||
})
|
||||
|
||||
it('addInquiry', async () => {
|
||||
const state = {
|
||||
inquiries: [1, 2, 3]
|
||||
}
|
||||
|
||||
await addInquiry({ state }, 4)
|
||||
expect(state.inquiries).to.eql([1, 2, 3, 4])
|
||||
})
|
||||
|
||||
it('deleteInquiries', async () => {
|
||||
const state = {
|
||||
inquiries: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
tabs: [{ id: 3 }, { id: 2 }]
|
||||
}
|
||||
const commit = sinon.spy()
|
||||
|
||||
await deleteInquiries({ state, commit }, new Set().add(2))
|
||||
expect(state.inquiries).to.eql([{ id: 1 }, { id: 3 }])
|
||||
expect(commit.calledWith('deleteTab', { id: 2 })).to.equal(true)
|
||||
})
|
||||
|
||||
it('renameInquiry', async () => {
|
||||
const state = {
|
||||
inquiries: [
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 2, name: 'bar' },
|
||||
{ id: 3, name: 'baz' }
|
||||
],
|
||||
tabs: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
|
||||
}
|
||||
const commit = sinon.spy()
|
||||
|
||||
await renameInquiry({ state, commit }, { inquiryId: 2, newName: 'new name' })
|
||||
expect(state.inquiries).to.eql([
|
||||
{ id: 1, name: 'foo' },
|
||||
{ id: 2, name: 'new name' },
|
||||
{ id: 3, name: 'baz' }
|
||||
])
|
||||
expect(commit.calledWith('updateTab', {
|
||||
tab: { id: 2, name: 'bar' },
|
||||
newValues: {
|
||||
name: 'new name'
|
||||
}
|
||||
})).to.equal(true)
|
||||
})
|
||||
|
||||
it('saveInquiry adds new inquiry in the storage', async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: null,
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
const state = {
|
||||
inquiries: [],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
const value = await saveInquiry({ state }, {
|
||||
inquiryTab: tab,
|
||||
newName: 'foo'
|
||||
})
|
||||
expect(value.id).to.equal(tab.id)
|
||||
expect(value.name).to.equal('foo')
|
||||
expect(value.query).to.equal(tab.query)
|
||||
expect(value.viewOptions).to.eql(['chart'])
|
||||
expect(value).to.have.property('createdAt').which.within(now, nowPlusMinute)
|
||||
expect(state.inquiries).to.eql([value])
|
||||
})
|
||||
|
||||
it('save updates existing inquiry in the storage', async () => {
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: null,
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const state = {
|
||||
inquiries: [],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
const first = await saveInquiry({ state }, {
|
||||
inquiryTab: tab,
|
||||
newName: 'foo'
|
||||
})
|
||||
|
||||
tab.name = 'foo'
|
||||
tab.query = 'select * from foo'
|
||||
await saveInquiry({ state }, { inquiryTab: tab })
|
||||
const inquiries = state.inquiries
|
||||
const second = inquiries[0]
|
||||
expect(inquiries).has.lengthOf(1)
|
||||
expect(second.id).to.equal(first.id)
|
||||
expect(second.name).to.equal(first.name)
|
||||
expect(second.query).to.equal(tab.query)
|
||||
expect(second.viewOptions).to.eql(['chart'])
|
||||
expect(new Date(second.createdAt).getTime()).to.equal(first.createdAt.getTime())
|
||||
})
|
||||
|
||||
it("save adds a new inquiry with new id if it's based on predefined inquiry", async () => {
|
||||
const now = new Date()
|
||||
const nowPlusMinute = new Date(now.getTime() + 60 * 1000)
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'select * from foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
name: 'foo predefined',
|
||||
dataView: {
|
||||
getOptionsForSave () {
|
||||
return ['chart']
|
||||
}
|
||||
},
|
||||
isPredefined: true
|
||||
}
|
||||
|
||||
const state = {
|
||||
inquiries: [],
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
await saveInquiry({ state }, {
|
||||
inquiryTab: tab,
|
||||
newName: 'foo'
|
||||
})
|
||||
|
||||
const inquiries = state.inquiries
|
||||
expect(inquiries).has.lengthOf(1)
|
||||
expect(inquiries[0]).to.have.property('id').which.not.equal(tab.id)
|
||||
expect(inquiries[0].name).to.equal('foo')
|
||||
expect(inquiries[0].query).to.equal(tab.query)
|
||||
expect(inquiries[0].viewOptions).to.eql(['chart'])
|
||||
expect(new Date(inquiries[0].createdAt)).to.be.within(now, nowPlusMinute)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,9 +5,11 @@ const {
|
||||
updateTab,
|
||||
deleteTab,
|
||||
setCurrentTabId,
|
||||
setCurrentTab,
|
||||
updatePredefinedInquiries,
|
||||
setDb
|
||||
setDb,
|
||||
setLoadingPredefinedInquiries,
|
||||
setPredefinedInquiriesLoaded,
|
||||
setInquiries
|
||||
} = mutations
|
||||
|
||||
describe('mutations', () => {
|
||||
@@ -35,8 +37,7 @@ describe('mutations', () => {
|
||||
isPredefined: false
|
||||
}
|
||||
|
||||
const newTab = {
|
||||
index: 0,
|
||||
const newValues = {
|
||||
id: 1,
|
||||
name: 'new test',
|
||||
query: 'SELECT * from bar',
|
||||
@@ -49,7 +50,7 @@ describe('mutations', () => {
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
updateTab(state, newTab)
|
||||
updateTab(state, { tab, newValues })
|
||||
expect(state.tabs[0]).to.eql({
|
||||
id: 1,
|
||||
name: 'new test',
|
||||
@@ -73,8 +74,7 @@ describe('mutations', () => {
|
||||
isPredefined: true
|
||||
}
|
||||
|
||||
const newTab = {
|
||||
index: 0,
|
||||
const newValues = {
|
||||
id: 2,
|
||||
name: 'new test',
|
||||
query: 'SELECT * from bar',
|
||||
@@ -88,7 +88,7 @@ describe('mutations', () => {
|
||||
currentTabId: 1
|
||||
}
|
||||
|
||||
updateTab(state, newTab)
|
||||
updateTab(state, { tab, newValues })
|
||||
expect(state.tabs).to.have.lengthOf(1)
|
||||
expect(state.currentTabId).to.equal(2)
|
||||
expect(state.tabs[0].id).to.equal(2)
|
||||
@@ -109,8 +109,7 @@ describe('mutations', () => {
|
||||
isSaved: false
|
||||
}
|
||||
|
||||
const newTab = {
|
||||
index: 0,
|
||||
const newValues = {
|
||||
id: 1,
|
||||
name: 'new test'
|
||||
}
|
||||
@@ -119,7 +118,7 @@ describe('mutations', () => {
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
updateTab(state, newTab)
|
||||
updateTab(state, { tab, newValues })
|
||||
expect(state.tabs).to.have.lengthOf(1)
|
||||
expect(state.tabs[0].id).to.equal(1)
|
||||
expect(state.tabs[0].name).to.equal('new test')
|
||||
@@ -139,8 +138,7 @@ describe('mutations', () => {
|
||||
isPredefined: true
|
||||
}
|
||||
|
||||
const newTab = {
|
||||
index: 0,
|
||||
const newValues = {
|
||||
isSaved: false
|
||||
}
|
||||
|
||||
@@ -148,7 +146,7 @@ describe('mutations', () => {
|
||||
tabs: [tab]
|
||||
}
|
||||
|
||||
updateTab(state, newTab)
|
||||
updateTab(state, { tab, newValues })
|
||||
expect(state.tabs).to.have.lengthOf(1)
|
||||
expect(state.tabs[0].id).to.equal(1)
|
||||
expect(state.tabs[0].name).to.equal('test')
|
||||
@@ -179,13 +177,15 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
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[0].id).to.equal(2)
|
||||
expect(state.currentTabId).to.equal(2)
|
||||
expect(state.currentTab).to.eql(tab2)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, last', () => {
|
||||
@@ -211,13 +211,15 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
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[0].id).to.equal(1)
|
||||
expect(state.currentTabId).to.equal(1)
|
||||
expect(state.currentTab).to.eql(tab1)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, in the middle', () => {
|
||||
@@ -253,14 +255,16 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
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[0].id).to.equal(1)
|
||||
expect(state.tabs[1].id).to.equal(3)
|
||||
expect(state.currentTabId).to.equal(3)
|
||||
expect(state.currentTab).to.eql(tab3)
|
||||
})
|
||||
|
||||
it('deleteTab - opened, single', () => {
|
||||
@@ -276,48 +280,19 @@ describe('mutations', () => {
|
||||
|
||||
const state = {
|
||||
tabs: [tab1],
|
||||
currentTabId: 1
|
||||
currentTabId: 1,
|
||||
currentTab: tab1
|
||||
}
|
||||
|
||||
deleteTab(state, 0)
|
||||
deleteTab(state, tab1)
|
||||
expect(state.tabs).to.have.lengthOf(0)
|
||||
expect(state.currentTabId).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)
|
||||
expect(state.currentTab).to.equal(null)
|
||||
})
|
||||
|
||||
it('setCurrentTabId', () => {
|
||||
const state = {
|
||||
tabs: [{ id: 1 }, { id: 2 }],
|
||||
currentTabId: 1
|
||||
}
|
||||
|
||||
@@ -325,15 +300,6 @@ describe('mutations', () => {
|
||||
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', () => {
|
||||
const inquiry = {
|
||||
id: 1,
|
||||
@@ -377,4 +343,31 @@ describe('mutations', () => {
|
||||
updatePredefinedInquiries(state, 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)
|
||||
})
|
||||
|
||||
it('setInquiries', () => {
|
||||
const state = {
|
||||
inquiries: []
|
||||
}
|
||||
|
||||
setInquiries(state, [1, 2, 3])
|
||||
expect(state.inquiries).to.eql([1, 2, 3])
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import Vuex from 'vuex'
|
||||
import Inquiries from '@/views/Main/Inquiries'
|
||||
import storedInquiries from '@/lib/storedInquiries'
|
||||
import mutations from '@/store/mutations'
|
||||
import actions from '@/store/actions'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
|
||||
describe('Inquiries.vue', () => {
|
||||
@@ -14,14 +15,16 @@ describe('Inquiries.vue', () => {
|
||||
|
||||
it('Shows start-guide message if there are no saved and predefined inquiries', () => {
|
||||
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
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, actions })
|
||||
const wrapper = shallowMount(Inquiries, { store })
|
||||
|
||||
expect(wrapper.find('#start-guide').exists()).to.equal(true)
|
||||
@@ -38,7 +41,10 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -55,15 +61,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-12-04T18:53:56.299Z'
|
||||
}
|
||||
])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const wrapper = shallowMount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('#start-guide').exists()).to.equal(false)
|
||||
@@ -92,7 +95,10 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -109,12 +115,10 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-12-04T18:53:56.299Z'
|
||||
}
|
||||
])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await wrapper.find('#toolbar-search input').setValue('OO')
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -136,7 +140,10 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -153,12 +160,10 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-12-04T18:53:56.299Z'
|
||||
}
|
||||
])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await wrapper.find('#toolbar-search input').setValue('baz')
|
||||
await wrapper.vm.$nextTick()
|
||||
@@ -179,7 +184,10 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -188,15 +196,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const wrapper = shallowMount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
@@ -206,7 +211,11 @@ describe('Inquiries.vue', () => {
|
||||
|
||||
it('Exports one inquiry', async () => {
|
||||
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
sinon.stub(storedInquiries, 'serialiseInquiries').returns('I am a serialized inquiry')
|
||||
sinon.stub(fu, 'exportToFile')
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -215,17 +224,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'serialiseInquiries').returns('I am a serialized inquiry')
|
||||
sinon.stub(fu, 'exportToFile')
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.findComponent({ name: 'ExportIcon' }).find('svg').trigger('click')
|
||||
expect(fu.exportToFile.calledOnceWith('I am a serialized inquiry', 'foo.json')).to.equals(true)
|
||||
@@ -241,7 +245,6 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const newInquiry = {
|
||||
id: 2,
|
||||
@@ -253,13 +256,13 @@ describe('Inquiries.vue', () => {
|
||||
}
|
||||
sinon.stub(storedInquiries, 'duplicateInquiry').returns(newInquiry)
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStorage]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.findComponent({ name: 'CopyIcon' }).find('svg').trigger('click')
|
||||
|
||||
@@ -269,9 +272,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo copy')
|
||||
expect(rows.at(1).findAll('td').at(1).text()).to.contains('3 December 2020 20:57')
|
||||
expect(
|
||||
storedInquiries.updateStorage.calledOnceWith(sinon.match([inquiryInStorage, newInquiry]))
|
||||
).to.equals(true)
|
||||
expect(state.inquiries).to.eql([inquiryInStorage, newInquiry])
|
||||
})
|
||||
|
||||
it('The copy of the inquiry is not selected if all inquiries were selected before duplication',
|
||||
@@ -285,8 +286,6 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const newInquiry = {
|
||||
id: 2,
|
||||
name: 'foo copy',
|
||||
@@ -297,13 +296,13 @@ describe('Inquiries.vue', () => {
|
||||
}
|
||||
sinon.stub(storedInquiries, 'duplicateInquiry').returns(newInquiry)
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStorage]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container')
|
||||
.trigger('click')
|
||||
@@ -324,10 +323,11 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
tabs: [],
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStorage]
|
||||
}
|
||||
const actions = { addTab: sinon.stub().resolves(1) }
|
||||
sinon.spy(mutations, 'setCurrentTabId')
|
||||
@@ -339,7 +339,6 @@ describe('Inquiries.vue', () => {
|
||||
mocks: { $router }
|
||||
})
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.find('tbody tr').trigger('click')
|
||||
|
||||
@@ -361,23 +360,26 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findComponent({ name: 'RenameIcon' }).exists()).to.equals(false)
|
||||
})
|
||||
|
||||
it('Renames an inquiry', async () => {
|
||||
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const state = {
|
||||
tabs: [{ id: 1, name: 'foo' }],
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -386,17 +388,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const state = {
|
||||
tabs: [{ id: 1, name: 'foo' }],
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// click Rename icon in the grid
|
||||
@@ -416,19 +413,20 @@ describe('Inquiries.vue', () => {
|
||||
.findAll('.dialog-buttons-container button').wrappers
|
||||
.find(button => button.text() === 'Rename')
|
||||
.trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// check that the name in the grid is changed
|
||||
expect(wrapper.find('tbody tr td').text()).to.equals('bar')
|
||||
|
||||
// check that storage is updated
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(sinon.match([{
|
||||
expect(state.inquiries).to.eql([{
|
||||
id: 1,
|
||||
name: 'bar',
|
||||
query: '',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}]))).to.equals(true)
|
||||
}])
|
||||
|
||||
// check that coresponding tab also changed the name
|
||||
expect(state.tabs[0].name).to.equals('bar')
|
||||
@@ -439,7 +437,11 @@ describe('Inquiries.vue', () => {
|
||||
|
||||
it('Shows an error if try to rename to empty string', async () => {
|
||||
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const state = {
|
||||
tabs: [{ id: 1, name: 'foo' }],
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -448,17 +450,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const state = {
|
||||
tabs: [{ id: 1, name: 'foo' }],
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// click Rename icon in the grid
|
||||
@@ -489,7 +486,6 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const importedInquiry = {
|
||||
id: 2,
|
||||
@@ -501,13 +497,13 @@ describe('Inquiries.vue', () => {
|
||||
}
|
||||
sinon.stub(storedInquiries, 'importInquiries').resolves([importedInquiry])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStorage]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = shallowMount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// click Import
|
||||
@@ -517,9 +513,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('bar')
|
||||
expect(rows.at(1).findAll('td').at(1).text()).to.equals('3 December 2020 20:57')
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(
|
||||
sinon.match([inquiryInStorage, importedInquiry])
|
||||
)).to.equals(true)
|
||||
expect(state.inquiries).to.eql([inquiryInStorage, importedInquiry])
|
||||
})
|
||||
|
||||
it('Imported inquiries are not selected if master check box was checked', async () => {
|
||||
@@ -532,7 +526,6 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
const importedInquiry = {
|
||||
id: 2,
|
||||
@@ -544,13 +537,13 @@ describe('Inquiries.vue', () => {
|
||||
}
|
||||
sinon.stub(storedInquiries, 'importInquiries').resolves([importedInquiry])
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStorage]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// click on master checkbox
|
||||
@@ -577,16 +570,15 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: []
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.findComponent({ name: 'DeleteIcon' }).exists()).to.equals(false)
|
||||
})
|
||||
@@ -609,18 +601,17 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
|
||||
const state = {
|
||||
tabs: [{ id: 1 }, { id: 2 }],
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
// click Delete icon in the first row of the grid
|
||||
await wrapper.findComponent({ name: 'DeleteIcon' }).find('svg').trigger('click')
|
||||
@@ -646,7 +637,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(state.tabs[0].id).to.equals(2)
|
||||
|
||||
// check that storage is updated
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(sinon.match([bar]))).to.equals(true)
|
||||
expect(state.inquiries).to.eql([bar])
|
||||
|
||||
// check that delete dialog is closed
|
||||
expect(wrapper.find('[data-modal="delete"]').exists()).to.equal(false)
|
||||
@@ -663,7 +654,10 @@ describe('Inquiries.vue', () => {
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
@@ -672,16 +666,12 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-11-03T19:57:56.299Z'
|
||||
}
|
||||
])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.find('#toolbar-btns-export').isVisible()).to.equal(false)
|
||||
@@ -723,26 +713,25 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStore, {
|
||||
|
||||
sinon.stub(storedInquiries, 'serialiseInquiries').returns('I am a serialized inquiries')
|
||||
sinon.stub(fu, 'exportToFile')
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStore, {
|
||||
id: 2,
|
||||
name: 'bar',
|
||||
query: '',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}])
|
||||
|
||||
sinon.stub(storedInquiries, 'serialiseInquiries').returns('I am a serialized inquiries')
|
||||
sinon.stub(fu, 'exportToFile')
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
}]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
@@ -780,19 +769,18 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStore])
|
||||
|
||||
sinon.stub(storedInquiries, 'serialiseInquiries').returns('I am a serialized inquiries')
|
||||
sinon.stub(fu, 'exportToFile')
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [inquiryInStore]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container')
|
||||
@@ -843,19 +831,18 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar, baz])
|
||||
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
|
||||
const state = {
|
||||
tabs: [{ id: 1 }, { id: 2 }, { id: 0 }, { id: 3 }],
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar, baz]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
@@ -890,7 +877,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(state.tabs[1].id).to.equals(3)
|
||||
|
||||
// check that storage is updated
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(sinon.match([baz]))).to.equals(true)
|
||||
expect(state.inquiries).to.eql([baz])
|
||||
|
||||
// check that delete dialog is closed
|
||||
expect(wrapper.find('[data-modal="delete"]').exists()).to.equal(false)
|
||||
@@ -922,18 +909,17 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
|
||||
const state = {
|
||||
tabs: [],
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
@@ -965,7 +951,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(wrapper.findAll('tbody tr').at(1).find('td').text()).to.equals('bar')
|
||||
|
||||
// check that storage is updated
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(sinon.match([bar]))).to.equals(true)
|
||||
expect(state.inquiries).to.eql([bar])
|
||||
|
||||
// check that delete dialog is closed
|
||||
expect(wrapper.find('[data-modal="delete"]').exists()).to.equal(false)
|
||||
@@ -997,18 +983,17 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
|
||||
sinon.stub(storedInquiries, 'updateStorage')
|
||||
|
||||
const state = {
|
||||
tabs: [],
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container')
|
||||
@@ -1036,7 +1021,7 @@ describe('Inquiries.vue', () => {
|
||||
expect(wrapper.findAll('tbody tr').at(0).find('td').text()).to.contains('hello_world')
|
||||
|
||||
// check that storage is updated
|
||||
expect(storedInquiries.updateStorage.calledOnceWith(sinon.match([]))).to.equals(true)
|
||||
expect(state.inquiries).to.eql([])
|
||||
|
||||
// check that delete dialog is closed
|
||||
expect(wrapper.find('[data-modal="delete"]').exists()).to.equal(false)
|
||||
@@ -1060,16 +1045,15 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const mainCheckBox = wrapper.findComponent({ ref: 'mainCheckBox' })
|
||||
@@ -1119,16 +1103,15 @@ describe('Inquiries.vue', () => {
|
||||
viewOptions: [],
|
||||
createdAt: '2020-03-08T19:57:56.299Z'
|
||||
}
|
||||
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
|
||||
|
||||
const state = {
|
||||
predefinedInquiries: []
|
||||
predefinedInquiries: [],
|
||||
inquiries: [foo, bar]
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
|
||||
const wrapper = mount(Inquiries, { store })
|
||||
await storedInquiries.readPredefinedInquiries.returnValues[0]
|
||||
await storedInquiries.getStoredInquiries.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
const mainCheckBox = wrapper.findComponent({ ref: 'mainCheckBox' })
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('MainMenu.vue', () => {
|
||||
it('Save is not visible if there is no tabs', () => {
|
||||
const state = {
|
||||
currentTab: null,
|
||||
tabs: [{}],
|
||||
tabs: [],
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
@@ -62,13 +62,15 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Save is disabled if current tab.isSaved is true', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
@@ -83,17 +85,19 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
|
||||
|
||||
await vm.$set(state.tabs[0], 'isSaved', true)
|
||||
await vm.$nextTick()
|
||||
expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
|
||||
})
|
||||
|
||||
it('Creates a tab', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const newInquiryId = 1
|
||||
@@ -121,13 +125,14 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Creates a tab and redirects to workspace', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
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"',
|
||||
async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
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"',
|
||||
async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
@@ -245,13 +252,14 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Ctrl B calls createNewInquiry', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
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',
|
||||
async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const store = new Vuex.Store({ state })
|
||||
@@ -325,28 +334,33 @@ describe('MainMenu.vue', () => {
|
||||
|
||||
it('Saves the inquiry when no need the new name',
|
||||
async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: 'foo', isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
sinon.stub(storedInquiries, 'save').returns({
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
})
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(false)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
store,
|
||||
@@ -359,18 +373,23 @@ describe('MainMenu.vue', () => {
|
||||
// check that the dialog is closed
|
||||
expect(wrapper.find('[data-modal="save"]').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via storedInquiries.save (newName='')
|
||||
expect(storedInquiries.save.calledOnceWith(state.currentTab, '')).to.equal(true)
|
||||
// check that the inquiry was saved via saveInquiry (newName='')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab, newName: ''
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||
index: 0,
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
}
|
||||
}))).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on $root
|
||||
@@ -378,28 +397,34 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Shows en error when the new name is needed but not specifyied', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
sinon.stub(storedInquiries, 'save').returns({
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
})
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
store,
|
||||
@@ -424,28 +449,34 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Saves the inquiry with a new name', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
sinon.stub(storedInquiries, 'save').returns({
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
})
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
store,
|
||||
@@ -467,21 +498,29 @@ describe('MainMenu.vue', () => {
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// check that the dialog is closed
|
||||
expect(wrapper.find('[data-modal="save"]').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via storedInquiries.save (newName='foo')
|
||||
expect(storedInquiries.save.calledOnceWith(state.currentTab, 'foo')).to.equal(true)
|
||||
// check that the inquiry was saved via saveInquiry (newName='foo')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab,
|
||||
newName: 'foo'
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||
index: 0,
|
||||
tab: tab,
|
||||
newValues: {
|
||||
name: 'foo',
|
||||
id: 1,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
}
|
||||
}))).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on $root
|
||||
@@ -489,11 +528,11 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Saves a predefined inquiry with a new name', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: 'foo',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0,
|
||||
isPredefined: true,
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
@@ -503,24 +542,29 @@ describe('MainMenu.vue', () => {
|
||||
]
|
||||
},
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
},
|
||||
tabs: [{ id: 1, name: 'foo', isSaved: false, isPredefined: true }],
|
||||
viewOptions: [],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
sinon.stub(storedInquiries, 'save').returns({
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'bar',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: []
|
||||
})
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
store,
|
||||
@@ -545,21 +589,29 @@ describe('MainMenu.vue', () => {
|
||||
.find(button => button.text() === 'Save')
|
||||
.trigger('click')
|
||||
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// check that the dialog is closed
|
||||
expect(wrapper.find('[data-modal="save"]').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was saved via storedInquiries.save (newName='bar')
|
||||
expect(storedInquiries.save.calledOnceWith(state.currentTab, 'bar')).to.equal(true)
|
||||
// check that the inquiry was saved via saveInquiry (newName='bar')
|
||||
expect(actions.saveInquiry.calledOnce).to.equal(true)
|
||||
expect(actions.saveInquiry.args[0][1]).to.eql({
|
||||
inquiryTab: state.currentTab,
|
||||
newName: 'bar'
|
||||
})
|
||||
|
||||
// check that the tab was updated
|
||||
expect(mutations.updateTab.calledOnceWith(state, sinon.match({
|
||||
index: 0,
|
||||
tab,
|
||||
newValues: {
|
||||
name: 'bar',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
viewType: 'chart',
|
||||
viewOptions: [],
|
||||
isSaved: true
|
||||
}
|
||||
}))).to.equal(true)
|
||||
|
||||
// check that 'inquirySaved' event was triggered on $root
|
||||
@@ -580,27 +632,33 @@ describe('MainMenu.vue', () => {
|
||||
})
|
||||
|
||||
it('Cancel saving', async () => {
|
||||
const state = {
|
||||
currentTab: {
|
||||
const tab = {
|
||||
id: 1,
|
||||
name: null,
|
||||
tempName: 'Untitled',
|
||||
query: 'SELECT * FROM foo',
|
||||
execute: sinon.stub(),
|
||||
tabIndex: 0
|
||||
},
|
||||
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
|
||||
isSaved: false
|
||||
}
|
||||
const state = {
|
||||
currentTab: tab,
|
||||
tabs: [tab],
|
||||
db: {}
|
||||
}
|
||||
const mutations = {
|
||||
updateTab: sinon.stub()
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
sinon.stub(storedInquiries, 'save').returns({
|
||||
const actions = {
|
||||
saveInquiry: sinon.stub().returns({
|
||||
name: 'bar',
|
||||
id: 2,
|
||||
query: 'SELECT * FROM foo',
|
||||
chart: []
|
||||
})
|
||||
}
|
||||
const store = new Vuex.Store({ state, mutations, actions })
|
||||
const $route = { path: '/workspace' }
|
||||
sinon.stub(storedInquiries, 'isTabNeedName').returns(true)
|
||||
|
||||
wrapper = mount(MainMenu, {
|
||||
store,
|
||||
@@ -623,7 +681,7 @@ describe('MainMenu.vue', () => {
|
||||
expect(wrapper.find('[data-modal="save"]').exists()).to.equal(false)
|
||||
|
||||
// check that the inquiry was not saved via storedInquiries.save
|
||||
expect(storedInquiries.save.called).to.equal(false)
|
||||
expect(actions.saveInquiry.called).to.equal(false)
|
||||
|
||||
// check that the tab was not updated
|
||||
expect(mutations.updateTab.called).to.equal(false)
|
||||
|
||||
@@ -125,7 +125,7 @@ describe('Schema.vue', () => {
|
||||
})
|
||||
|
||||
it('adds table', async () => {
|
||||
const file = { name: 'test.csv' }
|
||||
const file = new File([], 'test.csv')
|
||||
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
||||
|
||||
sinon.stub(csv, 'parse').resolves({
|
||||
@@ -152,20 +152,20 @@ describe('Schema.vue', () => {
|
||||
|
||||
const store = new Vuex.Store({ state, actions, mutations })
|
||||
const wrapper = mount(Schema, { store, localVue })
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
|
||||
sinon.spy(wrapper.vm, 'addCsv')
|
||||
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
|
||||
sinon.spy(wrapper.vm.$refs.addCsvJson, 'preview')
|
||||
sinon.spy(wrapper.vm, 'addCsvJson')
|
||||
sinon.spy(wrapper.vm.$refs.addCsvJson, 'loadToDb')
|
||||
|
||||
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
|
||||
await wrapper.vm.addCsv.returnValues[0]
|
||||
await wrapper.vm.$refs.addCsvJson.preview.returnValues[0]
|
||||
await wrapper.vm.addCsvJson.returnValues[0]
|
||||
await wrapper.vm.$nextTick()
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
||||
await wrapper.find('#csv-import').trigger('click')
|
||||
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
|
||||
await wrapper.find('#csv-finish').trigger('click')
|
||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||
await wrapper.find('#import-start').trigger('click')
|
||||
await wrapper.vm.$refs.addCsvJson.loadToDb.returnValues[0]
|
||||
await wrapper.find('#import-finish').trigger('click')
|
||||
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||
await state.db.refreshSchema.returnValues[0]
|
||||
|
||||
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
||||
|
||||
@@ -54,6 +54,10 @@ describe('DataView.vue', () => {
|
||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||
sinon.spy(pivot, 'saveAsSvg')
|
||||
|
||||
// Switch to Custom Chart renderer
|
||||
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||
await pivot.$nextTick()
|
||||
|
||||
// Export to svg
|
||||
await svgBtn.trigger('click')
|
||||
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, createWrapper } from '@vue/test-utils'
|
||||
import RunResult from '@/views/Main/Workspace/Tabs/Tab/RunResult'
|
||||
import csv from '@/lib/csv'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('RunResult.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('shows alert when ClipboardItem is not supported', async () => {
|
||||
const ClipboardItem = window.ClipboardItem
|
||||
delete window.ClipboardItem
|
||||
sinon.spy(window, 'alert')
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
expect(
|
||||
window.alert.calledOnceWith(
|
||||
"Your browser doesn't support copying into the clipboard. " +
|
||||
'If you use Firefox you can enable it ' +
|
||||
'by setting dom.events.asyncClipboard.clipboardItem to true.'
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
window.ClipboardItem = ClipboardItem
|
||||
})
|
||||
|
||||
it('copy to clipboard more than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(5000)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||
|
||||
// ... with Building message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('Building CSV...')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||
|
||||
// ... with Ready message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
||||
|
||||
// Click copy
|
||||
await wrapper.find('.dialog-buttons-container button.primary').trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
})
|
||||
|
||||
it('copy to clipboard less than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(500)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
// copyToClipboard is called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||
})
|
||||
|
||||
it('cancel long copy', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(5000)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Click cancel
|
||||
await wrapper.find('.dialog-buttons-container button.secondary').trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
// copyToClipboard is not called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||
})
|
||||
})
|
||||
116
tests/views/Main/Workspace/Tabs/Tab/RunResult/Record.spec.js
Normal file
116
tests/views/Main/Workspace/Tabs/Tab/RunResult/Record.spec.js
Normal file
@@ -0,0 +1,116 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Record from '@/views/Main/Workspace/Tabs/Tab/RunResult/Record'
|
||||
|
||||
describe('Record.vue', () => {
|
||||
it('shows record with selected cell', async () => {
|
||||
const wrapper = mount(Record, {
|
||||
propsData: {
|
||||
dataSet: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar']
|
||||
}
|
||||
},
|
||||
rowIndex: 1,
|
||||
selectedColumnIndex: 1
|
||||
}
|
||||
})
|
||||
|
||||
const rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(0).findAll('th').at(0).text()).to.equals('id')
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('2')
|
||||
expect(rows.at(1).findAll('th').at(0).text()).to.equals('name')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('bar')
|
||||
|
||||
const selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('bar')
|
||||
})
|
||||
|
||||
it('switches to the next or previous row', async () => {
|
||||
const wrapper = mount(Record, {
|
||||
propsData: {
|
||||
dataSet: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2, 3],
|
||||
name: ['foo', 'bar', 'baz']
|
||||
}
|
||||
},
|
||||
rowIndex: 0,
|
||||
selectedColumnIndex: 0
|
||||
}
|
||||
})
|
||||
|
||||
let rows = wrapper.findAll('tbody tr')
|
||||
expect(rows).to.have.lengthOf(2)
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||
let selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('1')
|
||||
|
||||
await wrapper.find('.next').trigger('click')
|
||||
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('2')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('bar')
|
||||
selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('2')
|
||||
|
||||
await wrapper.find('.prev').trigger('click')
|
||||
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||
selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('1')
|
||||
|
||||
await wrapper.find('.last').trigger('click')
|
||||
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('3')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('baz')
|
||||
selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('3')
|
||||
|
||||
await wrapper.find('.first').trigger('click')
|
||||
|
||||
rows = wrapper.findAll('tbody tr')
|
||||
expect(rows.at(0).findAll('td').at(0).text()).to.equals('1')
|
||||
expect(rows.at(1).findAll('td').at(0).text()).to.equals('foo')
|
||||
selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('1')
|
||||
})
|
||||
|
||||
it('removes selection when click on selected cell', async () => {
|
||||
const wrapper = mount(Record, {
|
||||
propsData: {
|
||||
dataSet: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar']
|
||||
}
|
||||
},
|
||||
rowIndex: 1,
|
||||
selectedColumnIndex: 1
|
||||
}
|
||||
})
|
||||
|
||||
const selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
await selectedCell.trigger('click')
|
||||
|
||||
const selectedCellAfterClick = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCellAfterClick.exists()).to.equals(false)
|
||||
})
|
||||
})
|
||||
348
tests/views/Main/Workspace/Tabs/Tab/RunResult/RunResult.spec.js
Normal file
348
tests/views/Main/Workspace/Tabs/Tab/RunResult/RunResult.spec.js
Normal file
@@ -0,0 +1,348 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount, createWrapper } from '@vue/test-utils'
|
||||
import RunResult from '@/views/Main/Workspace/Tabs/Tab/RunResult'
|
||||
import csv from '@/lib/csv'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('RunResult.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('shows alert when ClipboardItem is not supported', async () => {
|
||||
const ClipboardItem = window.ClipboardItem
|
||||
delete window.ClipboardItem
|
||||
sinon.spy(window, 'alert')
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
expect(
|
||||
window.alert.calledOnceWith(
|
||||
"Your browser doesn't support copying into the clipboard. " +
|
||||
'If you use Firefox you can enable it ' +
|
||||
'by setting dom.events.asyncClipboard.clipboardItem to true.'
|
||||
)
|
||||
).to.equal(true)
|
||||
|
||||
window.ClipboardItem = ClipboardItem
|
||||
})
|
||||
|
||||
it('copy to clipboard more than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(5000)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||
|
||||
// ... with Building message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('Building CSV...')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The dialog is shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(true)
|
||||
|
||||
// ... with Ready message...
|
||||
expect(wrapper.find('.dialog-body').text()).to.equal('CSV is ready')
|
||||
|
||||
// Click copy
|
||||
await wrapper.find('.dialog-buttons-container button.primary').trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
})
|
||||
|
||||
it('copy to clipboard less than 1 sec', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(500)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
// copyToClipboard is called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(true)
|
||||
})
|
||||
|
||||
it('cancel long copy', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const clock = sinon.useFakeTimers()
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1],
|
||||
name: ['foo']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
sinon.spy(wrapper.vm, 'copyToClipboard')
|
||||
sinon.stub(csv, 'serialize').callsFake(() => {
|
||||
clock.tick(5000)
|
||||
})
|
||||
|
||||
// Click copy to clipboard
|
||||
const copyBtn = createWrapper(wrapper.findComponent({ name: 'clipboardIcon' }).vm.$parent)
|
||||
await copyBtn.trigger('click')
|
||||
|
||||
// Switch to microtasks (let serialize run)
|
||||
clock.tick(0)
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
// Click cancel
|
||||
await wrapper.find('.dialog-buttons-container button.secondary').trigger('click')
|
||||
|
||||
// The dialog is not shown...
|
||||
expect(wrapper.find('[data-modal="prepareCSVCopy"]').exists()).to.equal(false)
|
||||
// copyToClipboard is not called
|
||||
expect(wrapper.vm.copyToClipboard.calledOnce).to.equal(false)
|
||||
})
|
||||
|
||||
it('shows value of selected cell - result set', async () => {
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Open cell value panel
|
||||
const viewValueBtn = createWrapper(
|
||||
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||
)
|
||||
await viewValueBtn.trigger('click')
|
||||
|
||||
/*
|
||||
Result set:
|
||||
|1 | foo
|
||||
+--+-----
|
||||
|2 | bar
|
||||
*/
|
||||
|
||||
// Click on '1' cell
|
||||
const rows = wrapper.findAll('table tbody tr')
|
||||
await rows.at(0).findAll('td').at(0).trigger('click')
|
||||
|
||||
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||
|
||||
// Go to 'foo' with right arrow key
|
||||
await wrapper.find('table').trigger('keydown.right')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||
|
||||
// Go to 'bar' with down arrow key
|
||||
await wrapper.find('table').trigger('keydown.down')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||
|
||||
// Go to '2' with left arrow key
|
||||
await wrapper.find('table').trigger('keydown.left')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('2')
|
||||
|
||||
// Go to '1' with up arrow key
|
||||
await wrapper.find('table').trigger('keydown.up')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||
|
||||
// Click on 'bar' cell
|
||||
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||
|
||||
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||
|
||||
// Click on 'bar' cell again
|
||||
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||
|
||||
expect(wrapper.find('.value-viewer-container .table-preview').text())
|
||||
.to.equals('No cell selected to view')
|
||||
})
|
||||
|
||||
it('shows value of selected cell - record view', async () => {
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [1, 2],
|
||||
name: ['foo', 'bar']
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Open cell value panel
|
||||
const viewValueBtn = createWrapper(
|
||||
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||
)
|
||||
await viewValueBtn.trigger('click')
|
||||
|
||||
// Go to record view
|
||||
const vierRecordBtn = createWrapper(
|
||||
wrapper.findComponent({ name: 'rowIcon' }).vm.$parent
|
||||
)
|
||||
await vierRecordBtn.trigger('click')
|
||||
|
||||
/*
|
||||
Record 1:
|
||||
|id | 1
|
||||
+-----+-----
|
||||
|name | foo
|
||||
|
||||
Record 2:
|
||||
|id | 2
|
||||
+-----+-----
|
||||
|name | bar
|
||||
|
||||
*/
|
||||
|
||||
// Click '1' is selected by default
|
||||
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||
|
||||
// Go to 'foo' with down arrow key
|
||||
await wrapper.find('table').trigger('keydown.down')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||
|
||||
// Go to next record
|
||||
await wrapper.find('.icon-btn.next').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.value-body').text()).to.equals('bar')
|
||||
|
||||
// Go to '2' with up arrow key
|
||||
await wrapper.find('table').trigger('keydown.up')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('2')
|
||||
|
||||
// Go to prev record
|
||||
await wrapper.find('.icon-btn.prev').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.value-body').text()).to.equals('1')
|
||||
|
||||
// Click on 'foo' cell
|
||||
const rows = wrapper.findAll('table tbody tr')
|
||||
await rows.at(1).find('td').trigger('click')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||
|
||||
// Click on 'foo' cell again
|
||||
await rows.at(1).find('td').trigger('click')
|
||||
expect(wrapper.find('.value-viewer-container .table-preview').text())
|
||||
.to.equals('No cell selected to view')
|
||||
})
|
||||
|
||||
it('keeps selected cell when switch between record and regular view', async () => {
|
||||
const wrapper = mount(RunResult, {
|
||||
propsData: {
|
||||
tab: { id: 1 },
|
||||
result: {
|
||||
columns: ['id', 'name'],
|
||||
values: {
|
||||
id: [...Array(30)].map((x, i) => i),
|
||||
name: [...Array(30)].map((x, i) => `name-${i}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Open cell value panel
|
||||
const viewValueBtn = createWrapper(
|
||||
wrapper.findComponent({ name: 'viewCellValueIcon' }).vm.$parent
|
||||
)
|
||||
await viewValueBtn.trigger('click')
|
||||
|
||||
// Click on 'name-1' cell
|
||||
const rows = wrapper.findAll('table tbody tr')
|
||||
await rows.at(1).findAll('td').at(1).trigger('click')
|
||||
|
||||
expect(wrapper.find('.value-body').text()).to.equals('name-1')
|
||||
|
||||
// Go to record view
|
||||
const vierRecordBtn = createWrapper(
|
||||
wrapper.findComponent({ name: 'rowIcon' }).vm.$parent
|
||||
)
|
||||
await vierRecordBtn.trigger('click')
|
||||
|
||||
// 'name-1' is selected
|
||||
expect(wrapper.find('.value-body').text()).to.equals('name-1')
|
||||
let selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('name-1')
|
||||
|
||||
// Go to last record
|
||||
await wrapper.find('.icon-btn.last').trigger('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
expect(wrapper.find('.value-body').text()).to.equals('name-29')
|
||||
|
||||
// Go to '29' with up arrow key
|
||||
await wrapper.find('table').trigger('keydown.up')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('29')
|
||||
|
||||
// Go to regular view
|
||||
await vierRecordBtn.trigger('click')
|
||||
|
||||
// '29' is selected
|
||||
expect(wrapper.find('.value-body').text()).to.equals('29')
|
||||
selectedCell = wrapper
|
||||
.find('.sqliteviz-table tbody td[aria-selected="true"]')
|
||||
expect(selectedCell.text()).to.equals('29')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { expect } from 'chai'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import ValueViewer from '@/views/Main/Workspace/Tabs/Tab/RunResult/ValueViewer'
|
||||
import sinon from 'sinon'
|
||||
|
||||
describe('ValueViewer.vue', () => {
|
||||
afterEach(() => {
|
||||
sinon.restore()
|
||||
})
|
||||
|
||||
it('shows value in text mode', async () => {
|
||||
const wrapper = mount(ValueViewer, {
|
||||
propsData: {
|
||||
cellValue: 'foo'
|
||||
}
|
||||
})
|
||||
|
||||
expect(wrapper.find('.value-body').text()).to.equals('foo')
|
||||
})
|
||||
|
||||
it('shows error in json mode if the value is not json', async () => {
|
||||
const wrapper = mount(ValueViewer, {
|
||||
propsData: {
|
||||
cellValue: 'foo'
|
||||
}
|
||||
})
|
||||
await wrapper.find('button.json').trigger('click')
|
||||
expect(wrapper.find('.value-body').text()).to.equals('Can\'t parse JSON.')
|
||||
})
|
||||
|
||||
it('copy to clipboard', async () => {
|
||||
sinon.stub(window.navigator.clipboard, 'writeText').resolves()
|
||||
const wrapper = mount(ValueViewer, {
|
||||
propsData: {
|
||||
cellValue: 'foo'
|
||||
}
|
||||
})
|
||||
|
||||
await wrapper.find('button.copy').trigger('click')
|
||||
|
||||
expect(window.navigator.clipboard.writeText.calledOnceWith('foo'))
|
||||
.to.equal(true)
|
||||
})
|
||||
})
|
||||
@@ -31,13 +31,23 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
tab: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewType: 'chart',
|
||||
initViewOptions: [],
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
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
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -60,7 +70,23 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
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)
|
||||
@@ -79,40 +105,51 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
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)
|
||||
})
|
||||
|
||||
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 () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
@@ -124,13 +161,7 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab: state.tabs[0]
|
||||
}
|
||||
})
|
||||
await wrapper.findComponent({ name: 'SqlEditor' }).vm.$emit('input', ' limit 100')
|
||||
@@ -141,7 +172,24 @@ describe('Tab.vue', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
@@ -153,13 +201,7 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab: state.tabs[0]
|
||||
}
|
||||
})
|
||||
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 () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute () { return new Promise(() => {}) }
|
||||
}
|
||||
currentTabId: 1
|
||||
}
|
||||
|
||||
const store = new Vuex.Store({ state, mutations })
|
||||
// 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, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab
|
||||
}
|
||||
})
|
||||
|
||||
wrapper.vm.execute()
|
||||
tab.isGettingResults = true
|
||||
await wrapper.vm.$nextTick()
|
||||
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 () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute: sinon.stub().rejects(new Error('There is no table foo')),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
}
|
||||
currentTabId: 1
|
||||
}
|
||||
|
||||
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
|
||||
const wrapper = mount(Tab, {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab
|
||||
}
|
||||
})
|
||||
|
||||
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-in-progress').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).isVisible()).to.equal(true)
|
||||
@@ -239,11 +302,26 @@ describe('Tab.vue', () => {
|
||||
}
|
||||
// mock store state
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {
|
||||
execute: sinon.stub().resolves(result),
|
||||
refreshSchema: sinon.stub().resolves()
|
||||
currentTabId: 1
|
||||
}
|
||||
|
||||
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 })
|
||||
@@ -253,83 +331,50 @@ describe('Tab.vue', () => {
|
||||
store,
|
||||
stubs: ['chart'],
|
||||
propsData: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab
|
||||
}
|
||||
})
|
||||
|
||||
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-in-progress').exists()).to.equal(false)
|
||||
expect(wrapper.findComponent({ name: 'logs' }).exists()).to.equal(false)
|
||||
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 () => {
|
||||
const state = {
|
||||
currentTabId: 1,
|
||||
db: {}
|
||||
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: {
|
||||
id: 1,
|
||||
initName: 'foo',
|
||||
initQuery: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
|
||||
initViewOptions: [],
|
||||
initViewType: 'chart',
|
||||
tabIndex: 0,
|
||||
isPredefined: false
|
||||
tab
|
||||
}
|
||||
})
|
||||
|
||||
@@ -361,4 +406,119 @@ describe('Tab.vue', () => {
|
||||
expect(wrapper.find('.above .sql-editor-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
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
@@ -125,8 +150,33 @@ describe('Tabs.vue', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
@@ -166,8 +216,33 @@ describe('Tabs.vue', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
@@ -211,8 +286,33 @@ describe('Tabs.vue', () => {
|
||||
// mock store state
|
||||
const state = {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -12,9 +12,11 @@ describe('Workspace.vue', () => {
|
||||
tabs: []
|
||||
}
|
||||
const store = new Vuex.Store({ state, actions, mutations })
|
||||
const $route = { path: '/workspace', query: {} }
|
||||
mount(Workspace, {
|
||||
store,
|
||||
stubs: ['router-link']
|
||||
stubs: ['router-link'],
|
||||
mocks: { $route }
|
||||
})
|
||||
|
||||
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].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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user