1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

34 Commits

Author SHA1 Message Date
lana-k
3c456ef135 fix sql hint: read properties of undefined #99 2022-07-29 15:27:01 +02:00
lana-k
c713c713b7 fix paths #97 2022-07-20 23:15:14 +02:00
lana-k
babf0074c0 add artifact with source map #97 2022-07-20 22:49:26 +02:00
lana-k
e71e6700c1 improve events 2022-07-20 22:47:40 +02:00
lana-k
84e66b8167 Update README.md 2022-07-10 23:18:52 +02:00
lana-k
9e84cf269e Merge branch 'master' of github.com:lana-k/sqliteviz 2022-07-10 22:54:09 +02:00
lana-k
e897b4913b cancel deploying to github pages; remove banner 2022-07-10 22:53:23 +02:00
saaj
0646f58ca0 Build SQLite 3.39.0 (#95) 2022-07-10 22:46:39 +02:00
lana-k
c674bf11e3 update version number 2022-07-10 22:29:20 +02:00
lana-k
2d8a91675e Remove console.log 2022-07-10 22:25:11 +02:00
lana-k
45b1021559 links to website 2022-07-10 21:26:26 +02:00
lana-k
7216e996d1 add banner 2022-07-10 21:25:51 +02:00
lana-k
6eae9a0f2d remove empty lines 2022-07-10 16:02:04 +02:00
lana-k
7486b32bd1 add head extention slot 2022-07-03 15:38:41 +02:00
lana-k
2c564767f8 Merge branch 'master' of github.com:lana-k/sqliteviz 2022-07-01 21:01:07 +02:00
lana-k
289a727cbe fix icon width 2022-07-01 21:01:02 +02:00
saaj
5f2b8ba5a9 Upgrade to SQLite 3.38.5 (#91)
* Update to SQLite 3.37 and latest generate_series extension

* Expect column types in upper case

* Rebuild newer SQLite, 3.38.5
2022-06-27 17:35:40 +02:00
lana-k
f0a4212e2b fix tests 2022-06-26 21:06:24 +02:00
lana-k
c8deff32c1 fix lint 2022-06-25 22:38:22 +02:00
lana-k
d56604a7d6 events refactor 2022-06-25 22:37:09 +02:00
lana-k
48e311bff8 minor change 2022-06-24 22:58:43 +02:00
lana-k
518b22b489 events 2022-06-24 21:29:40 +02:00
lana-k
a20dd7f849 forbid index minifying 2022-06-11 19:29:42 +02:00
lana-k
310a939109 #89 add tests 2021-12-24 16:13:42 +01:00
lana-k
bb9ba08902 #89 remove head and body 2021-12-22 20:42:53 +01:00
lana-k
c7c727ff78 fix lint 2021-12-21 22:15:21 +01:00
lana-k
8669a6a9e5 #89 export to html plolty charts and pivots 2021-12-21 22:13:02 +01:00
lana-k
c1cc5bb95e getHtml for chart #89 2021-12-20 22:31:08 +01:00
lana-k
9c55e76a41 update version 2021-12-19 16:00:11 +01:00
lana-k
70a9edf57e fix lint 2021-12-19 15:57:30 +01:00
lana-k
b2c2344951 update version 2021-12-19 15:38:17 +01:00
lana-k
cbec91e78a Merge branch 'master' of github.com:lana-k/sqliteviz 2021-12-19 15:37:01 +01:00
lana-k
816b0e6218 show plotly warnings and errors #55 2021-12-19 15:36:46 +01:00
saaj
4ed93bbea7 Two more extensions and improved extension documentation (#86) 2021-09-10 20:11:37 +02:00
43 changed files with 15603 additions and 12075 deletions

View File

@@ -1,4 +1,4 @@
name: Deploy to GitHub Pages and create release name: Create release
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
@@ -7,7 +7,7 @@ on:
jobs: jobs:
deploy: deploy:
name: Deploy to GitHub Pages and create release name: Create release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -24,10 +24,11 @@ jobs:
npm install npm install
npm run build npm run build
- name: Create archive - name: Create archives
run: | run: |
cd dist cd dist
zip -9 -r dist.zip . -x "js/*.map" -x "/*.map" zip -9 -r ../dist.zip . -x "js/*.map" -x "/*.map"
zip -9 -r ../dist_map.zip .
- name: Create Release Notes - name: Create Release Notes
run: | run: |
@@ -39,16 +40,6 @@ jobs:
- name: Create release - name: Create release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "dist/dist.zip" artifacts: "dist.zip,dist_map.zip"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md" bodyFile: "CHANGELOG.md"
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch: build # The branch the action should deploy to.
folder: dist/ # The folder the action should deploy.
clean: true # Automatically remove deleted files from the deploy branch
clean-exclude: .nojekyll

View File

@@ -18,10 +18,10 @@ With sqliteviz you can:
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4 https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
## Quickstart ## Quickstart
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6]. The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki ## Wiki
For user documentation, check out sqliteviz [Wiki][7]. For user documentation, check out sqliteviz [documentation][7].
## Motivation ## Motivation
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2]. It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
@@ -34,8 +34,8 @@ It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4]
[3]: https://github.com/plotly/react-chart-editor [3]: https://github.com/plotly/react-chart-editor
[4]: https://github.com/sql-js/sql.js [4]: https://github.com/sql-js/sql.js
[5]: https://github.com/vuejs/vue [5]: https://github.com/vuejs/vue
[6]: https://lana-k.github.io/sqliteviz/ [6]: https://sqliteviz.com/app/
[7]: https://github.com/lana-k/sqliteviz/wiki [7]: https://sqliteviz.com/docs
[8]: https://github.com/surmon-china/vue-codemirror#readme [8]: https://github.com/surmon-china/vue-codemirror#readme
[9]: https://www.papaparse.com/ [9]: https://www.papaparse.com/
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries [10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries

View File

@@ -13,6 +13,21 @@ SQLite [amalgamation][2] extensions included:
1. [FTS5][4] -- virtual table module that provides full-text search 1. [FTS5][4] -- virtual table module that provides full-text search
functionality functionality
2. [FTS3/FTS4][15] -- older virtual table modules for full-text search
3. [JSON1][16] -- scalar, aggregate and table-valued functions for managing JSON data
SQLite [contribution extensions][17]:
1. [extension-functions][18] -- mathematical and string extension functions for SQL queries.
Math: `acos`, `asin`, `atan`, `atn2`, `atan2`, `acosh`, `asinh`, `atanh`, `difference`,
`degrees`, `radians`, `cos`, `sin`, `tan`, `cot`, `cosh`, `sinh`, `tanh`, `coth`,
`exp`, `log`, `log10`, `power`, `sign`, `sqrt`, `square`, `ceil`, `floor`, `pi`.
String: `replicate`, `charindex`, `leftstr`, `rightstr`, `ltrim`, `rtrim`, `trim`,
`replace`, `reverse`, `proper`, `padl`, `padr`, `padc`, `strfilter`.
Aggregate: `stdev`, `variance`, `mode`, `median`, `lower_quartile`, `upper_quartile`.
SQLite [miscellaneous extensions][3] included: SQLite [miscellaneous extensions][3] included:
@@ -21,6 +36,9 @@ SQLite [miscellaneous extensions][3] included:
[Querying Tree Structures in SQLite][11] ([closure.c][8]) [Querying Tree Structures in SQLite][11] ([closure.c][8])
3. `uuid`, `uuid_str` and `uuid_blob` RFC-4122 UUID functions ([uuid.c][9]) 3. `uuid`, `uuid_str` and `uuid_blob` RFC-4122 UUID functions ([uuid.c][9])
4. `regexp` (hence `REGEXP` operator) and `regexpi` functions ([regexp.c][10]) 4. `regexp` (hence `REGEXP` operator) and `regexpi` functions ([regexp.c][10])
5. `percentile` function ([percentile.c][13])
6. `decimal`, `decimal_cmp`, `decimal_add`, `decimal_sub` and `decimal_mul` functions
([decimal.c][14])
SQLite 3rd party extensions included: SQLite 3rd party extensions included:
@@ -29,6 +47,9 @@ SQLite 3rd party extensions included:
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.
Examples of queries involving these extensions can be found in the test suite in
[sqliteExtensions.spec.js][19].
## Build method ## Build method
Basically it's extended amalgamation and `SQLITE_EXTRA_INIT` concisely Basically it's extended amalgamation and `SQLITE_EXTRA_INIT` concisely
@@ -71,3 +92,10 @@ described in [this message from SQLite Forum][12]:
[10]: https://sqlite.org/src/file/ext/misc/regexp.c [10]: https://sqlite.org/src/file/ext/misc/regexp.c
[11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/ [11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/
[12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw [12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw
[13]: https://sqlite.org/src/file/ext/misc/percentile.c
[14]: https://sqlite.org/src/file/ext/misc/decimal.c
[15]: https://sqlite.org/fts3.html
[16]: https://sqlite.org/json1.html
[17]: https://sqlite.org/contrib/
[18]: https://sqlite.org/contrib//download/extension-functions.c?get=25
[19]: https://github.com/lana-k/sqliteviz/blob/master/tests/lib/database/sqliteExtensions.spec.js

View File

@@ -1,4 +1,4 @@
FROM node:12-buster FROM node:14-bullseye
RUN set -ex; \ RUN set -ex; \
echo 'deb http://deb.debian.org/debian unstable main' \ echo 'deb http://deb.debian.org/debian unstable main' \

View File

@@ -1,21 +1,44 @@
#!/bin/bash -e #!/bin/bash -e
cleanup () {
rm -rf lib/dist $flag_file
}
trap cleanup EXIT
if [ ! -f sample.csv ]; then if [ ! -f sample.csv ]; then
wget --header="accept-encoding: gzip" -q -O- \ wget --header="accept-encoding: gzip" -q -O- \
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \ https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
| gunzip -c > sample.csv | gunzip -c > sample.csv
fi fi
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
test_ni=$(nice -n -1 nice)
if [ $test_ni == -1 ]; then
flag_file=$(mktemp)
fi
(
while [ -f $flag_file ]; do
root_pid=$(
docker ps -f status=running -f name='^sqljs-benchmark-' -q \
| xargs -r -I{} -- docker inspect -f '{{.State.Pid}}' {}
)
if [ ! -z $root_pid ]; then
procpath query -d $'\n' "$..children[?(@.stat.pid == $root_pid)]..pid" \
| xargs -I{} -- renice -n -1 -p {} > /dev/null
fi
sleep 1
done &
)
shopt -s nullglob
for d in lib/build-* ; do for d in lib/build-* ; do
rm -r lib/dist || true rm -rf lib/dist
cp -r $d lib/dist cp -r $d lib/dist
name=$(basename $d) name=$(basename $d)
docker build -t sqliteviz/sqljs-benchmark:$name . docker build -t sqliteviz/sqljs-benchmark:$name .
docker rm sqljs-benchmark-$name 2> /dev/null || true docker rm sqljs-benchmark-$name 2> /dev/null || true
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name docker run -it --cpus 2 --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
docker rm sqljs-benchmark-$name docker rm sqljs-benchmark-$name
done done
rm -r lib/dist

View File

@@ -21,7 +21,7 @@
"metadata": {}, "metadata": {},
"outputs": [], "outputs": [],
"source": [ "source": [
"benchmark_path = Path('/path/to/sqliteviz/project/sqliteviz/lib/sql-js/benchmark')" "benchmark_path = Path()"
] ]
}, },
{ {

View File

@@ -8,7 +8,7 @@ from pathlib import Path
from urllib import request from urllib import request
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip' amalgamation_url = 'https://sqlite.org/2022/sqlite-amalgamation-3390000.zip'
# Extension-functions # Extension-functions
# =================== # ===================
@@ -20,10 +20,12 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
extension_urls = ( extension_urls = (
# Miscellaneous extensions # Miscellaneous extensions
# ======================== # ========================
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_init'), ('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'), ('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'), ('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'), ('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
# Third-party extension # Third-party extension
# ===================== # =====================
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'), ('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'),

File diff suppressed because one or more lines are too long

Binary file not shown.

26814
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.16.0", "version": "0.21.1",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {

View File

@@ -69,6 +69,8 @@
} }
} }
</style> </style>
<!-- head extention slot start -->
<!-- head extention slot end -->
</head> </head>
<body> <body>
<noscript> <noscript>
@@ -93,6 +95,8 @@
</svg> </svg>
</div> </div>
</div> </div>
<!-- extention slot start -->
<!-- extention slot end -->
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

@@ -0,0 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
</svg>

After

Width:  |  Height:  |  Size: 862 B

View File

@@ -112,6 +112,7 @@ import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs' import Logs from '@/components/Logs'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default { export default {
name: 'CsvImport', name: 'CsvImport',
@@ -335,6 +336,7 @@ export default {
this.$store.commit('setCurrentTabId', tabId) this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false this.importCsvCompleted = false
this.$emit('finish') this.$emit('finish')
events.send('inquiry.create', null, { auto: true })
} }
} }
} }

View File

@@ -58,6 +58,7 @@ import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb' import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database' import database from '@/lib/database'
import CsvImport from '@/components/CsvImport' import CsvImport from '@/components/CsvImport'
import events from '@/lib/utils/events'
export default { export default {
name: 'DbUploader', name: 'DbUploader',
@@ -127,6 +128,11 @@ export default {
if (fIo.isDatabase(file)) { if (fIo.isDatabase(file)) {
this.loadDb(file) this.loadDb(file)
} else { } else {
events.send('database.import', file.size, {
from: 'csv',
new_db: true
})
this.file = file this.file = file
await this.$nextTick() await this.$nextTick()
const csvImport = this.$refs.addCsv const csvImport = this.$refs.addCsv

View File

@@ -0,0 +1,49 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5.1626 10.0745L7.56641 10.8831V12.2322L3.68164 10.6501V9.4812L7.56641
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
12.2366V10.8918L14.1099 10.0613Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
1.34541 0.0637207 2.17041 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 0.0637207H15.3052V1.56372H17.1704V9.84163H18.6704V1.56372C18.6704 0.73872
17.9954 0.0637207 17.1704 0.0637207Z"
fill="#A2B1C6"
/>
<path
d="M2.17041 17.1098H15.8754V15.6098H2.17041V8.78486H0.67041V15.6098C0.67041 16.4348
1.34541 17.1098 2.17041 17.1098Z"
fill="#A2B1C6"
/>
<path
d="M17.1704 17.1098H15.3052V15.6098H17.1704V8.55939H18.6704V15.6098C18.6704 16.4348
17.9954 17.1098 17.1704 17.1098Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M18.1197 4.13787H1.76172V3.03787H18.1197V4.13787Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'HtmlIcon'
}
</script>

View File

@@ -1,5 +1,6 @@
import dereference from 'react-chart-editor/lib/lib/dereference' import dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js' import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) { export function getOptionsFromDataSources (dataSources) {
if (!dataSources) { if (!dataSources) {
@@ -33,8 +34,43 @@ export async function getImageDataUrl (element, type) {
}) })
} }
export function getChartData (element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
layout: chartElement.layout
}
}
export function getHtml (options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
<div id="${chartId}"></div>
<script>
const el = document.getElementById("${chartId}")
let timeout
function debounceResize() {
clearTimeout(timeout)
timeout = setTimeout(() => {
var r = el.getBoundingClientRect()
Plotly.relayout(el, {width: r.width, height: r.height})
}, 200)
}
const resizeObserver = new ResizeObserver(debounceResize)
resizeObserver.observe(el)
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
</script>
`
}
export default { export default {
getOptionsFromDataSources, getOptionsFromDataSources,
getOptionsForSave, getOptionsForSave,
getImageDataUrl getImageDataUrl,
getHtml,
getChartData
} }

View File

@@ -7,6 +7,8 @@ import Worker from './_worker.js'
// https://github.com/nolanlawson/promise-worker // https://github.com/nolanlawson/promise-worker
import PromiseWorker from 'promise-worker' import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events'
function getNewDatabase () { function getNewDatabase () {
const worker = new Worker() const worker = new Worker()
return new Database(worker) return new Database(worker)
@@ -76,6 +78,11 @@ class Database {
this.dbName = file ? fu.getFileName(file) : 'database' this.dbName = file ? fu.getFileName(file) : 'database'
this.refreshSchema() this.refreshSchema()
events.send('database.import', file ? file.size : 0, {
from: file ? 'sqlite' : 'none',
new_db: true
})
} }
async refreshSchema () { async refreshSchema () {
@@ -114,6 +121,7 @@ class Database {
throw new Error(data.error) throw new Error(data.error)
} }
fu.exportToFile(data, fileName) fu.exportToFile(data, fileName)
events.send('database.export', data.byteLength, { to: 'sqlite' })
} }
async validateTableName (name) { async validateTableName (name) {

View File

@@ -1,5 +1,6 @@
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
import fu from '@/lib/utils/fileIo' import fu from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import migration from './_migrations' import migration from './_migrations'
const migrate = migration._migrate const migrate = migration._migrate
@@ -103,9 +104,19 @@ export default {
importInquiries () { importInquiries () {
return fu.importFile() return fu.importFile()
.then(str => { .then(str => {
return this.deserialiseInquiries(str) const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
return inquires
}) })
}, },
export (inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length)
},
async readPredefinedInquiries () { async readPredefinedInquiries () {
const res = await fu.readFile('./inquiries.json') const res = await fu.readFile('./inquiries.json')

12
src/lib/utils/events.js Normal file
View File

@@ -0,0 +1,12 @@
export default {
send (name, value, labels) {
const event = new CustomEvent('sqliteviz-app-event', {
detail: {
name,
value,
labels: labels || {}
}
})
window.dispatchEvent(event)
}
}

View File

@@ -1,3 +1,4 @@
import events from '@/lib/utils/events'
let refresh = false let refresh = false
function invokeServiceWorkerUpdateFlow (registration) { function invokeServiceWorkerUpdateFlow (registration) {
@@ -41,4 +42,8 @@ if ('serviceWorker' in navigator) {
} }
}) })
}) })
window.addEventListener('appinstalled', () => {
events.send('pwa.install')
})
} }

View File

@@ -62,6 +62,7 @@ export default {
<style scoped> <style scoped>
#app-info-icon { #app-info-icon {
cursor: pointer; cursor: pointer;
width: 24px;
} }
#app-info-container { #app-info-container {
display: flex; display: flex;

View File

@@ -159,7 +159,6 @@ import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/CheckBox'
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import fu from '@/lib/utils/fileIo'
export default { export default {
name: 'Inquiries', name: 'Inquiries',
@@ -385,8 +384,7 @@ export default {
return this.$store.state.tabs.findIndex(tab => tab.id === id) return this.$store.state.tabs.findIndex(tab => tab.id === id)
}, },
exportToFile (inquiryList, fileName) { exportToFile (inquiryList, fileName) {
const jsonStr = storedInquiries.serialiseInquiries(inquiryList) storedInquiries.export(inquiryList, fileName)
fu.exportToFile(jsonStr, fileName)
}, },
exportSelectedInquiries () { exportSelectedInquiries () {
const inquiryList = this.allInquiries.filter( const inquiryList = this.allInquiries.filter(

View File

@@ -1,9 +1,12 @@
<template> <template>
<nav> <nav>
<div> <div id="nav-links">
<a href="https://sqliteviz.com">
<img :src="require('@/assets/images/logo_simple.svg')">
</a>
<router-link to="/workspace">Workspace</router-link> <router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link> <router-link to="/inquiries">Inquiries</router-link>
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a> <a href="https://sqliteviz.com/docs" target="_blank">Help</a>
</div> </div>
<div id="nav-buttons"> <div id="nav-buttons">
<button <button
@@ -57,6 +60,7 @@ import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo' import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events'
export default { export default {
name: 'MainMenu', name: 'MainMenu',
@@ -110,6 +114,8 @@ export default {
this.$router.push('/workspace') this.$router.push('/workspace')
} }
}) })
events.send('inquiry.create', null, { auto: false })
}, },
cancelSave () { cancelSave () {
this.$modal.hide('save') this.$modal.hide('save')
@@ -163,6 +169,7 @@ export default {
// Signal about saving // Signal about saving
this.$root.$emit('inquirySaved') this.$root.$emit('inquirySaved')
events.send('inquiry.save')
}, },
_keyListener (e) { _keyListener (e) {
if (this.$route.path === '/workspace') { if (this.$route.path === '/workspace') {
@@ -237,4 +244,12 @@ button {
#nav-buttons { #nav-buttons {
display: flex; display: flex;
} }
#nav-links {
display: flex;
align-items: center;
}
#nav-links img {
width: 32px;
}
</style> </style>

View File

@@ -33,6 +33,7 @@
<script> <script>
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
import TableDescription from './TableDescription' import TableDescription from './TableDescription'
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import TreeChevron from '@/components/svg/treeChevron' import TreeChevron from '@/components/svg/treeChevron'
@@ -86,6 +87,11 @@ export default {
csvImport.reset() csvImport.reset()
await csvImport.previewCsv() await csvImport.previewCsv()
csvImport.open() csvImport.open()
events.send('database.import', this.file.size, {
from: 'csv',
new_db: false
})
} }
} }
} }

View File

@@ -31,10 +31,15 @@ import PlotlyEditor from 'react-chart-editor'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
import dereference from 'react-chart-editor/lib/lib/dereference' import dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default { export default {
name: 'Chart', name: 'Chart',
props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'], props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'forPivot'
],
components: { components: {
PlotlyEditor PlotlyEditor
}, },
@@ -55,6 +60,23 @@ export default {
return chartHelper.getOptionsFromDataSources(this.dataSources) return chartHelper.getOptionsFromDataSources(this.dataSources)
} }
}, },
created () {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() => this.state.data.map(trace => `${trace.type}-${trace.mode}`)
.join(','),
(value) => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
})
},
{ deep: true }
)
},
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer) this.resizeObserver.observe(this.$refs.chartContainer)
@@ -99,6 +121,13 @@ export default {
fIo.downloadFromUrl(url, 'chart') fIo.downloadFromUrl(url, 'chart')
}, },
saveAsHtml () {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') { async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type) return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
} }

View File

@@ -19,10 +19,11 @@ import $ from 'jquery'
import 'pivottable' import 'pivottable'
import 'pivottable/dist/pivot.css' import 'pivottable/dist/pivot.css'
import PivotUi from './PivotUi' import PivotUi from './PivotUi'
import { getPivotCanvas } from './pivotHelper' import pivotHelper from './pivotHelper'
import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart' import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart'
import chartHelper from '@/lib/chartHelper' import chartHelper from '@/lib/chartHelper'
import Vue from 'vue' import Vue from 'vue'
import events from '@/lib/utils/events'
const ChartClass = Vue.extend(Chart) const ChartClass = Vue.extend(Chart)
export default { export default {
@@ -87,8 +88,17 @@ export default {
'pivotOptions.rendererName': { 'pivotOptions.rendererName': {
immediate: true, immediate: true,
handler () { handler () {
this.$emit('update:importToPngEnabled', this.pivotOptions.rendererName !== 'TSV Export') this.$emit(
this.$emit('update:importToSvgEnabled', this.viewStandartChart || this.viewCustomChart) 'update:importToPngEnabled',
this.pivotOptions.rendererName !== 'TSV Export'
)
this.$emit(
'update:importToSvgEnabled',
this.viewStandartChart || this.viewCustomChart
)
events.send('viz_pivot.render', null, {
type: this.pivotOptions.rendererName
})
} }
}, },
pivotOptions () { pivotOptions () {
@@ -169,7 +179,7 @@ export default {
} else { } else {
const source = this.viewStandartChart const source = this.viewStandartChart
? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png') ? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (await getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png') : (await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
this.$emit('loadingImageCompleted') this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(source, 'pivot') fIo.downloadFromUrl(source, 'pivot')
@@ -179,11 +189,11 @@ export default {
async prepareCopy () { async prepareCopy () {
if (this.viewCustomChart) { if (this.viewCustomChart) {
return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy() return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy()
} else if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
} else {
return await getPivotCanvas(this.$refs.pivotOutput)
} }
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
}, },
async saveAsSvg () { async saveAsSvg () {
@@ -193,6 +203,28 @@ export default {
const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg') const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg')
fIo.downloadFromUrl(url, 'pivot') fIo.downloadFromUrl(url, 'pivot')
} }
},
saveAsHtml () {
if (this.viewCustomChart) {
this.pivotOptions.rendererOptions.customChartComponent.saveAsHtml()
return
}
if (this.viewStandartChart) {
const chartState = chartHelper.getChartData(this.$refs.pivotOutput)
fIo.exportToFile(
chartHelper.getHtml(chartState),
'chart.html',
'text/html'
)
return
}
fIo.exportToFile(
pivotHelper.getPivotHtml(this.$refs.pivotOutput),
'pivot.html',
'text/html'
)
} }
} }
} }

View File

@@ -51,6 +51,8 @@ export function _getDataSources (pivotData) {
function customChartRenderer (data, options) { function customChartRenderer (data, options) {
options.customChartComponent.dataSources = _getDataSources(data) options.customChartComponent.dataSources = _getDataSources(data)
options.customChartComponent.forPivot = true
options.customChartComponent.$mount() options.customChartComponent.$mount()
return $(options.customChartComponent.$el) return $(options.customChartComponent.$el)
@@ -81,3 +83,40 @@ export async function getPivotCanvas (pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable') const tableElement = pivotOutput.querySelector('.pvtTable')
return await html2canvas(tableElement, { logging: false }) return await html2canvas(tableElement, { logging: false })
} }
export function getPivotHtml (pivotOutput) {
return `
<style>
table.pvtTable {
font-family: Arial, sans-serif;
font-size: 12px;
text-align: left;
border-collapse: collapse;
min-width: 100%;
}
table.pvtTable .pvtColLabel {
text-align: center;
}
table.pvtTable .pvtTotalLabel {
text-align: right;
}
table.pvtTable tbody tr td {
color: #506784;
border: 1px solid #DFE8F3;
text-align: right;
}
table.pvtTable thead tr th,
table.pvtTable tbody tr th {
background-color: #506784;
color: #fff;
border: 1px solid #DFE8F3;
}
</style>
${pivotOutput.outerHTML}
`
}
export default {
getPivotCanvas,
getPivotHtml
}

View File

@@ -51,6 +51,13 @@
<export-to-svg-icon /> <export-to-svg-icon />
</icon-button> </icon-button>
<icon-button
tooltip="Save as HTML"
tooltip-position="top-left"
@click="saveAsHtml"
>
<HtmlIcon />
</icon-button>
<icon-button <icon-button
:loading="copyingImage" :loading="copyingImage"
tooltip="Copy visualisation to clipboard" tooltip="Copy visualisation to clipboard"
@@ -81,12 +88,14 @@ import SideToolBar from '../SideToolBar'
import IconButton from '@/components/IconButton' import IconButton from '@/components/IconButton'
import ChartIcon from '@/components/svg/chart' import ChartIcon from '@/components/svg/chart'
import PivotIcon from '@/components/svg/pivot' import PivotIcon from '@/components/svg/pivot'
import HtmlIcon from '@/components/svg/html'
import ExportToSvgIcon from '@/components/svg/exportToSvg' import ExportToSvgIcon from '@/components/svg/exportToSvg'
import PngIcon from '@/components/svg/png' import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard' import ClipboardIcon from '@/components/svg/clipboard'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog' import loadingDialog from '@/components/LoadingDialog'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default { export default {
name: 'DataView', name: 'DataView',
@@ -100,6 +109,7 @@ export default {
PivotIcon, PivotIcon,
ExportToSvgIcon, ExportToSvgIcon,
PngIcon, PngIcon,
HtmlIcon,
ClipboardIcon, ClipboardIcon,
loadingDialog loadingDialog
}, },
@@ -114,6 +124,11 @@ export default {
dataToCopy: null dataToCopy: null
} }
}, },
computed: {
plotlyInPivot () {
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
}
},
watch: { watch: {
mode () { mode () {
this.$emit('update') this.$emit('update')
@@ -139,6 +154,7 @@ export default {
*/ */
await time.sleep(0) await time.sleep(0)
this.$refs.viewComponent.saveAsPng() this.$refs.viewComponent.saveAsPng()
this.exportSignal('png')
}, },
getOptionsForSave () { getOptionsForSave () {
return this.$refs.viewComponent.getOptionsForSave() return this.$refs.viewComponent.getOptionsForSave()
@@ -169,6 +185,7 @@ export default {
async copyToClipboard () { async copyToClipboard () {
cIo.copyImage(this.dataToCopy) cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy') this.$modal.hide('prepareCopy')
this.exportSignal('clipboard')
}, },
cancelCopy () { cancelCopy () {
this.dataToCopy = null this.dataToCopy = null
@@ -177,6 +194,26 @@ export default {
saveAsSvg () { saveAsSvg () {
this.$refs.viewComponent.saveAsSvg() this.$refs.viewComponent.saveAsSvg()
this.exportSignal('svg')
},
saveAsHtml () {
this.$refs.viewComponent.saveAsHtml()
this.exportSignal('html')
},
exportSignal (to) {
const eventLabels = { type: to }
if (this.mode === 'chart' || this.plotlyInPivot) {
eventLabels.pivot = this.plotlyInPivot
}
events.send(
this.mode === 'chart' || this.plotlyInPivot
? 'viz_plotly.export'
: 'viz_pivot.export',
null,
eventLabels
)
} }
} }
} }

View File

@@ -72,6 +72,7 @@ import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo' import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog' import loadingDialog from '@/components/LoadingDialog'
import events from '@/lib/utils/events'
export default { export default {
name: 'RunResult', name: 'RunResult',
@@ -117,10 +118,24 @@ export default {
}, },
exportToCsv () { exportToCsv () {
if (this.result && this.result.values) {
events.send('resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'csv' }
)
}
fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv') fIo.exportToFile(csv.serialize(this.result), 'result_set.csv', 'text/csv')
}, },
async prepareCopy () { async prepareCopy () {
if (this.result && this.result.values) {
events.send('resultset.export',
this.result.values[this.result.columns[0]].length,
{ to: 'clipboard' }
)
}
if ('ClipboardItem' in window) { if ('ClipboardItem' in window) {
this.preparingCopy = true this.preparingCopy = true
this.$modal.show('prepareCSVCopy') this.$modal.show('prepareCSVCopy')

View File

@@ -3,14 +3,20 @@ import 'codemirror/addon/hint/show-hint.js'
import 'codemirror/addon/hint/sql-hint.js' import 'codemirror/addon/hint/sql-hint.js'
import store from '@/store' import store from '@/store'
function _getHintText (hint) {
return typeof hint === 'string' ? hint : hint.text
}
export function getHints (cm, options) { export function getHints (cm, options) {
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
const result = CM.hint.sql(cm, options) const result = CM.hint.sql(cm, options)
// Don't show the hint if there is only one option // Don't show the hint if there is only one option
// and the token is already completed with this option // and the replacingText is already equals to this option
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) { const replacedText = cm.getRange(result.from, result.to).toUpperCase()
if (result.list.length === 1 &&
_getHintText(result.list[0]).toUpperCase() === replacedText) {
result.list = [] result.list = []
} }
return result return result
} }

View File

@@ -56,6 +56,7 @@ import DataView from './DataView'
import RunResult from './RunResult' import RunResult from './RunResult'
import time from '@/lib/utils/time' import time from '@/lib/utils/time'
import Teleport from 'vue2-teleport' import Teleport from 'vue2-teleport'
import events from '@/lib/utils/events'
export default { export default {
name: 'Tab', name: 'Tab',
@@ -108,6 +109,8 @@ export default {
const fromPosition = this.layout[from] const fromPosition = this.layout[from]
this.layout[from] = this.layout[to] this.layout[from] = this.layout[to]
this.layout[to] = fromPosition this.layout[to] = fromPosition
events.send('inquiry.panel', null, { panel: to })
}, },
onDataViewUpdate () { onDataViewUpdate () {
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false }) this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false })
@@ -121,11 +124,21 @@ export default {
const start = new Date() const start = new Date()
this.result = await state.db.execute(this.query + ';') this.result = await state.db.execute(this.query + ';')
this.time = time.getPeriod(start, new Date()) this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
this.result.values[this.result.columns[0]].length
)
}
events.send('query.run', parseFloat(this.time), { status: 'success' })
} catch (err) { } catch (err) {
this.error = { this.error = {
type: 'error', type: 'error',
message: err message: err
} }
events.send('query.run', 0, { status: 'error' })
} }
state.db.refreshSchema() state.db.refreshSchema()
this.isGettingResults = false this.isGettingResults = false

View File

@@ -19,6 +19,7 @@
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema' import Schema from './Schema'
import Tabs from './Tabs' import Tabs from './Tabs'
import events from '@/lib/utils/events'
export default { export default {
name: 'Workspace', name: 'Workspace',
@@ -49,6 +50,8 @@ export default {
const tabId = await this.$store.dispatch('addTab', { query: stmt }) const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId) this.$store.commit('setCurrentTabId', tabId)
events.send('inquiry.create', null, { auto: true })
} }
} }
} }

View File

@@ -55,6 +55,7 @@ describe('Logs.vue', () => {
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
messages.push({ type: 'error', message: 'msg 5' }) messages.push({ type: 'error', message: 'msg 5' })
await wrapper.vm.$nextTick()
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
const height = wrapper.find('.logs-container').element.scrollHeight const height = wrapper.find('.logs-container').element.scrollHeight
expect(wrapper.find('.logs-container').element.scrollTop) expect(wrapper.find('.logs-container').element.scrollTop)

View File

@@ -64,7 +64,40 @@ describe('chartHelper.js', () => {
expect(/^data:image\/png/.test(url)).to.equal(true) expect(/^data:image\/png/.test(url)).to.equal(true)
url = await chartHelper.getImageDataUrl(element, 'svg') url = await chartHelper.getImageDataUrl(element, 'svg')
console.log()
expect(/^data:image\/svg\+xml/.test(url)).to.equal(true) expect(/^data:image\/svg\+xml/.test(url)).to.equal(true)
}) })
it('getChartData returns plotly data and layout from element', async () => {
const element = document.createElement('div')
const child = document.createElement('div')
element.append(child)
child.classList.add('js-plotly-plot')
child.data = 'plotly data'
child.layout = 'plotly layout'
const chartData = chartHelper.getChartData(element)
expect(chartData).to.eql({
data: 'plotly data',
layout: 'plotly layout'
})
})
it('getHtml returns valid html', async () => {
const options = {
data: 'plotly data',
layout: 'plotly layout'
}
const html = chartHelper.getHtml(options)
const doc = document.createElement('div')
doc.innerHTML = html
expect(doc.innerHTML).to.equal(html)
expect(doc.children).to.have.lengthOf(3)
expect(doc.children[0].src).to.includes('plotly-latest.js')
expect(doc.children[1].id).to.have.lengthOf(21)
expect(doc.children[2].innerHTML).to.includes(doc.children[1].id)
expect(doc.children[2].innerHTML)
.to.includes('Plotly.newPlot(el, "plotly data", "plotly layout"')
})
}) })

View File

@@ -44,7 +44,7 @@ describe('database.js', () => {
expect(schema[0].columns[0].type).to.equal('N/A') expect(schema[0].columns[0].type).to.equal('N/A')
expect(schema[0].columns[1].name).to.equal('col2') expect(schema[0].columns[1].name).to.equal('col2')
expect(schema[0].columns[1].type).to.equal('integer') expect(schema[0].columns[1].type).to.equal('INTEGER')
}) })
it('creates schema with view', async () => { it('creates schema with view', async () => {
@@ -68,12 +68,12 @@ describe('database.js', () => {
expect(schema[0].columns[1]).to.eql({ expect(schema[0].columns[1]).to.eql({
name: 'col2', name: 'col2',
type: 'integer' type: 'INTEGER'
}) })
expect(schema[1].columns).to.eql([{ expect(schema[1].columns).to.eql([{
name: 'amount', name: 'amount',
type: 'integer' type: 'INTEGER'
}]) }])
}) })

View File

@@ -269,6 +269,48 @@ describe('SQLite extensions', function () {
}) })
}) })
it('supports percentile', async function () {
const actual = await db.execute(`
CREATE TABLE s(x INTEGER);
INSERT INTO s VALUES (15), (20), (35), (40), (50);
SELECT
percentile(x, 5) p5,
percentile(x, 30) p30,
percentile(x, 40) p40,
percentile(x, 50) p50,
percentile(x, 100) p100
FROM s;
`)
expect(actual.values).to.eql({
p5: [16],
p30: [23],
p40: [29],
p50: [35],
p100: [50]
})
})
it('supports decimal', async function () {
const actual = await db.execute(`
select
decimal_add(decimal('0.1'), decimal('0.2')) "add",
decimal_sub(0.2, 0.1) sub,
decimal_mul(power(2, 69), 2) mul,
decimal_cmp(decimal('0.1'), 0.1) cmp_e,
decimal_cmp(decimal('0.1'), decimal('0.099999')) cmp_g,
decimal_cmp(decimal('0.199999'), decimal('0.2')) cmp_l
`)
expect(actual.values).to.eql({
add: ['0.3'],
sub: ['0.1'],
mul: ['1180591620717412000000'],
cmp_e: [0],
cmp_g: [1],
cmp_l: [-1]
})
})
it('supports FTS5', async function () { it('supports FTS5', async function () {
const actual = await db.execute(` const actual = await db.execute(`
CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize = 'porter ascii'); CREATE VIRTUAL TABLE email USING fts5(sender, title, body, tokenize = 'porter ascii');
@@ -296,4 +338,96 @@ describe('SQLite extensions', function () {
sender: ['bar@localhost'] sender: ['bar@localhost']
}) })
}) })
it('supports FTS3', async function () {
const actual = await db.execute(`
CREATE VIRTUAL TABLE email USING fts3(sender, title, body, tokenize = 'porter');
INSERT INTO email VALUES
(
'foo@localhost',
'fts3/4',
'FTS3 and FTS4 are SQLite virtual table modules that allows users to perform '
|| 'full-text searches on a set of documents.'
),
(
'bar@localhost',
'fts4',
'FTS5 is an SQLite virtual table module that provides full-text search '
|| 'functionality to database applications.'
);
SELECT sender
FROM email
WHERE body MATCH '("full-text" NOT document AND (functionality OR table))';
`)
expect(actual.values).to.eql({
sender: ['bar@localhost']
})
})
it('supports FTS4', async function () {
const actual = await db.execute(`
CREATE VIRTUAL TABLE email USING fts4(
sender, title, body, notindexed=sender, tokenize='simple'
);
INSERT INTO email VALUES
(
'foo@localhost',
'fts3/4',
'FTS3 and FTS4 are SQLite virtual table modules that allows users to perform '
|| 'full-text searches on a set of documents.'
),
(
'bar@localhost',
'fts4',
'FTS5 is an SQLite virtual table module that provides full-text search '
|| 'functionality to database applications.'
);
SELECT sender
FROM email
WHERE body MATCH '("full-text" NOT document AND (functionality OR table NOT modules))';
`)
expect(actual.values).to.eql({
sender: ['bar@localhost']
})
})
it('supports JSON1', async function () {
const actual = await db.execute(`
WITH input(filename) AS (
VALUES
('/etc/redis/redis.conf'),
('/run/redis/redis-server.pid'),
('/var/log/redis-server.log')
), tmp AS (
SELECT
filename,
'["' || replace(filename, '/', '", "') || '"]' as filename_array
FROM input
)
SELECT (
SELECT group_concat(ip.value, '/')
FROM json_each(filename_array) ip
WHERE ip.id <= p.id
) AS path
FROM tmp, json_each(filename_array) AS p
WHERE p.id > 1 -- because the filenames start with the separator
`)
expect(actual.values).to.eql({
path: [
'/etc',
'/etc/redis',
'/etc/redis/redis.conf',
'/run',
'/run/redis',
'/run/redis/redis-server.pid',
'/var',
'/var/log',
'/var/log/redis-server.log'
]
})
})
}) })

View File

@@ -29,12 +29,12 @@ describe('time.js', () => {
}) })
it('sleep resolves after n ms', async () => { it('sleep resolves after n ms', async () => {
let before = performance.now() let before = Date.now()
await time.sleep(10) await time.sleep(10)
expect(performance.now() - before).to.be.least(10) expect(Date.now() - before).to.be.least(10)
before = performance.now() before = Date.now()
await time.sleep(30) await time.sleep(30)
expect(performance.now() - before).to.be.least(30) expect(Date.now() - before).to.be.least(30)
}) })
}) })

View File

@@ -59,6 +59,31 @@ describe('DataView.vue', () => {
expect(pivot.saveAsSvg.calledOnce).to.equal(true) expect(pivot.saveAsSvg.calledOnce).to.equal(true)
}) })
it('method saveAsHtml calls the same method of the current view component', async () => {
const wrapper = mount(DataView)
// Find chart and spy the method
const chart = wrapper.findComponent({ name: 'Chart' }).vm
sinon.spy(chart, 'saveAsHtml')
// Export to html
const htmlBtn = createWrapper(wrapper.findComponent({ name: 'htmlIcon' }).vm.$parent)
await htmlBtn.trigger('click')
expect(chart.saveAsHtml.calledOnce).to.equal(true)
// Switch to pivot
const pivotBtn = createWrapper(wrapper.findComponent({ name: 'pivotIcon' }).vm.$parent)
await pivotBtn.trigger('click')
// Find pivot and spy the method
const pivot = wrapper.findComponent({ name: 'pivot' }).vm
sinon.spy(pivot, 'saveAsHtml')
// Export to svg
await htmlBtn.trigger('click')
expect(pivot.saveAsHtml.calledOnce).to.equal(true)
})
it('shows alert when ClipboardItem is not supported', async () => { it('shows alert when ClipboardItem is not supported', async () => {
const ClipboardItem = window.ClipboardItem const ClipboardItem = window.ClipboardItem
delete window.ClipboardItem delete window.ClipboardItem

View File

@@ -5,6 +5,7 @@ import chartHelper from '@/lib/chartHelper'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' import $ from 'jquery'
import sinon from 'sinon' import sinon from 'sinon'
import pivotHelper from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
describe('Pivot.vue', () => { describe('Pivot.vue', () => {
let container let container
@@ -271,6 +272,41 @@ describe('Pivot.vue', () => {
expect(chartComponent.saveAsSvg.called).to.equal(true) expect(chartComponent.saveAsSvg.called).to.equal(true)
}) })
it('saveAsHtml calls chart method if renderer is Custom Chart', async () => {
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Custom chart'],
rendererName: 'Custom chart',
rendererOptions: {
customChartOptions: {
data: [],
layout: {},
frames: []
}
}
}
},
attachTo: container
})
const chartComponent = wrapper.vm.pivotOptions.rendererOptions.customChartComponent
sinon.stub(chartComponent, 'saveAsHtml')
await wrapper.vm.saveAsHtml()
expect(chartComponent.saveAsHtml.called).to.equal(true)
})
it('saveAsPng calls chart method if renderer is Custom Chart', async () => { it('saveAsPng calls chart method if renderer is Custom Chart', async () => {
const wrapper = mount(Pivot, { const wrapper = mount(Pivot, {
propsData: { propsData: {
@@ -333,6 +369,66 @@ describe('Pivot.vue', () => {
expect(chartHelper.getImageDataUrl.calledOnce).to.equal(true) expect(chartHelper.getImageDataUrl.calledOnce).to.equal(true)
}) })
it('saveAsHtml - standart chart', async () => {
sinon.spy(chartHelper, 'getChartData')
sinon.spy(chartHelper, 'getHtml')
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers['Bar Chart'],
rendererName: 'Bar Chart'
}
},
attachTo: container
})
await wrapper.vm.saveAsHtml()
expect(chartHelper.getChartData.calledOnce).to.equal(true)
const chartData = await chartHelper.getChartData.returnValues[0]
expect(chartHelper.getHtml.calledOnceWith(chartData)).to.equal(true)
})
it('saveAsHtml - table', async () => {
sinon.stub(pivotHelper, 'getPivotHtml')
sinon.stub(fIo, 'exportToFile')
const wrapper = mount(Pivot, {
propsData: {
dataSources: {
item: ['foo', 'bar', 'bar', 'bar'],
year: [2021, 2021, 2020, 2020]
},
initOptions: {
rows: ['item'],
cols: ['year'],
colOrder: 'key_a_to_z',
rowOrder: 'key_a_to_z',
aggregatorName: 'Count',
vals: [],
renderer: $.pivotUtilities.renderers.Table,
rendererName: 'Table'
}
},
attachTo: container
})
await wrapper.vm.saveAsHtml()
expect(pivotHelper.getPivotHtml.calledOnce).to.equal(true)
const html = pivotHelper.getPivotHtml.returnValues[0]
expect(fIo.exportToFile.calledOnceWith(html, 'pivot.html', 'text/html')).to.equal(true)
})
it('saveAsPng - standart chart', async () => { it('saveAsPng - standart chart', async () => {
sinon.stub(chartHelper, 'getImageDataUrl').returns('standat chart data url') sinon.stub(chartHelper, 'getImageDataUrl').returns('standat chart data url')
sinon.stub(fIo, 'downloadFromUrl') sinon.stub(fIo, 'downloadFromUrl')
@@ -365,6 +461,7 @@ describe('Pivot.vue', () => {
}) })
it('saveAsPng - table', async () => { it('saveAsPng - table', async () => {
sinon.stub(pivotHelper, 'getPivotCanvas').returns(document.createElement('canvas'))
sinon.stub(HTMLCanvasElement.prototype, 'toDataURL').returns('canvas data url') sinon.stub(HTMLCanvasElement.prototype, 'toDataURL').returns('canvas data url')
sinon.stub(fIo, 'downloadFromUrl') sinon.stub(fIo, 'downloadFromUrl')

View File

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import { _getDataSources, getPivotCanvas } import { _getDataSources, getPivotCanvas, getPivotHtml }
from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper' from '@/views/Main/Workspace/Tabs/Tab/DataView/Pivot/pivotHelper'
describe('pivotHelper.js', () => { describe('pivotHelper.js', () => {
it('_getDataSources returns data sources', () => { it('_getDataSources returns data sources', () => {
@@ -63,4 +63,19 @@ describe('pivotHelper.js', () => {
expect(await getPivotCanvas(pivotOutput)).to.be.instanceof(HTMLCanvasElement) expect(await getPivotCanvas(pivotOutput)).to.be.instanceof(HTMLCanvasElement)
}) })
it('getPivotHtml returns html with styles', async () => {
const pivotOutput = document.createElement('div')
pivotOutput.append('test')
const html = getPivotHtml(pivotOutput)
const doc = document.createElement('div')
doc.innerHTML = html
expect(doc.innerHTML).to.equal(html)
expect(doc.children).to.have.lengthOf(2)
expect(doc.children[0].tagName).to.equal('STYLE')
expect(doc.children[1].tagName).to.equal('DIV')
expect(doc.children[1].innerHTML).to.equal('test')
})
}) })

View File

@@ -138,15 +138,37 @@ describe('hint.js', () => {
'getHints returns [ ] if there is only one option and token is completed with this option', 'getHints returns [ ] if there is only one option and token is completed with this option',
() => { () => {
// mock CM.hint.sql and editor // mock CM.hint.sql and editor
sinon.stub(CM.hint, 'sql').returns({ list: [{ text: 'SELECT' }] }) sinon.stub(CM.hint, 'sql').returns({
list: [{ text: 'SELECT' }],
from: null, // from/to doesn't metter because getRange is mocked
to: null
})
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'select'
string: 'select', }
type: 'keyword' }
}
}, const hints = getHints(editor, {})
getCursor: sinon.stub() expect(hints.list).to.eql([])
}
)
it(
'getHints returns [ ] if there is only one string option and token ' +
'is completed with this option',
() => {
// mock CM.hint.sql and editor
sinon.stub(CM.hint, 'sql').returns({
list: ['house.name'],
from: null, // from/to doesn't metter because getRange is mocked
to: null
})
const editor = {
getRange () {
return 'house.name'
}
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})
@@ -160,15 +182,11 @@ describe('hint.js', () => {
{ text: 'SELECT' }, { text: 'SELECT' },
{ text: 'ST' } { text: 'ST' }
] ]
sinon.stub(CM.hint, 'sql').returns({ list }) sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'se'
string: 'se', }
type: 'keyword'
}
},
getCursor: sinon.stub()
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})
@@ -182,15 +200,11 @@ describe('hint.js', () => {
() => { () => {
// mock CM.hint.sql and editor // mock CM.hint.sql and editor
const list = [{ text: 'SELECT' }] const list = [{ text: 'SELECT' }]
sinon.stub(CM.hint, 'sql').returns({ list }) sinon.stub(CM.hint, 'sql').returns({ list, from: null, to: null })
const editor = { const editor = {
getTokenAt () { getRange () {
return { return 'sele'
string: 'sele', }
type: 'keyword'
}
},
getCursor: sinon.stub()
} }
const hints = getHints(editor, {}) const hints = getHints(editor, {})

View File

@@ -41,5 +41,12 @@ module.exports = {
.end() .end()
config.module.rule('js').exclude.add(/worker\.js$/) config.module.rule('js').exclude.add(/worker\.js$/)
config
.plugin('html')
.tap(args => {
args[0].minify = false
return args
})
} }
} }