mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-06 18:18:53 +08:00
Compare commits
32 Commits
41e0ae7332
...
0.25.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -3,7 +3,7 @@
|
|||||||
# docker build -t sqliteviz/test -f Dockerfile.test .
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
#
|
#
|
||||||
|
|
||||||
FROM node:12
|
FROM node:12.22-buster
|
||||||
|
|
||||||
RUN set -ex; \
|
RUN set -ex; \
|
||||||
apt update; \
|
apt update; \
|
||||||
|
|||||||
@@ -4,11 +4,12 @@
|
|||||||
|
|
||||||
# sqliteviz
|
# 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:
|
With sqliteviz you can:
|
||||||
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
- 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
|
- export result set to CSV file
|
||||||
- manage inquiries and run them against different databases
|
- manage inquiries and run them against different databases
|
||||||
- import/export inquiries from/to a JSON file
|
- import/export inquiries from/to a JSON file
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ SQLite 3rd party extensions included:
|
|||||||
1. [pivot_vtab][5] -- a pivot virtual table
|
1. [pivot_vtab][5] -- a pivot virtual table
|
||||||
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
2. `pearson` correlation coefficient function extension from [sqlean][21]
|
||||||
(which is part of [squib][20])
|
(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
|
To ease the step to have working clone locally, the build is committed into
|
||||||
the repository.
|
the repository.
|
||||||
@@ -103,3 +105,4 @@ described in [this message from SQLite Forum][12]:
|
|||||||
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
|
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js
|
||||||
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
|
||||||
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/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; \
|
RUN set -ex; \
|
||||||
echo 'deb http://deb.debian.org/debian unstable main' \
|
|
||||||
> /etc/apt/sources.list.d/unstable.list; \
|
|
||||||
apt-get update; \
|
apt-get update; \
|
||||||
apt-get install -y -t unstable firefox; \
|
apt-get install -y firefox-esr; \
|
||||||
apt-get install -y chromium
|
apt-get install -y chromium
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|||||||
@@ -69,6 +69,19 @@
|
|||||||
],
|
],
|
||||||
"metadata": {}
|
"metadata": {}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"!du -b lib | head -n 2"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false,
|
||||||
|
"outputHidden": false,
|
||||||
|
"inputHidden": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"cell_type": "code",
|
"cell_type": "code",
|
||||||
"source": [
|
"source": [
|
||||||
@@ -176,7 +189,7 @@
|
|||||||
},
|
},
|
||||||
"language_info": {
|
"language_info": {
|
||||||
"name": "python",
|
"name": "python",
|
||||||
"version": "3.10.7",
|
"version": "3.10.14",
|
||||||
"mimetype": "text/x-python",
|
"mimetype": "text/x-python",
|
||||||
"codemirror_mode": {
|
"codemirror_mode": {
|
||||||
"name": "ipython",
|
"name": "ipython",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ cflags = (
|
|||||||
# Compile-time optimisation
|
# Compile-time optimisation
|
||||||
'-Os', # reduces the code size about in half comparing to -O2
|
'-Os', # reduces the code size about in half comparing to -O2
|
||||||
'-flto',
|
'-flto',
|
||||||
|
'-Isrc', '-Isrc/lua',
|
||||||
)
|
)
|
||||||
emflags = (
|
emflags = (
|
||||||
# Base
|
# Base
|
||||||
@@ -61,6 +62,15 @@ def build(src: Path, dst: Path):
|
|||||||
'-c', src / 'extension-functions.c',
|
'-c', src / 'extension-functions.c',
|
||||||
'-o', out / 'extension-functions.o',
|
'-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')
|
logging.info('Building WASM from bitcode')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
@@ -68,6 +78,7 @@ def build(src: Path, dst: Path):
|
|||||||
*emflags,
|
*emflags,
|
||||||
out / 'sqlite3.o',
|
out / 'sqlite3.o',
|
||||||
out / 'extension-functions.o',
|
out / 'extension-functions.o',
|
||||||
|
out / 'sqlitelua.o',
|
||||||
'-o', out / 'sql-wasm.js',
|
'-o', out / 'sql-wasm.js',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -30,8 +32,14 @@ extension_urls = (
|
|||||||
# =====================
|
# =====================
|
||||||
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/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'),
|
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
|
||||||
|
# Third-party extension with own dependencies
|
||||||
|
# ===========================================
|
||||||
|
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
|
||||||
|
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
|
||||||
|
|
||||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +67,38 @@ def _get_amalgamation(tgt: Path):
|
|||||||
shutil.copyfileobj(fr, fw)
|
shutil.copyfileobj(fr, fw)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lua(tgt: Path):
|
||||||
|
# Library definitions from lua/Makefile
|
||||||
|
lib_str = '''
|
||||||
|
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||||
|
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||||
|
ltm.o lundump.o lvm.o lzio.o
|
||||||
|
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||||
|
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
|
||||||
|
LUA_O= lua.o
|
||||||
|
'''
|
||||||
|
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
|
||||||
|
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
|
||||||
|
|
||||||
|
logging.info('Downloading and extracting Lua %s', lua_url)
|
||||||
|
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
|
||||||
|
(tgt / 'lua').mkdir()
|
||||||
|
for tarinfo in archive:
|
||||||
|
tarpath = Path(tarinfo.name)
|
||||||
|
if tarpath.match('src/*') and tarpath.stem in lib_names:
|
||||||
|
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
|
||||||
|
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
|
||||||
|
|
||||||
|
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
|
||||||
|
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
|
||||||
|
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||||
|
(tgt / 'sqlitelua').mkdir()
|
||||||
|
for zpath in (archive_root_dir / 'src').iterdir():
|
||||||
|
if zpath.name != 'main.c':
|
||||||
|
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
|
||||||
|
shutil.copyfileobj(fr, fw)
|
||||||
|
|
||||||
|
|
||||||
def _get_contrib_functions(tgt: Path):
|
def _get_contrib_functions(tgt: Path):
|
||||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||||
|
|
||||||
@@ -70,6 +110,7 @@ def _get_extensions(tgt: Path):
|
|||||||
for url, init_fn in extension_urls:
|
for url, init_fn in extension_urls:
|
||||||
logging.info('Downloading and appending to amalgamation %s', url)
|
logging.info('Downloading and appending to amalgamation %s', url)
|
||||||
with request.urlopen(url) as resp:
|
with request.urlopen(url) as resp:
|
||||||
|
f.write(b'\n')
|
||||||
shutil.copyfileobj(resp, f)
|
shutil.copyfileobj(resp, f)
|
||||||
init_functions.append(init_fn)
|
init_functions.append(init_fn)
|
||||||
|
|
||||||
@@ -90,6 +131,7 @@ def _get_sqljs(tgt: Path):
|
|||||||
def configure(tgt: Path):
|
def configure(tgt: Path):
|
||||||
_get_amalgamation(tgt)
|
_get_amalgamation(tgt)
|
||||||
_get_contrib_functions(tgt)
|
_get_contrib_functions(tgt)
|
||||||
|
_get_lua(tgt)
|
||||||
_get_extensions(tgt)
|
_get_extensions(tgt)
|
||||||
_get_sqljs(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.
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.23.1",
|
"version": "0.25.0",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"background_color": "white",
|
"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",
|
"display": "fullscreen",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -107,3 +107,9 @@ table.sqliteviz-table {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sqliteviz-table tbody td[data-isNull="true"],
|
||||||
|
.sqliteviz-table tbody td[data-isBlob="true"] {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,8 +8,8 @@
|
|||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
CSV import
|
{{ typeName }} import
|
||||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
<close-icon @click="cancelImport" :disabled="disableDialog"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<text-field
|
<text-field
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
width="484px"
|
width="484px"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
:error-msg="tableNameError"
|
:error-msg="tableNameError"
|
||||||
id="csv-table-name"
|
id="csv-json-table-name"
|
||||||
/>
|
/>
|
||||||
<div class="chars">
|
<div v-if="!isJson && !isNdJson" class="chars">
|
||||||
<delimiter-selector
|
<delimiter-selector
|
||||||
v-model="delimiter"
|
v-model="delimiter"
|
||||||
width="210px"
|
width="210px"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
@input="previewCsv"
|
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Quote char"
|
label="Quote char"
|
||||||
@@ -36,6 +36,7 @@
|
|||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
id="quote-char"
|
id="quote-char"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Escape char"
|
label="Escape char"
|
||||||
@@ -49,52 +50,52 @@
|
|||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
id="escape-char"
|
id="escape-char"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<check-box
|
<check-box
|
||||||
@click="header = $event"
|
v-if="!isJson && !isNdJson"
|
||||||
:init="true"
|
:init="header"
|
||||||
label="Use first row as column headers"
|
label="Use first row as column headers"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
|
@click="changeHeaderDisplaying"
|
||||||
/>
|
/>
|
||||||
<sql-table
|
<sql-table
|
||||||
v-if="previewData
|
v-if="previewData && previewData.rowCount > 0"
|
||||||
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
|
|
||||||
"
|
|
||||||
:data-set="previewData"
|
:data-set="previewData"
|
||||||
class="preview-table"
|
|
||||||
:preview="true"
|
:preview="true"
|
||||||
|
class="preview-table"
|
||||||
/>
|
/>
|
||||||
<div v-else class="no-data">No data</div>
|
<div v-else class="no-data">No data</div>
|
||||||
<logs
|
<logs
|
||||||
class="import-csv-errors"
|
class="import-errors"
|
||||||
:messages="importCsvMessages"
|
:messages="importMessages"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button
|
<button
|
||||||
class="secondary"
|
class="secondary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
@click="cancelCsvImport"
|
@click="cancelImport"
|
||||||
id="csv-cancel"
|
id="import-cancel"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-show="!importCsvCompleted"
|
v-show="!importCompleted"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog || disableImport"
|
||||||
@click="loadFromCsv(file)"
|
@click="loadToDb(file)"
|
||||||
id="csv-import"
|
id="import-start"
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-show="importCsvCompleted"
|
v-show="importCompleted"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
@click="finish"
|
@click="finish"
|
||||||
id="csv-finish"
|
id="import-finish"
|
||||||
>
|
>
|
||||||
Finish
|
Finish
|
||||||
</button>
|
</button>
|
||||||
@@ -115,7 +116,7 @@ import fIo from '@/lib/utils/fileIo'
|
|||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'CsvImport',
|
name: 'CsvJsonImport',
|
||||||
components: {
|
components: {
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -124,33 +125,50 @@ export default {
|
|||||||
SqlTable,
|
SqlTable,
|
||||||
Logs
|
Logs
|
||||||
},
|
},
|
||||||
props: ['file', 'db', 'dialogName'],
|
props: {
|
||||||
|
file: File,
|
||||||
|
db: Object,
|
||||||
|
dialogName: String
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
disableDialog: false,
|
disableDialog: false,
|
||||||
|
disableImport: false,
|
||||||
tableName: '',
|
tableName: '',
|
||||||
delimiter: '',
|
delimiter: '',
|
||||||
quoteChar: '"',
|
quoteChar: '"',
|
||||||
escapeChar: '"',
|
escapeChar: '"',
|
||||||
header: true,
|
header: true,
|
||||||
importCsvCompleted: false,
|
importCompleted: false,
|
||||||
importCsvMessages: [],
|
importMessages: [],
|
||||||
previewData: null,
|
previewData: null,
|
||||||
addedTable: null,
|
addedTable: null,
|
||||||
tableNameError: ''
|
tableNameError: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isJson () {
|
||||||
|
return fIo.isJSON(this.file)
|
||||||
|
},
|
||||||
|
isNdJson () {
|
||||||
|
return fIo.isNDJSON(this.file)
|
||||||
|
},
|
||||||
|
typeName () {
|
||||||
|
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
quoteChar () {
|
isJson () {
|
||||||
this.previewCsv()
|
if (this.isJson) {
|
||||||
|
this.delimiter = '\u001E'
|
||||||
|
this.header = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
isNdJson () {
|
||||||
escapeChar () {
|
if (this.isNdJson) {
|
||||||
this.previewCsv()
|
this.delimiter = '\u001E'
|
||||||
},
|
this.header = false
|
||||||
|
}
|
||||||
header () {
|
|
||||||
this.previewCsv()
|
|
||||||
},
|
},
|
||||||
tableName: time.debounce(function () {
|
tableName: time.debounce(function () {
|
||||||
this.tableNameError = ''
|
this.tableNameError = ''
|
||||||
@@ -164,7 +182,11 @@ export default {
|
|||||||
}, 400)
|
}, 400)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
changeHeaderDisplaying (e) {
|
||||||
|
this.header = e
|
||||||
|
this.preview()
|
||||||
|
},
|
||||||
|
cancelImport () {
|
||||||
if (!this.disableDialog) {
|
if (!this.disableDialog) {
|
||||||
if (this.addedTable) {
|
if (this.addedTable) {
|
||||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||||
@@ -175,14 +197,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
reset () {
|
reset () {
|
||||||
this.header = true
|
this.header = !this.isJson && !this.isNdJson
|
||||||
this.quoteChar = '"'
|
this.quoteChar = '"'
|
||||||
this.escapeChar = '"'
|
this.escapeChar = '"'
|
||||||
this.delimiter = ''
|
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
|
||||||
this.tableName = ''
|
this.tableName = ''
|
||||||
this.disableDialog = false
|
this.disableDialog = false
|
||||||
this.importCsvCompleted = false
|
this.disableImport = false
|
||||||
this.importCsvMessages = []
|
this.importCompleted = false
|
||||||
|
this.importMessages = []
|
||||||
this.previewData = null
|
this.previewData = null
|
||||||
this.addedTable = null
|
this.addedTable = null
|
||||||
this.tableNameError = ''
|
this.tableNameError = ''
|
||||||
@@ -191,39 +214,69 @@ export default {
|
|||||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||||
this.$modal.show(this.dialogName)
|
this.$modal.show(this.dialogName)
|
||||||
},
|
},
|
||||||
async previewCsv () {
|
async preview () {
|
||||||
this.importCsvCompleted = false
|
this.disableImport = false
|
||||||
|
if (!this.file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.importCompleted = false
|
||||||
const config = {
|
const config = {
|
||||||
preview: 3,
|
preview: 3,
|
||||||
quoteChar: this.quoteChar || '"',
|
quoteChar: this.quoteChar || '"',
|
||||||
escapeChar: this.escapeChar,
|
escapeChar: this.escapeChar,
|
||||||
header: this.header,
|
header: this.header,
|
||||||
delimiter: this.delimiter
|
delimiter: this.delimiter,
|
||||||
|
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const start = new Date()
|
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()
|
const end = new Date()
|
||||||
this.previewData = parseResult.data
|
this.previewData = parseResult.data
|
||||||
|
this.previewData.rowCount = parseResult.rowCount
|
||||||
this.delimiter = parseResult.delimiter
|
this.delimiter = parseResult.delimiter
|
||||||
|
|
||||||
// In parseResult.messages we can get parse errors
|
// 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) {
|
if (!parseResult.hasErrors) {
|
||||||
this.importCsvMessages.push({
|
this.importMessages.push({
|
||||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.importCsvMessages = [{
|
console.error(err)
|
||||||
|
this.importMessages = [{
|
||||||
message: err,
|
message: err,
|
||||||
type: 'error'
|
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) {
|
if (!this.tableName) {
|
||||||
this.tableNameError = "Table name can't be empty"
|
this.tableNameError = "Table name can't be empty"
|
||||||
return
|
return
|
||||||
@@ -234,17 +287,18 @@ export default {
|
|||||||
quoteChar: this.quoteChar || '"',
|
quoteChar: this.quoteChar || '"',
|
||||||
escapeChar: this.escapeChar,
|
escapeChar: this.escapeChar,
|
||||||
header: this.header,
|
header: this.header,
|
||||||
delimiter: this.delimiter
|
delimiter: this.delimiter,
|
||||||
|
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||||
}
|
}
|
||||||
const parseCsvMsg = {
|
const parsingMsg = {
|
||||||
message: 'Parsing CSV...',
|
message: `Parsing ${this.typeName}...`,
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}
|
}
|
||||||
this.importCsvMessages.push(parseCsvMsg)
|
this.importMessages.push(parsingMsg)
|
||||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
|
||||||
|
|
||||||
const importMsg = {
|
const importMsg = {
|
||||||
message: 'Importing CSV into a SQLite database...',
|
message: `Importing ${this.typeName} into a SQLite database...`,
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}
|
}
|
||||||
let importLoadingIndicator = null
|
let importLoadingIndicator = null
|
||||||
@@ -256,27 +310,30 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let start = new Date()
|
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()
|
let end = new Date()
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
if (!parseResult.hasErrors) {
|
||||||
const rowCount = parseResult.rowCount
|
const rowCount = parseResult.rowCount
|
||||||
let period = time.getPeriod(start, end)
|
let period = time.getPeriod(start, end)
|
||||||
parseCsvMsg.type = 'success'
|
parsingMsg.type = 'success'
|
||||||
|
|
||||||
if (parseResult.messages.length > 0) {
|
if (parseResult.messages.length > 0) {
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||||
} else {
|
} else {
|
||||||
// Inform about csv parsing success
|
// Inform about parsing success
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading indicator for csv parsing is not needed anymore
|
// Loading indicator for parsing is not needed anymore
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
clearTimeout(parsingLoadingIndicator)
|
||||||
|
|
||||||
// Add info about import start
|
// Add info about import start
|
||||||
this.importCsvMessages.push(importMsg)
|
this.importMessages.push(importMsg)
|
||||||
|
|
||||||
// Show import progress after 1 second
|
// Show import progress after 1 second
|
||||||
importLoadingIndicator = setTimeout(() => {
|
importLoadingIndicator = setTimeout(() => {
|
||||||
@@ -291,52 +348,108 @@ export default {
|
|||||||
this.addedTable = this.tableName
|
this.addedTable = this.tableName
|
||||||
// Inform about import success
|
// Inform about import success
|
||||||
period = time.getPeriod(start, end)
|
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'
|
importMsg.type = 'success'
|
||||||
|
|
||||||
// Loading indicator for import is not needed anymore
|
// Loading indicator for import is not needed anymore
|
||||||
clearTimeout(importLoadingIndicator)
|
clearTimeout(importLoadingIndicator)
|
||||||
|
|
||||||
this.importCsvCompleted = true
|
this.importCompleted = true
|
||||||
} else {
|
} else {
|
||||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
parsingMsg.message = 'Parsing ended with errors.'
|
||||||
parseCsvMsg.type = 'info'
|
parsingMsg.type = 'info'
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (parseCsvMsg.type === 'loading') {
|
console.error(err)
|
||||||
parseCsvMsg.type = 'info'
|
if (parsingMsg.type === 'loading') {
|
||||||
|
parsingMsg.type = 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importMsg.type === 'loading') {
|
if (importMsg.type === 'loading') {
|
||||||
importMsg.type = 'info'
|
importMsg.type = 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importCsvMessages.push({
|
this.importMessages.push({
|
||||||
message: err,
|
message: err,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
clearTimeout(parsingLoadingIndicator)
|
||||||
clearTimeout(importLoadingIndicator)
|
clearTimeout(importLoadingIndicator)
|
||||||
this.db.deleteProgressCounter(progressCounterId)
|
this.db.deleteProgressCounter(progressCounterId)
|
||||||
this.disableDialog = false
|
this.disableDialog = false
|
||||||
},
|
},
|
||||||
async finish () {
|
async finish () {
|
||||||
this.$modal.hide(this.dialogName)
|
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.`,
|
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||||
' * You can run this SQL query to make all CSV records available for charting.',
|
' * You can run this SQL query to make all CSV records available for charting.',
|
||||||
' */',
|
' */',
|
||||||
`SELECT * FROM "${this.addedTable}"`
|
`SELECT * FROM "${this.addedTable}"`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
},
|
||||||
this.$store.commit('setCurrentTabId', tabId)
|
getNdJsonQueryExample () {
|
||||||
this.importCsvCompleted = false
|
try {
|
||||||
this.$emit('finish')
|
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||||
events.send('inquiry.create', null, { auto: true })
|
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;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#csv-json-table-name {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.chars {
|
.chars {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
margin: 24px 0 20px;
|
margin: 0 0 20px;
|
||||||
}
|
}
|
||||||
.char-input {
|
.char-input {
|
||||||
margin-right: 44px;
|
margin-right: 44px;
|
||||||
@@ -359,7 +476,7 @@ export default {
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-csv-errors {
|
.import-errors {
|
||||||
height: 136px;
|
height: 136px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
@click="browse"
|
@click="browse"
|
||||||
>
|
>
|
||||||
<div class="text">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,13 +42,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv or json dialog -->
|
||||||
<csv-import
|
<csv-json-import
|
||||||
ref="addCsv"
|
ref="addCsvJson"
|
||||||
:file="file"
|
:file="file"
|
||||||
:db="newDb"
|
:db="newDb"
|
||||||
dialog-name="importFromCsv"
|
dialog-name="importFromCsvJson"
|
||||||
@cancel="cancelCsvImport"
|
@cancel="cancelImport"
|
||||||
@finish="finish"
|
@finish="finish"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +58,7 @@
|
|||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@@ -79,7 +80,7 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChangeDbIcon,
|
ChangeDbIcon,
|
||||||
CsvImport
|
CsvJsonImport
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -102,7 +103,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
cancelImport () {
|
||||||
if (this.newDb) {
|
if (this.newDb) {
|
||||||
this.newDb.shutDown()
|
this.newDb.shutDown()
|
||||||
this.newDb = null
|
this.newDb = null
|
||||||
@@ -128,21 +129,22 @@ export default {
|
|||||||
if (fIo.isDatabase(file)) {
|
if (fIo.isDatabase(file)) {
|
||||||
this.loadDb(file)
|
this.loadDb(file)
|
||||||
} else {
|
} else {
|
||||||
|
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||||
events.send('database.import', file.size, {
|
events.send('database.import', file.size, {
|
||||||
from: 'csv',
|
from: isJson ? 'json' : 'csv',
|
||||||
new_db: true
|
new_db: true
|
||||||
})
|
})
|
||||||
|
|
||||||
this.file = file
|
this.file = file
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const csvImport = this.$refs.addCsv
|
const csvJsonImportModal = this.$refs.addCsvJson
|
||||||
csvImport.reset()
|
csvJsonImportModal.reset()
|
||||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
|
||||||
.then(csvImport.open)
|
.then(csvJsonImportModal.open)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
browse () {
|
browse () {
|
||||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||||
.then(this.checkFile)
|
.then(this.checkFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<button
|
||||||
:class="['icon-btn', { active }, { disabled }]"
|
:class="['icon-btn', { active }]"
|
||||||
|
:disabled="disabled"
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||||
@mouseleave="hideTooltip"
|
@mouseleave="hideTooltip"
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -38,11 +39,12 @@ export default {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
width: 26px;
|
width: 26px;
|
||||||
height: 26px;
|
height: 26px;
|
||||||
cursor: pointer;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.icon-btn:hover {
|
.icon-btn:hover {
|
||||||
border: 1px solid var(--color-border);
|
border: 1px solid var(--color-border);
|
||||||
@@ -56,12 +58,12 @@ export default {
|
|||||||
fill: var(--color-accent);
|
fill: var(--color-accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn .icon >>> path,
|
.icon-btn:disabled .icon >>> path,
|
||||||
.disabled.icon-btn .icon >>> circle {
|
.icon-btn:disabled .icon >>> circle {
|
||||||
fill: var(--color-border);
|
fill: var(--color-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.disabled.icon-btn {
|
.icon-btn:disabled {
|
||||||
cursor: default;
|
cursor: default;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
ref="table-container"
|
ref="table-container"
|
||||||
@scroll="onScrollTable"
|
@scroll="onScrollTable"
|
||||||
>
|
>
|
||||||
<table ref="table" class="sqliteviz-table">
|
<table
|
||||||
|
ref="table"
|
||||||
|
class="sqliteviz-table"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onTableKeydown"
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="(th, index) in columns" :key="index" ref="th">
|
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||||
@@ -28,9 +33,18 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
<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">
|
<div class="cell-data" :style="cellStyle">
|
||||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
{{ getCellText(col, rowIndex) }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -44,7 +58,11 @@
|
|||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
<span v-if="time">in {{ time }}</span>
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -57,19 +75,25 @@ export default {
|
|||||||
components: { Pager },
|
components: { Pager },
|
||||||
props: {
|
props: {
|
||||||
dataSet: Object,
|
dataSet: Object,
|
||||||
time: String,
|
time: [String, Number],
|
||||||
pageSize: {
|
pageSize: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 20
|
default: 20
|
||||||
},
|
},
|
||||||
preview: Boolean
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
preview: Boolean,
|
||||||
|
selectedCellCoordinates: Object
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
tableWidth: null,
|
tableWidth: null,
|
||||||
currentPage: 1,
|
currentPage: this.page,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
selectedCellElement: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
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: {
|
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 () {
|
calculateHeadersWidth () {
|
||||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -110,18 +167,95 @@ export default {
|
|||||||
},
|
},
|
||||||
onScrollTable () {
|
onScrollTable () {
|
||||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||||
|
},
|
||||||
|
onTableKeydown (e) {
|
||||||
|
const keyCodeMap = {
|
||||||
|
37: 'left',
|
||||||
|
39: 'right',
|
||||||
|
38: 'up',
|
||||||
|
40: 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.selectedCellElement ||
|
||||||
|
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||||
|
},
|
||||||
|
onCellClick (e) {
|
||||||
|
this.selectCell(e.target.closest('td'), false)
|
||||||
|
},
|
||||||
|
selectCell (cell, scrollTo = true) {
|
||||||
|
if (!cell) {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
cell.ariaSelected = 'true'
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else {
|
||||||
|
cell.ariaSelected = 'false'
|
||||||
|
this.selectedCellElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedCellElement && scrollTo) {
|
||||||
|
this.selectedCellElement.scrollIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||||
|
},
|
||||||
|
moveFocusInTable (initialCell, direction) {
|
||||||
|
const currentRowIndex = +initialCell.dataset.row
|
||||||
|
const currentColIndex = +initialCell.dataset.col
|
||||||
|
let newRowIndex, newColIndex
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
if (currentColIndex === this.columns.length - 1) {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = 0
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex + 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'left') {
|
||||||
|
if (currentColIndex === 0) {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = this.columns.length - 1
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex - 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
} else if (direction === 'down') {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
|
||||||
|
if (newCell) {
|
||||||
|
this.selectCell(newCell)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
|
||||||
this.resizeObserver.observe(this.$refs.table)
|
|
||||||
this.calculateHeadersWidth()
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.resizeObserver.unobserve(this.$refs.table)
|
this.resizeObserver.unobserve(this.$refs.table)
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentPageData: 'calculateHeadersWidth',
|
currentPageData () {
|
||||||
|
this.calculateHeadersWidth()
|
||||||
|
this.selectCell(null)
|
||||||
|
},
|
||||||
dataSet () {
|
dataSet () {
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
}
|
}
|
||||||
@@ -130,4 +264,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
Add new table from CSV
|
Add new table from CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</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>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
Load another database or CSV
|
Load another database, CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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 {
|
export default {
|
||||||
getResult (source) {
|
getResult (source, columns) {
|
||||||
const result = {
|
const result = {
|
||||||
columns: []
|
columns: columns || []
|
||||||
}
|
}
|
||||||
const values = {}
|
const values = {}
|
||||||
if (source.meta.fields) {
|
if (source.meta.fields) {
|
||||||
@@ -24,8 +24,18 @@ export default {
|
|||||||
return value
|
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 {
|
} 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}`
|
const colName = `col${i + 1}`
|
||||||
result.columns.push(colName)
|
result.columns.push(colName)
|
||||||
values[colName] = source.data.map(row => {
|
values[colName] = source.data.map(row => {
|
||||||
@@ -76,7 +86,7 @@ export default {
|
|||||||
let res
|
let res
|
||||||
try {
|
try {
|
||||||
res = {
|
res = {
|
||||||
data: this.getResult(results),
|
data: this.getResult(results, config.columns),
|
||||||
delimiter: results.meta.delimiter,
|
delimiter: results.meta.delimiter,
|
||||||
hasErrors: false,
|
hasErrors: false,
|
||||||
rowCount: results.data.length
|
rowCount: results.data.length
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ function _getDataSourcesFromSqlResult (sqlResult) {
|
|||||||
if (!sqlResult) {
|
if (!sqlResult) {
|
||||||
return {}
|
return {}
|
||||||
}
|
}
|
||||||
const dataSorces = {}
|
const dataSources = {}
|
||||||
sqlResult.columns.forEach((column, index) => {
|
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 {
|
export default class Sql {
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ import Lib from 'plotly.js/src/lib'
|
|||||||
import dataUrlToBlob from 'dataurl-to-blob'
|
import dataUrlToBlob from 'dataurl-to-blob'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async copyCsv (str) {
|
async copyText (str, notifyMessage) {
|
||||||
await navigator.clipboard.writeText(str)
|
await navigator.clipboard.writeText(str)
|
||||||
Lib.notifier('CSV copied to clipboard successfully', 'long')
|
if (notifyMessage) {
|
||||||
|
Lib.notifier(notifyMessage, 'long')
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async copyImage (source) {
|
async copyImage (source) {
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
|
isJSON (file) {
|
||||||
|
return file && file.type === 'application/json'
|
||||||
|
},
|
||||||
|
isNDJSON (file) {
|
||||||
|
return file && file.name.endsWith('.ndjson')
|
||||||
|
},
|
||||||
isDatabase (file) {
|
isDatabase (file) {
|
||||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||||
return file.type
|
return file.type
|
||||||
@@ -51,19 +57,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
importFile () {
|
importFile () {
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
return this.getFileFromUser('.json')
|
return this.getFileFromUser('.json')
|
||||||
.then(file => {
|
.then(file => {
|
||||||
return new Promise((resolve, reject) => {
|
return this.getFileContent(file)
|
||||||
reader.onload = e => {
|
|
||||||
resolve(e.target.result)
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFileContent (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onload = e => resolve(e.target.result)
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
readFile (path) {
|
readFile (path) {
|
||||||
return fetch(path)
|
return fetch(path)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,9 +36,11 @@ export default {
|
|||||||
state.currentTabId = state.tabs[index - 1].id
|
state.currentTabId = state.tabs[index - 1].id
|
||||||
} else {
|
} else {
|
||||||
state.currentTabId = null
|
state.currentTabId = null
|
||||||
state.currentTab = null
|
|
||||||
state.untitledLastIndex = 0
|
state.untitledLastIndex = 0
|
||||||
}
|
}
|
||||||
|
state.currentTab = state.currentTabId
|
||||||
|
? state.tabs.find(tab => tab.id === state.currentTabId)
|
||||||
|
: null
|
||||||
}
|
}
|
||||||
state.tabs.splice(index, 1)
|
state.tabs.splice(index, 1)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<db-uploader id="db-edit" type="small" />
|
<db-uploader id="db-edit" type="small" />
|
||||||
<export-icon tooltip="Export database" @click="exportToFile"/>
|
<export-icon tooltip="Export database" @click="exportToFile"/>
|
||||||
<add-table-icon @click="addCsv"/>
|
<add-table-icon @click="addCsvJson"/>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="schemaVisible" class="schema">
|
<div v-show="schemaVisible" class="schema">
|
||||||
<table-description
|
<table-description
|
||||||
@@ -21,12 +21,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv or json dialog -->
|
||||||
<csv-import
|
<csv-json-import
|
||||||
ref="addCsv"
|
ref="addCsvJson"
|
||||||
:file="file"
|
:file="file"
|
||||||
:db="$store.state.db"
|
:db="$store.state.db"
|
||||||
dialog-name="addCsv"
|
dialog-name="addCsvJson"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -40,7 +40,7 @@ import TreeChevron from '@/components/svg/treeChevron'
|
|||||||
import DbUploader from '@/components/DbUploader'
|
import DbUploader from '@/components/DbUploader'
|
||||||
import ExportIcon from '@/components/svg/export'
|
import ExportIcon from '@/components/svg/export'
|
||||||
import AddTableIcon from '@/components/svg/addTable'
|
import AddTableIcon from '@/components/svg/addTable'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Schema',
|
name: 'Schema',
|
||||||
@@ -51,7 +51,7 @@ export default {
|
|||||||
DbUploader,
|
DbUploader,
|
||||||
ExportIcon,
|
ExportIcon,
|
||||||
AddTableIcon,
|
AddTableIcon,
|
||||||
CsvImport
|
CsvJsonImport
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -80,16 +80,17 @@ export default {
|
|||||||
exportToFile () {
|
exportToFile () {
|
||||||
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
this.$store.state.db.export(`${this.dbName}.sqlite`)
|
||||||
},
|
},
|
||||||
async addCsv () {
|
async addCsvJson () {
|
||||||
this.file = await fIo.getFileFromUser('.csv')
|
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const csvImport = this.$refs.addCsv
|
const csvJsonImportModal = this.$refs.addCsvJson
|
||||||
csvImport.reset()
|
csvJsonImportModal.reset()
|
||||||
await csvImport.previewCsv()
|
await csvJsonImportModal.preview()
|
||||||
csvImport.open()
|
csvJsonImportModal.open()
|
||||||
|
|
||||||
|
const isJson = fIo.isJSON(this.file) || fIo.isNDJSON(this.file)
|
||||||
events.send('database.import', this.file.size, {
|
events.send('database.import', this.file.size, {
|
||||||
from: 'csv',
|
from: isJson ? 'json' : 'csv',
|
||||||
new_db: false
|
new_db: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ export default {
|
|||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
)
|
)
|
||||||
|
this.$emit('update:importToSvgEnabled', true)
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
|
|||||||
@@ -41,7 +41,6 @@
|
|||||||
>
|
>
|
||||||
<png-icon />
|
<png-icon />
|
||||||
</icon-button>
|
</icon-button>
|
||||||
|
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!importToSvgEnabled"
|
:disabled="!importToSvgEnabled"
|
||||||
tooltip="Save as SVG"
|
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>
|
<template>
|
||||||
<div class="run-result-panel" ref="runResultPanel">
|
<div class="run-result-panel" ref="runResultPanel">
|
||||||
<div class="run-result-panel-content">
|
<component
|
||||||
<div
|
:is="viewValuePanelVisible ? 'splitpanes':'div'"
|
||||||
v-show="result === null && !isGettingResults && !error"
|
:before="{ size: 50, max: 100 }"
|
||||||
class="table-preview result-before"
|
:after="{ size: 50, max: 100 }"
|
||||||
>
|
:default="{ before: 50, after: 50 }"
|
||||||
Run your query and get results here
|
class="run-result-panel-content"
|
||||||
</div>
|
>
|
||||||
<div v-if="isGettingResults" class="table-preview result-in-progress">
|
<template #left-pane>
|
||||||
<loading-indicator :size="30"/>
|
<div :id="'run-result-left-pane-'+tab.id" class="result-set-container"/>
|
||||||
Fetching results...
|
</template>
|
||||||
</div>
|
<div :id="'run-result-result-set-'+tab.id" class="result-set-container"/>
|
||||||
<div
|
<template #right-pane v-if="viewValuePanelVisible">
|
||||||
v-show="result === undefined && !isGettingResults && !error"
|
<div class="value-viewer-container">
|
||||||
class="table-preview result-empty"
|
<value-viewer
|
||||||
>
|
v-show="selectedCell"
|
||||||
No rows retrieved according to your query
|
:cellValue="selectedCell
|
||||||
</div>
|
? result.values[result.columns[selectedCell.dataset.col]][selectedCell.dataset.row]
|
||||||
<logs v-if="error" :messages="[error]"/>
|
: ''"
|
||||||
<sql-table
|
/>
|
||||||
v-if="result"
|
<div v-show="!selectedCell" class="table-preview">
|
||||||
:data-set="result"
|
No cell selected to view
|
||||||
:time="time"
|
</div>
|
||||||
:pageSize="pageSize"
|
</div>
|
||||||
class="straight"
|
</template>
|
||||||
/>
|
</component>
|
||||||
</div>
|
|
||||||
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
|
||||||
<icon-button
|
<icon-button
|
||||||
:disabled="!result"
|
:disabled="!result"
|
||||||
@@ -44,6 +44,26 @@
|
|||||||
>
|
>
|
||||||
<clipboard-icon/>
|
<clipboard-icon/>
|
||||||
</icon-button>
|
</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>
|
</side-tool-bar>
|
||||||
|
|
||||||
<loading-dialog
|
<loading-dialog
|
||||||
@@ -56,6 +76,48 @@
|
|||||||
@action="copyToClipboard"
|
@action="copyToClipboard"
|
||||||
@cancel="cancelCopy"
|
@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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -63,9 +125,12 @@
|
|||||||
import Logs from '@/components/Logs'
|
import Logs from '@/components/Logs'
|
||||||
import SqlTable from '@/components/SqlTable'
|
import SqlTable from '@/components/SqlTable'
|
||||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
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 ExportToCsvIcon from '@/components/svg/exportToCsv'
|
||||||
import ClipboardIcon from '@/components/svg/clipboard'
|
import ClipboardIcon from '@/components/svg/clipboard'
|
||||||
|
import ViewCellValueIcon from '@/components/svg/viewCellValue'
|
||||||
|
import RowIcon from '@/components/svg/row'
|
||||||
import IconButton from '@/components/IconButton'
|
import IconButton from '@/components/IconButton'
|
||||||
import csv from '@/lib/csv'
|
import csv from '@/lib/csv'
|
||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
@@ -73,16 +138,30 @@ import cIo from '@/lib/utils/clipboardIo'
|
|||||||
import time from '@/lib/utils/time'
|
import time from '@/lib/utils/time'
|
||||||
import loadingDialog from '@/components/LoadingDialog'
|
import loadingDialog from '@/components/LoadingDialog'
|
||||||
import events from '@/lib/utils/events'
|
import events from '@/lib/utils/events'
|
||||||
|
import Teleport from 'vue2-teleport'
|
||||||
|
import ValueViewer from './ValueViewer'
|
||||||
|
import Record from './Record/index.vue'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RunResult',
|
name: 'RunResult',
|
||||||
props: ['result', 'isGettingResults', 'error', 'time'],
|
props: {
|
||||||
|
tab: Object,
|
||||||
|
result: Object,
|
||||||
|
isGettingResults: Boolean,
|
||||||
|
error: Object,
|
||||||
|
time: [String, Number]
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
resizeObserver: null,
|
resizeObserver: null,
|
||||||
pageSize: 20,
|
pageSize: 20,
|
||||||
preparingCopy: false,
|
preparingCopy: false,
|
||||||
dataToCopy: null
|
dataToCopy: null,
|
||||||
|
viewValuePanelVisible: false,
|
||||||
|
selectedCell: null,
|
||||||
|
viewRecord: false,
|
||||||
|
defaultPage: 1,
|
||||||
|
defaultSelectedCell: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -93,7 +172,23 @@ export default {
|
|||||||
ExportToCsvIcon,
|
ExportToCsvIcon,
|
||||||
IconButton,
|
IconButton,
|
||||||
ClipboardIcon,
|
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 () {
|
mounted () {
|
||||||
this.resizeObserver = new ResizeObserver(this.handleResize)
|
this.resizeObserver = new ResizeObserver(this.handleResize)
|
||||||
@@ -103,6 +198,12 @@ export default {
|
|||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
this.resizeObserver.unobserve(this.$refs.runResultPanel)
|
||||||
},
|
},
|
||||||
|
watch: {
|
||||||
|
result () {
|
||||||
|
this.defaultSelectedCell = null
|
||||||
|
this.selectedCell = null
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleResize () {
|
handleResize () {
|
||||||
this.calculatePageSize()
|
this.calculatePageSize()
|
||||||
@@ -160,13 +261,35 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
copyToClipboard () {
|
copyToClipboard () {
|
||||||
cIo.copyCsv(this.dataToCopy)
|
cIo.copyText(this.dataToCopy, 'CSV copied to clipboard successfully')
|
||||||
this.$modal.hide('prepareCSVCopy')
|
this.$modal.hide('prepareCSVCopy')
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelCopy () {
|
cancelCopy () {
|
||||||
this.dataToCopy = null
|
this.dataToCopy = null
|
||||||
this.$modal.hide('prepareCSVCopy')
|
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 {
|
.run-result-panel-content {
|
||||||
position: relative;
|
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-set-container,
|
||||||
|
.result-set-container > div {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
.value-viewer-container {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--color-white);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
.table-preview {
|
.table-preview {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -194,6 +329,7 @@ export default {
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.result-in-progress {
|
.result-in-progress {
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
<teleport :to="`#${tab.layout.table}-${tab.id}`">
|
||||||
<run-result
|
<run-result
|
||||||
|
:tab="tab"
|
||||||
:result="tab.result"
|
:result="tab.result"
|
||||||
:is-getting-results="tab.isGettingResults"
|
:is-getting-results="tab.isGettingResults"
|
||||||
:error="tab.error"
|
:error="tab.error"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<main-menu />
|
<main-menu />
|
||||||
<keep-alive include="Workspace">
|
<keep-alive include="Workspace,Inquiries">
|
||||||
<router-view id="main-view" />
|
<router-view id="main-view" />
|
||||||
</keep-alive>
|
</keep-alive>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import { expect } from 'chai'
|
|||||||
import sinon from 'sinon'
|
import sinon from 'sinon'
|
||||||
import Vuex from 'vuex'
|
import Vuex from 'vuex'
|
||||||
import { mount } from '@vue/test-utils'
|
import { mount } from '@vue/test-utils'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||||
import csv from '@/lib/csv'
|
import csv from '@/lib/csv'
|
||||||
|
|
||||||
describe('CsvImport.vue', () => {
|
describe('CsvJsonImport.vue', () => {
|
||||||
let state = {}
|
let state = {}
|
||||||
let actions = {}
|
let actions = {}
|
||||||
let mutations = {}
|
let mutations = {}
|
||||||
@@ -13,7 +13,7 @@ describe('CsvImport.vue', () => {
|
|||||||
let clock
|
let clock
|
||||||
let wrapper
|
let wrapper
|
||||||
const newTabId = 1
|
const newTabId = 1
|
||||||
const file = { name: 'my data.csv' }
|
const file = new File([], 'my data.csv')
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
clock = sinon.useFakeTimers()
|
clock = sinon.useFakeTimers()
|
||||||
@@ -40,11 +40,11 @@ describe('CsvImport.vue', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mount the component
|
// mount the component
|
||||||
wrapper = mount(CsvImport, {
|
wrapper = mount(CsvJsonImport, {
|
||||||
store,
|
store,
|
||||||
propsData: {
|
propsData: {
|
||||||
file,
|
file,
|
||||||
dialogName: 'addCsv',
|
dialogName: 'addCsvJson',
|
||||||
db
|
db
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -74,11 +74,12 @@ describe('CsvImport.vue', () => {
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
await wrapper.vm.open()
|
await wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||||
expect(wrapper.find('#csv-table-name input').element.value).to.equal('my_data')
|
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.findComponent({ name: 'delimiter-selector' }).vm.value).to.equal('|')
|
||||||
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
expect(wrapper.find('#quote-char input').element.value).to.equal('"')
|
||||||
expect(wrapper.find('#escape-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.')
|
.to.include('Information about row 0. Comma was used as a standart delimiter.')
|
||||||
expect(wrapper.findComponent({ name: 'logs' }).text())
|
expect(wrapper.findComponent({ name: 'logs' }).text())
|
||||||
.to.include('Preview parsing is completed in')
|
.to.include('Preview parsing is completed in')
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
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 () => {
|
it('reparses when parameters changes', async () => {
|
||||||
@@ -111,7 +140,7 @@ describe('CsvImport.vue', () => {
|
|||||||
rowCount: 1
|
rowCount: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await csv.parse.returnValues[0]
|
await csv.parse.returnValues[0]
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
@@ -231,20 +260,32 @@ describe('CsvImport.vue', () => {
|
|||||||
col2: ['foo']
|
col2: ['foo']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
rowCount: 1
|
rowCount: 1,
|
||||||
|
messages: []
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
let resolveParsing
|
let resolveParsing
|
||||||
parse.onCall(1).returns(new Promise(resolve => {
|
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-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
// "Parsing CSV..." in the logs
|
// "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('#quote-char input').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||||
await resolveParsing()
|
await resolveParsing()
|
||||||
await parse.returnValues[1]
|
await parse.returnValues[1]
|
||||||
|
|
||||||
@@ -306,7 +347,7 @@ describe('CsvImport.vue', () => {
|
|||||||
messages: []
|
messages: []
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
@@ -315,8 +356,8 @@ describe('CsvImport.vue', () => {
|
|||||||
resolveImport = resolve
|
resolveImport = resolve
|
||||||
}))
|
}))
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||||
await resolveImport()
|
await resolveImport()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -377,12 +418,12 @@ describe('CsvImport.vue', () => {
|
|||||||
resolveImport = resolve
|
resolveImport = resolve
|
||||||
}))
|
}))
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||||
await resolveImport()
|
await resolveImport()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -440,12 +481,12 @@ describe('CsvImport.vue', () => {
|
|||||||
}]
|
}]
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-finish').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.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)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('has proper state before import is completed', async () => {
|
it('has proper state before import is completed', async () => {
|
||||||
@@ -501,12 +542,12 @@ describe('CsvImport.vue', () => {
|
|||||||
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
wrapper.vm.db.addTableFromCsv = sinon.stub()
|
||||||
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
.resolves(new Promise(resolve => { resolveImport = resolve }))
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(true)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').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.findComponent({ name: 'close-icon' }).vm.disabled).to.equal(true)
|
||||||
expect(wrapper.find('#csv-finish').isVisible()).to.equal(false)
|
expect(wrapper.find('#import-finish').isVisible()).to.equal(false)
|
||||||
expect(wrapper.find('#csv-import').isVisible()).to.equal(true)
|
expect(wrapper.find('#import-start').isVisible()).to.equal(true)
|
||||||
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
expect(wrapper.vm.db.addTableFromCsv.getCall(0).args[0]).to.equal('foo') // table name
|
||||||
|
|
||||||
// After resolving - loading indicator is not shown
|
// After resolving - loading indicator is not shown
|
||||||
@@ -570,12 +611,12 @@ describe('CsvImport.vue', () => {
|
|||||||
messages: []
|
messages: []
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-finish').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.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 () => {
|
it('import fails', async () => {
|
||||||
@@ -627,12 +668,12 @@ describe('CsvImport.vue', () => {
|
|||||||
|
|
||||||
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
wrapper.vm.db.addTableFromCsv = sinon.stub().rejects(new Error('fail'))
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-table-name input').setValue('foo')
|
await wrapper.find('#csv-json-table-name input').setValue('foo')
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await csv.parse.returnValues[1]
|
await csv.parse.returnValues[1]
|
||||||
await wrapper.vm.$nextTick()
|
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('#quote-char input').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#escape-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.findComponent({ name: 'check-box' }).vm.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-cancel').element.disabled).to.equal(false)
|
expect(wrapper.find('#import-cancel').element.disabled).to.equal(false)
|
||||||
expect(wrapper.find('#csv-finish').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.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 () => {
|
it('import finish', async () => {
|
||||||
@@ -668,19 +709,19 @@ describe('CsvImport.vue', () => {
|
|||||||
messages: []
|
messages: []
|
||||||
})
|
})
|
||||||
|
|
||||||
wrapper.vm.previewCsv()
|
wrapper.vm.preview()
|
||||||
wrapper.vm.open()
|
wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
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)
|
expect(actions.addTab.calledOnce).to.equal(true)
|
||||||
await actions.addTab.returnValues[0]
|
await actions.addTab.returnValues[0]
|
||||||
expect(mutations.setCurrentTabId.calledOnceWith(state, newTabId)).to.equal(true)
|
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)
|
expect(wrapper.emitted('finish')).to.have.lengthOf(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -699,47 +740,525 @@ describe('CsvImport.vue', () => {
|
|||||||
messages: []
|
messages: []
|
||||||
})
|
})
|
||||||
|
|
||||||
await wrapper.vm.previewCsv()
|
await wrapper.vm.preview()
|
||||||
await wrapper.vm.open()
|
await wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
|
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
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.execute.calledOnceWith('DROP TABLE "my_data"')).to.equal(true)
|
||||||
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
expect(wrapper.vm.db.refreshSchema.calledOnce).to.equal(true)
|
||||||
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
expect(wrapper.emitted('cancel')).to.have.lengthOf(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('checks table name', async () => {
|
it('checks table name', async () => {
|
||||||
sinon.stub(csv, 'parse').resolves()
|
sinon.stub(csv, 'parse').resolves({
|
||||||
await wrapper.vm.previewCsv()
|
data: {},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: []
|
||||||
|
})
|
||||||
|
await wrapper.vm.preview()
|
||||||
await wrapper.vm.open()
|
await wrapper.vm.open()
|
||||||
await wrapper.vm.$nextTick()
|
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 clock.tick(400)
|
||||||
await wrapper.vm.$nextTick()
|
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'))
|
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 clock.tick(400)
|
||||||
await wrapper.vm.$nextTick()
|
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.')
|
.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 clock.tick(400)
|
||||||
await wrapper.vm.$nextTick()
|
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')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
expect(wrapper.find('#csv-table-name .text-field-error').text())
|
expect(wrapper.find('#csv-json-table-name .text-field-error').text())
|
||||||
.to.equal("Table name can't be empty")
|
.to.equal("Table name can't be empty")
|
||||||
expect(wrapper.vm.db.addTableFromCsv.called).to.equal(false)
|
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 { expect } from 'chai'
|
||||||
import { mount, shallowMount } from '@vue/test-utils'
|
import { mount, shallowMount } from '@vue/test-utils'
|
||||||
import DelimiterSelector from '@/components/CsvImport/DelimiterSelector'
|
import DelimiterSelector from '@/components/CsvJsonImport/DelimiterSelector'
|
||||||
|
|
||||||
describe('DelimiterSelector', async () => {
|
describe('DelimiterSelector', async () => {
|
||||||
it('shows the name of value', 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 () => {
|
it('loads db on click and redirects to /workspace', async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = { name: 'test.db' }
|
const file = new File([], 'test.db')
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
@@ -85,7 +85,7 @@ describe('DbUploader.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// mock a file dropped by a user
|
// mock a file dropped by a user
|
||||||
const file = { name: 'test.db' }
|
const file = new File([], 'test.db')
|
||||||
const dropData = { dataTransfer: new DataTransfer() }
|
const dropData = { dataTransfer: new DataTransfer() }
|
||||||
Object.defineProperty(dropData.dataTransfer, 'files', {
|
Object.defineProperty(dropData.dataTransfer, 'files', {
|
||||||
value: [file],
|
value: [file],
|
||||||
@@ -103,7 +103,7 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
it("doesn't redirect if already on /workspace", async () => {
|
it("doesn't redirect if already on /workspace", async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = { name: 'test.db' }
|
const file = new File([], 'test.db')
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock db loading
|
// mock db loading
|
||||||
@@ -136,7 +136,7 @@ describe('DbUploader.vue', () => {
|
|||||||
|
|
||||||
it('shows parse dialog if gets csv file', async () => {
|
it('shows parse dialog if gets csv file', async () => {
|
||||||
// mock getting a file from user
|
// mock getting a file from user
|
||||||
const file = { name: 'test.csv' }
|
const file = new File([], 'test.csv')
|
||||||
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock router
|
// 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, 'reset')
|
||||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
sinon.stub(CsvImport, 'preview').resolves()
|
||||||
sinon.stub(CsvImport, 'open')
|
sinon.stub(CsvImport, 'open')
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(CsvImport.reset.calledOnce).to.equal(true)
|
expect(CsvImport.reset.calledOnce).to.equal(true)
|
||||||
await wrapper.vm.animationPromise
|
await wrapper.vm.animationPromise
|
||||||
expect(CsvImport.previewCsv.calledOnce).to.equal(true)
|
expect(CsvImport.preview.calledOnce).to.equal(true)
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(CsvImport.open.calledOnce).to.equal(true)
|
expect(CsvImport.open.calledOnce).to.equal(true)
|
||||||
wrapper.destroy()
|
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
|
// 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)
|
sinon.stub(fu, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
// mock router
|
// 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, 'reset')
|
||||||
sinon.stub(CsvImport, 'previewCsv').resolves()
|
sinon.stub(CsvImport, 'preview').resolves()
|
||||||
sinon.stub(CsvImport, 'open')
|
sinon.stub(CsvImport, 'open')
|
||||||
|
|
||||||
await wrapper.find('.drop-area').trigger('click')
|
await wrapper.find('.drop-area').trigger('click')
|
||||||
|
|||||||
@@ -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 = {
|
const source = {
|
||||||
data: [
|
data: [
|
||||||
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
[1, 'foo', new Date('2021-06-30T14:10:24.717Z')],
|
||||||
|
|||||||
@@ -455,4 +455,85 @@ describe('SQLite extensions', function () {
|
|||||||
xx: [1], xy: [0], xz: [1], yx: [0], yy: [1], yz: [1], zx: [1], zy: [1], zz: [1]
|
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'] })
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ describe('clipboardIo.js', async () => {
|
|||||||
sinon.restore()
|
sinon.restore()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('copyCsv', async () => {
|
it('copyText', async () => {
|
||||||
sinon.stub(navigator.clipboard, 'writeText').resolves(true)
|
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'))
|
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.')
|
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', () => {
|
it('isDatabase', () => {
|
||||||
let file = { type: 'application/vnd.sqlite3' }
|
let file = { type: 'application/vnd.sqlite3' }
|
||||||
expect(fIo.isDatabase(file)).to.equal(true)
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
|
file = { type: 'application/json' }
|
||||||
|
expect(fIo.isDatabase(file)).to.equal(false)
|
||||||
|
|
||||||
file = { type: 'application/x-sqlite3' }
|
file = { type: 'application/x-sqlite3' }
|
||||||
expect(fIo.isDatabase(file)).to.equal(true)
|
expect(fIo.isDatabase(file)).to.equal(true)
|
||||||
|
|
||||||
@@ -125,6 +180,9 @@ describe('fileIo.js', () => {
|
|||||||
file = { type: '', name: 'test.csv' }
|
file = { type: '', name: 'test.csv' }
|
||||||
expect(fIo.isDatabase(file)).to.equal(false)
|
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' }
|
file = { type: 'text', name: 'test.db' }
|
||||||
expect(fIo.isDatabase(file)).to.equal(false)
|
expect(fIo.isDatabase(file)).to.equal(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -29,12 +29,12 @@ describe('time.js', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('sleep resolves after n ms', async () => {
|
it('sleep resolves after n ms', async () => {
|
||||||
let before = Date.now()
|
let before = performance.now()
|
||||||
await time.sleep(10)
|
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)
|
await time.sleep(30)
|
||||||
expect(Date.now() - before).to.be.least(30)
|
expect(performance.now() - before).to.be.least(30)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -176,13 +176,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, tab1)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(2)
|
expect(state.tabs[0].id).to.equal(2)
|
||||||
expect(state.currentTabId).to.equal(2)
|
expect(state.currentTabId).to.equal(2)
|
||||||
|
expect(state.currentTab).to.eql(tab2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, last', () => {
|
it('deleteTab - opened, last', () => {
|
||||||
@@ -208,13 +210,15 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2],
|
tabs: [tab1, tab2],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, tab2)
|
deleteTab(state, tab2)
|
||||||
expect(state.tabs).to.have.lengthOf(1)
|
expect(state.tabs).to.have.lengthOf(1)
|
||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.currentTabId).to.equal(1)
|
expect(state.currentTabId).to.equal(1)
|
||||||
|
expect(state.currentTab).to.eql(tab1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, in the middle', () => {
|
it('deleteTab - opened, in the middle', () => {
|
||||||
@@ -250,7 +254,8 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1, tab2, tab3],
|
tabs: [tab1, tab2, tab3],
|
||||||
currentTabId: 2
|
currentTabId: 2,
|
||||||
|
currentTab: tab2
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, tab2)
|
deleteTab(state, tab2)
|
||||||
@@ -258,6 +263,7 @@ describe('mutations', () => {
|
|||||||
expect(state.tabs[0].id).to.equal(1)
|
expect(state.tabs[0].id).to.equal(1)
|
||||||
expect(state.tabs[1].id).to.equal(3)
|
expect(state.tabs[1].id).to.equal(3)
|
||||||
expect(state.currentTabId).to.equal(3)
|
expect(state.currentTabId).to.equal(3)
|
||||||
|
expect(state.currentTab).to.eql(tab3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('deleteTab - opened, single', () => {
|
it('deleteTab - opened, single', () => {
|
||||||
@@ -273,12 +279,14 @@ describe('mutations', () => {
|
|||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
tabs: [tab1],
|
tabs: [tab1],
|
||||||
currentTabId: 1
|
currentTabId: 1,
|
||||||
|
currentTab: tab1
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteTab(state, tab1)
|
deleteTab(state, tab1)
|
||||||
expect(state.tabs).to.have.lengthOf(0)
|
expect(state.tabs).to.have.lengthOf(0)
|
||||||
expect(state.currentTabId).to.equal(null)
|
expect(state.currentTabId).to.equal(null)
|
||||||
|
expect(state.currentTab).to.equal(null)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('setCurrentTabId', () => {
|
it('setCurrentTabId', () => {
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ describe('Schema.vue', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('adds table', async () => {
|
it('adds table', async () => {
|
||||||
const file = { name: 'test.csv' }
|
const file = new File([], 'test.csv')
|
||||||
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
sinon.stub(fIo, 'getFileFromUser').resolves(file)
|
||||||
|
|
||||||
sinon.stub(csv, 'parse').resolves({
|
sinon.stub(csv, 'parse').resolves({
|
||||||
@@ -152,20 +152,20 @@ describe('Schema.vue', () => {
|
|||||||
|
|
||||||
const store = new Vuex.Store({ state, actions, mutations })
|
const store = new Vuex.Store({ state, actions, mutations })
|
||||||
const wrapper = mount(Schema, { store, localVue })
|
const wrapper = mount(Schema, { store, localVue })
|
||||||
sinon.spy(wrapper.vm.$refs.addCsv, 'previewCsv')
|
sinon.spy(wrapper.vm.$refs.addCsvJson, 'preview')
|
||||||
sinon.spy(wrapper.vm, 'addCsv')
|
sinon.spy(wrapper.vm, 'addCsvJson')
|
||||||
sinon.spy(wrapper.vm.$refs.addCsv, 'loadFromCsv')
|
sinon.spy(wrapper.vm.$refs.addCsvJson, 'loadToDb')
|
||||||
|
|
||||||
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
await wrapper.findComponent({ name: 'add-table-icon' }).find('svg').trigger('click')
|
||||||
await wrapper.vm.$refs.addCsv.previewCsv.returnValues[0]
|
await wrapper.vm.$refs.addCsvJson.preview.returnValues[0]
|
||||||
await wrapper.vm.addCsv.returnValues[0]
|
await wrapper.vm.addCsvJson.returnValues[0]
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
await wrapper.vm.$nextTick()
|
await wrapper.vm.$nextTick()
|
||||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(true)
|
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(true)
|
||||||
await wrapper.find('#csv-import').trigger('click')
|
await wrapper.find('#import-start').trigger('click')
|
||||||
await wrapper.vm.$refs.addCsv.loadFromCsv.returnValues[0]
|
await wrapper.vm.$refs.addCsvJson.loadToDb.returnValues[0]
|
||||||
await wrapper.find('#csv-finish').trigger('click')
|
await wrapper.find('#import-finish').trigger('click')
|
||||||
expect(wrapper.find('[data-modal="addCsv"]').exists()).to.equal(false)
|
expect(wrapper.find('[data-modal="addCsvJson"]').exists()).to.equal(false)
|
||||||
await state.db.refreshSchema.returnValues[0]
|
await state.db.refreshSchema.returnValues[0]
|
||||||
|
|
||||||
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
expect(wrapper.vm.$store.state.db.schema).to.eql([
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ describe('DataView.vue', () => {
|
|||||||
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
|
||||||
sinon.spy(pivot, 'saveAsSvg')
|
sinon.spy(pivot, 'saveAsSvg')
|
||||||
|
|
||||||
|
// Switch to Custom Chart renderer
|
||||||
|
pivot.pivotOptions.rendererName = 'Custom chart'
|
||||||
|
await pivot.$nextTick()
|
||||||
|
|
||||||
// Export to svg
|
// Export to svg
|
||||||
await svgBtn.trigger('click')
|
await svgBtn.trigger('click')
|
||||||
expect(pivot.saveAsSvg.calledOnce).to.equal(true)
|
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)
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user