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

90 Commits

Author SHA1 Message Date
lana-k
8aac7af481 update package.json 2023-07-03 23:33:52 +02:00
lana-k
6982204e68 Update currentTab when close tabs #112 2023-07-03 23:13:09 +02:00
lana-k
41e0ae7332 fix test for firefox #110 2023-06-29 23:14:08 +02:00
lana-k
ebb5af4f10 send event when sharing 2023-06-29 22:57:39 +02:00
lana-k
ae26358b25 add test #110 2023-06-29 22:28:41 +02:00
lana-k
d9ee702b8e update papaparse #111 2023-06-29 22:14:28 +02:00
lana-k
446045fa55 Catch parsing errors in compete #110 2023-06-29 22:13:56 +02:00
lana-k
1a9d1b308b check data format #109 2023-06-10 20:05:42 +02:00
lana-k
014ecf145e update version 2023-06-10 19:11:15 +02:00
lana-k
0044d82b6f Loading remote database and inquiries #109 2023-06-05 22:31:39 +02:00
lana-k
998e8d66f7 Tab refactor 2023-06-01 14:42:51 +02:00
lana-k
db3dbdf993 Merge branch 'master' of github.com:lana-k/sqliteviz 2023-05-17 21:41:17 +02:00
lana-k
4e13a16e33 No blocking while loading predifined #107 2023-05-17 21:37:41 +02:00
saaj
9c0103fd05 SQLite 3.41.0 and pearson correlation extension function (#106)
* Build SQLite 3.41.0

* Update pivot_vtab

* Add Pearson correlation coefficient function extension, build

* Add an easy way to running test locally without Nodejs

* Use RSS sum to pick top2 processes for the report

* Try previous Ubuntu LTS as a workaround for Firefox worker not starting
2023-03-04 22:51:25 +01:00
saaj
e4b117ffb9 Sqljs upgrade and benchmark improvements (#103)
* Update to sql.js 1.7.0

* Update to emsdk 3.0.1, replace/remove deprecated/irrelevant settings

- Renamed .bc extension to .o
- Remove deprecated INLINING_LIMIT setting
- Remove SINGLE_FILE

* Update SQLite to 3.39.3

* Collect and plot CPU and RSS charts from the benchmark containers

* Move procpath commands to a playbook, plot only top 2 RSS & CPU usage

* Optimise for size, put -flto for both compile and link
2023-03-04 17:00:46 +01:00
lana-k
6320f818cb fix undefined in tests and chart metrics 2022-07-30 16:42:30 +02:00
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
lana-k
3e3a70430f fix lint 2021-09-03 16:03:06 +02:00
lana-k
bce3854792 fix test 2021-09-03 15:58:37 +02:00
lana-k
307cac06e6 remove html2canvas logging 2021-09-03 15:58:20 +02:00
lana-k
c4b0bdc870 fix lint 2021-09-03 15:25:30 +02:00
lana-k
69e0b2129b add tests for Pivot and Chart 2021-09-03 15:25:23 +02:00
lana-k
4ebb3715d6 add tests for RinResult 2021-09-03 15:22:48 +02:00
lana-k
858e32c9fd fix errors in tests 2021-09-01 22:42:34 +02:00
lana-k
42cce95ed0 fix errors in tests 2021-09-01 22:27:51 +02:00
lana-k
61ffcc82d6 add test for clipboard copying in DataView 2021-08-30 23:10:57 +02:00
lana-k
4716bcf258 DataView tests 2021-08-29 22:27:25 +02:00
lana-k
23d2421584 add tests for clipboard lib 2021-08-29 21:32:13 +02:00
lana-k
4730afc68a add test for tab 2021-08-29 21:31:50 +02:00
lana-k
6b902d5f00 fix line size 2021-08-29 21:31:30 +02:00
lana-k
a484c1e022 add test for pivot helper 2021-08-29 21:30:53 +02:00
lana-k
1ed5bf2fae add test for sleep 2021-08-29 21:29:36 +02:00
lana-k
da8dc71f23 add test to csv (serialize) 2021-08-29 21:29:21 +02:00
lana-k
ebac3d8f6c add test for chart helper 2021-08-29 21:28:55 +02:00
lana-k
0336168bdc line size lint checking 2021-08-29 21:27:02 +02:00
lana-k
e38f482933 saveAsPng refactoring 2021-08-28 19:07:10 +02:00
lana-k
bcd98fe1ad add sleep function 2021-08-23 22:20:03 +02:00
lana-k
ec3ec6f57a add test for selection and filter #84 2021-08-22 16:50:17 +02:00
lana-k
ae9e2bc03c fix lint errors 2021-08-22 15:01:22 +02:00
lana-k
fe11d446f3 add NO found msg #83; fix checkbox behaviour #84 2021-08-22 15:00:34 +02:00
lana-k
e6bef139f1 Support vector graphic chart download #39 2021-08-21 22:51:17 +02:00
lana-k
d7e0e52da5 add copy dialog for csv #50 2021-08-21 21:41:06 +02:00
lana-k
09cec13a9c show dialog for keeping user activity #50 2021-08-21 18:44:29 +02:00
lana-k
0cae368350 fix lint errors #50 2021-08-20 22:33:15 +02:00
lana-k
88c62a1dfb show message about ClipboardItem support #50
for Firefox
2021-08-20 22:30:48 +02:00
lana-k
ead861b610 copy png to clipboard #50 2021-08-20 21:45:05 +02:00
lana-k
949e6b626e fix lint errors #81 2021-08-20 18:07:30 +02:00
lana-k
6dc81f7be6 copy result set as tsv #50 2021-08-20 18:06:58 +02:00
lana-k
bb39dcbc2f use tab for export to csv #81
(support paste to google sheets)
2021-08-20 18:05:51 +02:00
lana-k
131efb55df update readme #81 2021-08-20 15:38:18 +02:00
lana-k
b33974c26c Export result set to CSV file #81 2021-08-20 15:36:19 +02:00
lana-k
e9c97beb5a fix lint error 2021-08-18 22:09:32 +02:00
lana-k
cabc4f8acd fix JS error during running queries #80 2021-08-18 21:50:42 +02:00
lana-k
679a785d70 add test for view #78 2021-08-18 21:24:12 +02:00
lana-k
a91add40cc uninstall sqlite parser #78 2021-08-18 20:58:53 +02:00
twoxfh
53b2d8372f Updating how database object information is getting retrieved (#79)
* Updating how database object information is getting retrieved

I updated the SQLite query for gathering database objects to make use of the JSON1 extension so you can grab tables and views name, their associated columns with types and set it to the schema. This removes the need to work with DDL's. Hints for Tables and Views works since my approach is they are both database objects.
2021-08-18 20:22:30 +02:00
lana-k
f3e8448851 update version 2021-08-11 23:16:11 +02:00
116 changed files with 19831 additions and 13337 deletions

View File

@@ -13,7 +13,8 @@ module.exports = {
rules: { rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-case-declarations': 'off' 'no-case-declarations': 'off',
'max-len': [2, 100, 4, { ignoreUrls: true }]
}, },
overrides: [ overrides: [
{ {

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

@@ -11,7 +11,7 @@ on:
jobs: jobs:
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js - name: Use Node.js

24
Dockerfile.test Normal file
View File

@@ -0,0 +1,24 @@
# An easy way to run tests locally without Nodejs installed:
#
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12
RUN set -ex; \
apt update; \
apt install -y chromium firefox-esr; \
npm install -g npm@7
WORKDIR /tmp/build
COPY package.json package-lock.json ./
COPY lib lib
RUN npm install
COPY . .
RUN set -ex; \
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
RUN npm run lint -- --no-fix && npm run test

View File

@@ -9,18 +9,19 @@ Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
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 file into a SQLite database and visualize imported data
- 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 to/from a JSON file - import/export inquiries from/to a JSON file
- export a modified SQLite database - export a modified SQLite database
- use it offline from your OS application menu like any other desktop app - use it offline from your OS application menu like any other desktop app
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4 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].
@@ -33,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

@@ -55,12 +55,15 @@ module.exports = function (config) {
junitReporter: { junitReporter: {
outputDir: 'report', // results will be saved as $outputDir/$browserName.xml outputDir: 'report', // results will be saved as $outputDir/$browserName.xml
outputFile: undefined, // if included, results will be saved as $outputDir/$browserName/$outputFile // if included, results will be saved as $outputDir/$browserName/$outputFile
outputFile: undefined,
suite: '', // suite will become the package name attribute in xml testsuite element suite: '', // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names useBrowserName: true, // add browser name to report and classes names
nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element // function (browser, result) to customize the name attribute in xml testcase element
classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element nameFormatter: undefined,
properties: {} // key value pair of properties to add to the <properties> section of the report // function (browser, result) to customize the classname attribute in xml testcase element
classNameFormatter: undefined,
properties: {} // key value pairs add to the <properties> section of the report
}, },
// web server port // web server port
@@ -70,7 +73,8 @@ module.exports = function (config) {
colors: true, colors: true,
// level of logging // level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
// config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes // enable / disable watching file and executing tests whenever any file changes
@@ -80,7 +84,8 @@ module.exports = function (config) {
FirefoxHeadlessTouch: { FirefoxHeadlessTouch: {
base: 'FirefoxHeadless', base: 'FirefoxHeadless',
prefs: { prefs: {
'dom.w3c_touch_events.enabled': 1 'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
} }
} }
}, },

View File

@@ -1,4 +1,4 @@
FROM emscripten/emsdk:2.0.24 FROM emscripten/emsdk:3.0.1
WORKDIR /tmp/build WORKDIR /tmp/build

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,14 +36,22 @@ 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:
1. [pivot_vtab][5] -- a pivot virtual table 1. [pivot_vtab][5] -- a pivot virtual table
2. `pearson` correlation coefficient function extension from [sqlean][21]
(which is part of [squib][20])
To ease the step to have working clone locally, the build is committed into To ease the step to have working clone locally, the build is committed into
the repository. the repository.
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 +94,12 @@ 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
[20]: https://github.com/mrwilson/squib/blob/master/pearson.c
[21]: https://github.com/nalgeon/sqlean/blob/incubator/src/pearson.c

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,14 +1,25 @@
# SQLite WebAssembly build micro-benchmark # SQLite WebAssembly build micro-benchmark
This directory contains a micro-benchmark for evaluating SQLite This directory contains a micro-benchmark for evaluating SQLite WebAssembly
WebAssembly builds performance on typical SQL queries, run from builds performance on read and write SQL queries, run from `make.sh` script. If
`make.sh` script. It can also serve as a smoke test. the script has permission to `nice` processes and [Procpath][1] is installed,
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
processes running inside the benchmark containers. It can also serve as a smoke
test (e.g. for memory leaks).
The benchmark operates on a set of SQLite WebAssembly builds expected The benchmark operates on a set of SQLite WebAssembly builds expected in
in `lib/build-$NAME` directories each containing `sql-wasm.js` and `lib/build-$NAME` directories each containing `sql-wasm.js` and
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs `sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
the benchmark in Firefox and Chromium using Karma in the container. benchmark in Firefox and Chromium using Karma in the container.
After successful run, the benchmark result of each build is contained After successful run, the benchmark produces the following per each build:
in `build-$NAME-result.json`. The JSON result files can be analysed
using `result-analysis.ipynb` Jupyter notebook. - `build-$NAME-result.json`
- `build-$NAME.sqlite` (if Procpath is installed)
- `build-$NAME.svg` (if Procpath is installed)
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
run, then Firefox run per container).
[1]: https://pypi.org/project/Procpath/

View File

@@ -1,21 +1,47 @@
#!/bin/bash -e #!/bin/bash -e
cleanup () {
rm -rf lib/dist "$renice_flag_file"
docker rm -f sqljs-benchmark-run 2> /dev/null || true
}
trap cleanup EXIT
if [ ! -f sample.csv ]; then if [ ! -f sample.csv ]; then
wget --header="accept-encoding: gzip" -q -O- \ wget --header="accept-encoding: gzip" -q -O- \
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \ https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
| gunzip -c > sample.csv | gunzip -c > sample.csv
fi fi
PLAYBOOK=procpath/karma_docker.procpath
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
test_ni=$(nice -n -5 nice)
if [ $test_ni == -5 ]; then
renice_flag_file=$(mktemp)
fi
{
while [ -f $renice_flag_file ]; do
procpath --logging-level ERROR play -f $PLAYBOOK renice:watch
done
} &
shopt -s nullglob
for d in lib/build-* ; do for d in lib/build-* ; do
rm -r lib/dist || true rm -rf lib/dist
cp -r $d lib/dist cp -r $d lib/dist
sample_name=$(basename $d)
name=$(basename $d) docker build -t sqliteviz/sqljs-benchmark .
docker build -t sqliteviz/sqljs-benchmark:$name . docker rm sqljs-benchmark-run 2> /dev/null || true
docker rm sqljs-benchmark-$name 2> /dev/null || true docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name {
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json rm -f ${sample_name}.sqlite
docker rm sqljs-benchmark-$name procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
-o plot_file=${sample_name}.svg track:plot
} &
docker attach sqljs-benchmark-run
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
docker rm sqljs-benchmark-run
done done
rm -r lib/dist

View File

@@ -0,0 +1,28 @@
# This command may run when "sqljs-benchmark-run" does not yet exist or run
[renice:watch]
interval: 2
repeat: 30
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
query:
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
command:
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
# Expected input arguments: database_file
[track:record]
interval: 1
stop_without_result: 1
environment:
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
query:
$..children[?(@.stat.pid == $ROOT_PID)]
pid_list: $ROOT_PID
# Expected input arguments: database_file, plot_file
[track:plot]
moving_average_window: 5
title: Chromium vs Firefox (№1 RSS, №2 CPU)
custom_query_file:
procpath/top2_rss.sql
procpath/top2_cpu.sql

View File

@@ -0,0 +1,29 @@
WITH diff_all AS (
SELECT
record_id,
ts,
stat_pid,
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) tick_diff,
ts - LAG(ts) OVER (
PARTITION BY stat_pid
ORDER BY record_id
) ts_diff
FROM record
), diff AS (
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
), one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_utime + stat_stime) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
FROM diff
JOIN one_time_pid_condition USING(stat_pid)

View File

@@ -0,0 +1,13 @@
WITH one_time_pid_condition AS (
SELECT stat_pid
FROM record
GROUP BY 1
ORDER BY SUM(stat_rss) DESC
LIMIT 2
)
SELECT
ts,
stat_pid pid,
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
FROM record
JOIN one_time_pid_condition USING(stat_pid)

File diff suppressed because one or more lines are too long

View File

@@ -2,9 +2,11 @@ import logging
import subprocess import subprocess
from pathlib import Path from pathlib import Path
# See the setting descriptions on these pages:
# - https://emscripten.org/docs/optimizing/Optimizing-Code.html
# - https://github.com/emscripten-core/emscripten/blob/main/src/settings.js
cflags = ( cflags = (
'-O2', # SQLite configuration
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB '-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
'-DSQLITE_DEFAULT_MEMSTATUS=0', '-DSQLITE_DEFAULT_MEMSTATUS=0',
'-DSQLITE_DEFAULT_SYNCHRONOUS=0', '-DSQLITE_DEFAULT_SYNCHRONOUS=0',
@@ -13,26 +15,26 @@ cflags = (
'-DSQLITE_ENABLE_FTS3', '-DSQLITE_ENABLE_FTS3',
'-DSQLITE_ENABLE_FTS3_PARENTHESIS', '-DSQLITE_ENABLE_FTS3_PARENTHESIS',
'-DSQLITE_ENABLE_FTS5', '-DSQLITE_ENABLE_FTS5',
'-DSQLITE_ENABLE_JSON1',
'-DSQLITE_ENABLE_NORMALIZE', '-DSQLITE_ENABLE_NORMALIZE',
'-DSQLITE_EXTRA_INIT=extra_init', '-DSQLITE_EXTRA_INIT=extra_init',
'-DSQLITE_OMIT_DEPRECATED', '-DSQLITE_OMIT_DEPRECATED',
'-DSQLITE_OMIT_LOAD_EXTENSION', '-DSQLITE_OMIT_LOAD_EXTENSION',
'-DSQLITE_OMIT_SHARED_CACHE', '-DSQLITE_OMIT_SHARED_CACHE',
'-DSQLITE_THREADSAFE=0', '-DSQLITE_THREADSAFE=0',
# Compile-time optimisation
'-Os', # reduces the code size about in half comparing to -O2
'-flto',
) )
emflags = ( emflags = (
# Base # Base
'--memory-init-file', '0', '--memory-init-file', '0',
'-s', 'RESERVED_FUNCTION_POINTERS=64',
'-s', 'ALLOW_TABLE_GROWTH=1', '-s', 'ALLOW_TABLE_GROWTH=1',
'-s', 'SINGLE_FILE=0',
# WASM # WASM
'-s', 'WASM=1', '-s', 'WASM=1',
'-s', 'ALLOW_MEMORY_GROWTH=1', '-s', 'ALLOW_MEMORY_GROWTH=1',
# Optimisation '-s', 'ENVIRONMENT=web,worker',
'-s', 'INLINING_LIMIT=50', # Link-time optimisation
'-O3', '-Os',
'-flto', '-flto',
# sql.js # sql.js
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json', '-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
@@ -50,22 +52,22 @@ def build(src: Path, dst: Path):
'emcc', 'emcc',
*cflags, *cflags,
'-c', src / 'sqlite3.c', '-c', src / 'sqlite3.c',
'-o', out / 'sqlite3.bc', '-o', out / 'sqlite3.o',
]) ])
logging.info('Building LLVM bitcode for extension-functions.c') logging.info('Building LLVM bitcode for extension-functions.c')
subprocess.check_call([ subprocess.check_call([
'emcc', 'emcc',
*cflags, *cflags,
'-c', src / 'extension-functions.c', '-c', src / 'extension-functions.c',
'-o', out / 'extension-functions.bc', '-o', out / 'extension-functions.o',
]) ])
logging.info('Building WASM from bitcode') logging.info('Building WASM from bitcode')
subprocess.check_call([ subprocess.check_call([
'emcc', 'emcc',
*emflags, *emflags,
out / 'sqlite3.bc', out / 'sqlite3.o',
out / 'extension-functions.bc', out / 'extension-functions.o',
'-o', out / 'sql-wasm.js', '-o', out / 'sql-wasm.js',
]) ])

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/2023/sqlite-amalgamation-3410000.zip'
# Extension-functions # Extension-functions
# =================== # ===================
@@ -20,16 +20,19 @@ 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/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
) )
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip' sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
def _generate_extra_init_c_function(init_function_names): def _generate_extra_init_c_function(init_function_names):

File diff suppressed because one or more lines are too long

Binary file not shown.

26796
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.15.2", "version": "0.23.2",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -12,10 +12,11 @@
"dependencies": { "dependencies": {
"codemirror": "^5.57.0", "codemirror": "^5.57.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"dataurl-to-blob": "^0.0.1",
"html2canvas": "^1.1.4", "html2canvas": "^1.1.4",
"jquery": "^3.6.0", "jquery": "^3.6.0",
"nanoid": "^3.1.12", "nanoid": "^3.1.12",
"papaparse": "^5.3.1", "papaparse": "^5.4.1",
"pivottable": "^2.23.0", "pivottable": "^2.23.0",
"plotly.js": "^1.58.4", "plotly.js": "^1.58.4",
"promise-worker": "^2.0.1", "promise-worker": "^2.0.1",
@@ -23,7 +24,6 @@
"react-chart-editor": "^0.45.0", "react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"sqlite-parser": "^1.0.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-codemirror": "^4.0.6", "vue-codemirror": "^4.0.6",
"vue-js-modal": "^2.0.0-rc.6", "vue-js-modal": "^2.0.0-rc.6",
@@ -51,6 +51,7 @@
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"flush-promises": "^1.0.2",
"karma": "^3.1.4", "karma": "^3.1.4",
"karma-firefox-launcher": "^2.1.0", "karma-firefox-launcher": "^2.1.0",
"karma-webpack": "^4.0.2", "karma-webpack": "^4.0.2",

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

@@ -39,7 +39,10 @@
/> />
<text-field <text-field
label="Escape char" label="Escape char"
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").' hint='
The character used to escape the quote character within a field
(e.g. "column with ""quotes"" in text").
'
max-hint-width="242px" max-hint-width="242px"
v-model="escapeChar" v-model="escapeChar"
width="93px" width="93px"
@@ -100,7 +103,7 @@
</template> </template>
<script> <script>
import csv from './csv' import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector' import DelimiterSelector from './DelimiterSelector'
@@ -109,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',
@@ -332,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,83 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
:clickToClose="false"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon"/>
{{ loadingMsg }}
</div>
<div v-else class="loading-dialog-body">
<img :src="require('@/assets/images/success.svg')" class="success-icon state-icon" />
{{ successMsg }}
</div>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="loading"
@click="$emit('cancel')"
>
Cancel
</button>
<button
class="primary"
:disabled="loading"
@click="$emit('action')"
>
{{ actionBtnName }}
</button>
</div>
</modal>
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
props: {
loadingMsg: String,
successMsg: String,
actionBtnName: String,
name: String,
title: String,
loading: Boolean
},
watch: {
loading () {
if (this.loading) {
this.$modal.show(this.name)
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel () {
this.$emit('cancel')
}
}
}
</script>
<style scoped>
.loading-dialog-body {
display: flex;
align-items: center;
}
.success-icon {
width: 30px;
}
.state-icon {
margin-right: 8px;
}
</style>

View File

@@ -26,7 +26,10 @@
<div <div
:class="[ :class="[
'toggle-btns', 'toggle-btns',
{'both': after.max === 100 && before.max === 100 && paneAfter.size > 0 && paneBefore.size > 0} {
'both': after.max === 100 && before.max === 100 &&
paneAfter.size > 0 && paneBefore.size > 0
}
]" ]"
> >
<div <div
@@ -72,14 +75,23 @@ export default {
props: { props: {
horizontal: { type: Boolean, default: false }, horizontal: { type: Boolean, default: false },
before: { type: Object }, before: { type: Object },
after: { type: Object } after: { type: Object },
default: {
type: Object,
default: () => {
return {
before: 50,
after: 50
}
}
}
}, },
data () { data () {
return { return {
container: null, container: null,
paneBefore: this.before, paneBefore: this.before,
paneAfter: this.after, paneAfter: this.after,
beforeMinimising: { beforeMinimising: !this.after.size || !this.before.size ? this.default : {
before: this.before.size, before: this.before.size,
after: this.after.size after: this.after.size
}, },

View File

@@ -28,8 +28,11 @@ export default {
return { return {
page: this.value, page: this.value,
chevron: ` chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="9" height="9" viewBox="0 0 8 12" fill="none">
<path d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"/> <path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
5.79997L1.99992 11.2L0.721924 9.93097Z" fill="#506784"
/>
</svg> </svg>
` `
} }

View File

@@ -14,14 +14,15 @@
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d=" :d="
M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899 `M13.6573 1.5H2.59985C1.77485 1.5 1.09985 2.175 1.09985 3V13.6649C1.09985 14.4899
1.77485 15.1649 2.59985 1.77485 15.1649 2.59985
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573 15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
2.17 14.4873 1.5 13.6573 1.5ZM13.6573 2.17 14.4873 1.5 13.6573 1.5ZM13.6573
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985 7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702 9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.1717H13.1702V10.8434Z 10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.` +
'1717H13.1702V10.8434Z'
" "
fill="#A2B1C6" fill="#A2B1C6"
/> />

View File

@@ -11,7 +11,13 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12 9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3 6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75 8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995 9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z" d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3
10.5ZM9 9.75C5.685 9.75 3 8.4075 3 6.75V9C3 10.6575 5.685 12 9 12C9.2925 12 9.5775 12
9.87 12L12.75 9.09C11.55 9.54 10.2825 9.75 9 9.75ZM9 2.25C5.685 2.25 3 3.5925 3 5.25C3
6.9075 5.685 8.25 9 8.25C12.315 8.25 15 6.9075 15 5.25C15 3.5925 12.315 2.25 9 2.25ZM15.75
8.3475C15.6375 8.3475 15.5325 8.3925 15.4575 8.475L14.7075 9.225L16.245 10.725L16.995
9.975C17.1525 9.825 17.16 9.57 16.995 9.3975L16.065 8.475C15.99 8.3925 15.885 8.3475 15.78
8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -5,12 +5,43 @@
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
> >
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.41943 16V10H10.4194V16H8.41943Z" fill="#A2B1C6"/> <path
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.41943 16L2.41943 10H4.41943V16H2.41943Z" fill="#A2B1C6"/> fill-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.4194 16V7H13.4194V16H11.4194Z" fill="#A2B1C6"/> clip-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.4194 16V8H16.4194V16H14.4194Z" fill="#A2B1C6"/> d="M8.41943 16V10H10.4194V16H8.41943Z"
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.41943 12V16H5.41943V12H7.41943Z" fill="#A2B1C6"/> fill="#A2B1C6"
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.4332 1.80676L16.6265 6.00001L15.9194 6.70712L12.4055 3.19326L5.93169 9.1691L1.71436 5.55424L2.36515 4.79499L5.90707 7.83092L12.4332 1.80676Z" fill="#A2B1C6"/> />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M2.41943 16L2.41943 10H4.41943V16H2.41943Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M11.4194 16V7H13.4194V16H11.4194Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M14.4194 16V8H16.4194V16H14.4194Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M7.41943 12V16H5.41943V12H7.41943Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M12.4332 1.80676L16.6265 6.00001L15.9194 6.70712L12.4055 3.19326L5.93169 9.1691L1.71436
5.55424L2.36515 4.79499L5.90707 7.83092L12.4332 1.80676Z"
fill="#A2B1C6"
/>
</svg> </svg>
</template> </template>

View File

@@ -8,7 +8,13 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M14.3481 14.849C13.8791 15.318 13.1191 15.318 12.6511 14.849L10.0001 11.819L7.34907 14.848C6.88007 15.317 6.12007 15.317 5.65207 14.848C5.18307 14.379 5.18307 13.619 5.65207 13.151L8.41007 10.001L5.65107 6.84898C5.18207 6.37998 5.18207 5.62098 5.65107 5.15198C6.12007 4.68298 6.87907 4.68298 7.34807 5.15198L10.0001 8.18298L12.6511 5.15198C13.1201 4.68298 13.8791 4.68298 14.3481 5.15198C14.8171 5.62098 14.8171 6.38098 14.3481 6.84898L11.5901 10.001L14.3481 13.151C14.8171 13.62 14.8171 14.38 14.3481 14.849V14.849Z" d="M14.3481 14.849C13.8791 15.318 13.1191 15.318 12.6511 14.849L10.0001 11.819L7.34907
14.848C6.88007 15.317 6.12007 15.317 5.65207 14.848C5.18307 14.379 5.18307 13.619
5.65207 13.151L8.41007 10.001L5.65107 6.84898C5.18207 6.37998 5.18207 5.62098 5.65107
5.15198C6.12007 4.68298 6.87907 4.68298 7.34807 5.15198L10.0001 8.18298L12.6511
5.15198C13.1201 4.68298 13.8791 4.68298 14.3481 5.15198C14.8171 5.62098 14.8171 6.38098
14.3481 6.84898L11.5901 10.001L14.3481 13.151C14.8171 13.62 14.8171 14.38 14.3481
14.849V14.849Z"
fill="#C8D4E3" fill="#C8D4E3"
/> />
</svg> </svg>

View File

@@ -0,0 +1,33 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
3.14291V16.2422C2.07422 17.2115 2.86276 18 3.83203 18H14.1917C15.161 18 15.9495 17.2115
15.9495 16.2422V3.14291C15.9495 2.17365 15.1609 1.3851 14.1917 1.3851V1.3851ZM11.0744
1.40625V2.77034H6.94899C6.94899 2.30301 6.94899 1.91698 6.94899 1.40625H11.0744ZM14.5433
16.2422C14.5433 16.4361 14.3855 16.5938 14.1917 16.5938H3.83203C3.63812 16.5938 3.48047
16.4361 3.48047 16.2422V3.14291C3.48047 2.94914 3.63812 2.79135 3.83203
2.79135H5.54274V3.47346C5.54274 3.86183 5.85764 4.17659 6.24586 4.17659H11.7775C12.1657
4.17659 12.4806 3.86183 12.4806 3.47346V2.79135H14.1917C14.3855 2.79135 14.5433 2.94914
14.5433 3.14291V16.2422ZM12.2745 8.20967C12.5491 8.48433 12.5491 8.92955 12.2745
9.20407L8.5795 12.899C8.30498 13.1737 7.85976 13.1737 7.5851 12.899L5.74887
11.0628C5.47435 10.7883 5.47435 10.3431 5.74887 10.0684C6.02353 9.7939 6.46861 9.7939
6.74327 10.0684L8.08237 11.4075L11.2801 8.20981C11.5547 7.93515 11.9998 7.93515 12.2745
8.20967V8.20967Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ClipboardIcon'
}
</script>

View File

@@ -9,7 +9,8 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 12.59L8.41 7L14 1.41Z" d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14
12.59L8.41 7L14 1.41Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -5,9 +5,28 @@
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
> >
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.5552 6.91522C13.7584 6.91357 13.9535 6.99442 14.096 7.13926L17.2582 10.3541C17.5486 10.6494 17.5447 11.1242 17.2494 11.4147C16.9541 11.7051 16.4793 11.7012 16.1888 11.4059L13.57 8.74357L9.29577 13.2318C9.01977 13.5216 8.56484 13.5436 8.2621 13.2819L5.35435 10.7677L2.03285 13.7321C1.72382 14.0079 1.24971 13.981 0.973901 13.6719C0.69809 13.3629 0.725022 12.8888 1.03406 12.613L4.8471 9.20986C5.12827 8.95892 5.55198 8.95559 5.83705 9.20208L8.70249 11.6797L13.0182 7.14796C13.1583 7.00084 13.3521 6.91686 13.5552 6.91522Z" fill="#A2B1C6"/> <path
fill-rule="evenodd"
clip-rule="evenodd"
d="M13.5552 6.91522C13.7584 6.91357 13.9535 6.99442 14.096 7.13926L17.2582 10.3541C17.5486
10.6494 17.5447 11.1242 17.2494 11.4147C16.9541 11.7051 16.4793 11.7012 16.1888
11.4059L13.57 8.74357L9.29577 13.2318C9.01977 13.5216 8.56484 13.5436 8.2621
13.2819L5.35435 10.7677L2.03285 13.7321C1.72382 14.0079 1.24971 13.981 0.973901
13.6719C0.69809 13.3629 0.725022 12.8888 1.03406 12.613L4.8471 9.20986C5.12827 8.95892
5.55198 8.95559 5.83705 9.20208L8.70249 11.6797L13.0182 7.14796C13.1583 7.00084 13.3521
6.91686 13.5552 6.91522Z"
fill="#A2B1C6"
/>
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6"/> <circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.036 1.21788L1.96546 1.213C1.14046 1.213 0.465462 1.888 0.465462 2.713L0.464111 15.2821C0.464111 16.1071 1.13911 16.7821 1.96411 16.7821L16.0347 16.787C16.8674 16.787 17.5347 16.1121 17.5347 15.287L17.536 2.71788C17.536 1.88787 16.866 1.21788 16.036 1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z" fill="#A2B1C6"/> <path
fill-rule="evenodd"
clip-rule="evenodd"
d="M16.036 1.21788L1.96546 1.213C1.14046 1.213 0.465462 1.888 0.465462 2.713L0.464111
15.2821C0.464111 16.1071 1.13911 16.7821 1.96411 16.7821L16.0347 16.787C16.8674 16.787
17.5347 16.1121 17.5347 15.287L17.536 2.71788C17.536 1.88787 16.866 1.21788 16.036
1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z"
fill="#A2B1C6"
/>
</svg> </svg>
</template> </template>

View File

@@ -7,7 +7,10 @@
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path d="M6.175 7.15002L10 10.975L13.825 7.15002L15 8.33336L10 13.3334L5 8.33336L6.175 7.15002Z" fill="#C8D4E3"/> <path
d="M6.175 7.15002L10 10.975L13.825 7.15002L15 8.33336L10 13.3334L5 8.33336L6.175 7.15002Z"
fill="#C8D4E3"
/>
</svg> </svg>
</template> </template>

View File

@@ -11,7 +11,9 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5 15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425 12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z" d="M10.5 1.5H4.5C3.675 1.5 3 2.175 3 3V15C3 15.825 3.675 16.5 4.5 16.5H13.5C14.325 16.5
15 15.825 15 15V6L10.5 1.5ZM13.5 15H4.5V3H9.75V6.75H13.5V15ZM12 8.25V13.575L10.425
12L8.325 14.1L6.225 12L8.325 9.9L6.675 8.25H12Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -0,0 +1,62 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<path
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
1.93115 15.5561 1.44775 15.0082C0.964355 14.4604 0.722656 13.687 0.722656
12.6879V12.3715C0.722656 11.7446 0.831055 11.1967 1.04785 10.728C1.26758 10.2563 1.58252
9.89301 1.99268 9.63812C2.40283 9.38031 2.87744 9.2514 3.4165 9.2514C4.19287 9.2514
4.81689 9.45648 5.28857 9.86664C5.76025 10.2739 6.02832 10.8364 6.09277
11.5541H4.55469C4.54297 11.1645 4.44482 10.8847 4.26025 10.7148C4.07568 10.5449 3.79443
10.4599 3.4165 10.4599C3.03271 10.4599 2.75146 10.6035 2.57275 10.8906C2.39404 11.1777
2.30029 11.6362 2.2915 12.2661V12.7187C2.2915 13.4013 2.37646 13.8891 2.54639
14.1821C2.71924 14.475 3.01514 14.6215 3.43408 14.6215C3.78857 14.6215 4.05957 14.538
4.24707 14.371C4.43457 14.204 4.53418 13.9389 4.5459 13.5756H6.07959ZM10.1094
14.0414C10.1094 13.8159 10.0288 13.6401 9.86768 13.5141C9.70947 13.3881 9.42969 13.2578
9.02832 13.123C8.62695 12.9882 8.29883 12.8578 8.04395 12.7319C7.21484 12.3246 6.80029
11.7651 6.80029 11.0532C6.80029 10.6987 6.90283 10.3867 7.10791 10.1171C7.31592 9.84467
7.60889 9.63373 7.98682 9.48431C8.36475 9.33197 8.78955 9.2558 9.26123 9.2558C9.72119
9.2558 10.1328 9.33783 10.4961 9.50189C10.8623 9.66595 11.1465 9.90033 11.3486
10.205C11.5508 10.5068 11.6519 10.8525 11.6519 11.2421H10.1138C10.1138 10.9814 10.0332
10.7792 9.87207 10.6357C9.71387 10.4921 9.49854 10.4203 9.22607 10.4203C8.95068 10.4203
8.73242 10.4819 8.57129 10.6049C8.41309 10.725 8.33398 10.8788 8.33398 11.0663C8.33398
11.2304 8.42188 11.3798 8.59766 11.5146C8.77344 11.6464 9.08252 11.7841 9.5249
11.9277C9.96729 12.0683 10.3306 12.2206 10.6147 12.3847C11.3062 12.7831 11.6519 13.3325
11.6519 14.0327C11.6519 14.5922 11.4409 15.0317 11.019 15.351C10.5972 15.6703 10.0186
15.83 9.2832 15.83C8.76465 15.83 8.29443 15.7377 7.87256 15.5532C7.45361 15.3657 7.13721
15.1108 6.92334 14.7885C6.7124 14.4633 6.60693 14.0898 6.60693 13.6679H8.15381C8.15381
14.0107 8.2417 14.2641 8.41748 14.4282C8.59619 14.5893 8.88477 14.6699 9.2832
14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407
10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463
9.34369H13.6558L14.9258 14.0019Z"
fill="#A2B1C6"
/>
<path
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
2.20845 0.991333 3.03345 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M15.917 0.991333H14.0517V2.49133H15.917V7.93074H17.417V2.49133C17.417 1.66633 16.742
0.991333 15.917 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M9.53043 1.28245e-05L13.2199 3.83975L10.9929 3.7953L10.9337 6.76455L7.96442
6.70529L8.02368 3.73603L5.6907 3.68947L9.53043 1.28245e-05Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ExportToCsvIcon'
}
</script>

View File

@@ -0,0 +1,61 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<path
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798
0.974609 11.7203 0.974609 11.0084C0.974609 10.6539 1.07715 10.3419 1.28223
10.0723C1.49023 9.79987 1.7832 9.58893 2.16113 9.43951C2.53906 9.28717 2.96387 9.211
3.43555 9.211C3.89551 9.211 4.30713 9.29303 4.67041 9.45709C5.03662 9.62115 5.3208
9.85553 5.52295 10.1602C5.7251 10.462 5.82617 10.8077 5.82617 11.1973H4.28809C4.28809
10.9366 4.20752 10.7344 4.04639 10.5909C3.88818 10.4473 3.67285 10.3755 3.40039
10.3755C3.125 10.3755 2.90674 10.4371 2.74561 10.5601C2.5874 10.6802 2.5083 10.834
2.5083 11.0215C2.5083 11.1856 2.59619 11.335 2.77197 11.4698C2.94775 11.6016 3.25684
11.7393 3.69922 11.8829C4.1416 12.0235 4.50488 12.1758 4.78906 12.3399C5.48047 12.7383
5.82617 13.2877 5.82617 13.9879C5.82617 14.5474 5.61523 14.9869 5.19336 15.3062C4.77148
15.6255 4.19287 15.7852 3.45752 15.7852C2.93896 15.7852 2.46875 15.6929 2.04688
15.5084C1.62793 15.3209 1.31152 15.066 1.09766 14.7437C0.886719 14.4185 0.78125 14.045
0.78125 13.6231H2.32812C2.32812 13.9659 2.41602 14.2193 2.5918 14.3834C2.77051 14.5445
3.05908 14.6251 3.45752 14.6251C3.7124 14.6251 3.91309 14.5709 4.05957 14.4625C4.20898
14.3511 4.28369 14.1959 4.28369 13.9966ZM9.1001 13.9571L10.3745 9.29889H12.0972L9.94385
15.6973H8.25635L6.12061 9.29889H7.83008L9.1001 13.9571ZM17.7441 14.9063C17.5068 15.17
17.1597 15.3824 16.7026 15.5435C16.2456 15.7047 15.7446 15.7852 15.1997 15.7852C14.3618
15.7852 13.6924 15.5289 13.1914 15.0162C12.6904 14.5035 12.4224 13.7901 12.3872
12.876L12.3828 12.3223C12.3828 11.6924 12.4941 11.1431 12.7168 10.6744C12.9395 10.2027
13.2573 9.84088 13.6704 9.58893C14.0864 9.33405 14.5669 9.2066 15.1118 9.2066C15.9087
9.2066 16.5269 9.38971 16.9663 9.75592C17.4087 10.1192 17.6665 10.6627 17.7397
11.3863H16.2544C16.2017 11.0289 16.0874 10.774 15.9116 10.6216C15.7358 10.4693 15.4868
10.3931 15.1646 10.3931C14.7778 10.3931 14.479 10.5572 14.2681 10.8853C14.0571 11.2134
13.9502 11.6822 13.9473 12.2916V12.6783C13.9473 13.317 14.0557 13.7974 14.2725
14.1197C14.4922 14.439 14.8364 14.5987 15.3052 14.5987C15.7065 14.5987 16.0054 14.5093
16.2017 14.3306V13.3375H15.1294V12.2784H17.7441V14.9063Z"
fill="#A2B1C6"
/>
<path
d="M2.86768 0.991333H4.73292V2.49133H2.86768V7.93074H1.36768V2.49133C1.36768 1.66633
2.04268 0.991333 2.86768 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M15.7512 0.991333H13.886V2.49133H15.7512V7.93074H17.2512V2.49133C17.2512 1.66633 16.5762
0.991333 15.7512 0.991333Z"
fill="#A2B1C6"
/>
<path
d="M9.36466 -1.76931e-05L13.0541 3.83972L10.8272 3.79527L10.7679 6.76452L7.79865
6.70526L7.85791 3.736L5.52492 3.68944L9.36466 -1.76931e-05Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'ExportToSvgIcon'
}
</script>

View File

@@ -10,8 +10,28 @@
@mouseenter="showTooltip" @mouseenter="showTooltip"
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path d="M8.75 14.1666H10.4167V12.5H8.75V14.1666ZM9.58333 16.25C5.90833 16.25 2.91667 13.2583 2.91667 9.58333C2.91667 5.90833 5.90833 2.91667 9.58333 2.91667C13.2583 2.91667 16.25 5.90833 16.25 9.58333C16.25 13.2583 13.2583 16.25 9.58333 16.25ZM9.58333 1.25C8.48898 1.25 7.40535 1.46555 6.3943 1.88434C5.38326 2.30313 4.4646 2.91696 3.69078 3.69078C2.12797 5.25358 1.25 7.3732 1.25 9.58333C1.25 11.7935 2.12797 13.9131 3.69078 15.4759C4.4646 16.2497 5.38326 16.8635 6.3943 17.2823C7.40535 17.7011 8.48898 17.9167 9.58333 17.9167C11.7935 17.9167 13.9131 17.0387 15.4759 15.4759C17.0387 13.9131 17.9167 11.7935 17.9167 9.58333C17.9167 8.48898 17.7011 7.40535 17.2823 6.3943C16.8635 5.38326 16.2497 4.4646 15.4759 3.69078C14.7021 2.91696 13.7834 2.30313 12.7724 1.88434C11.7613 1.46555 10.6777 1.25 9.58333 1.25Z" fill="#A2B1C6"/> <path
<path d="M9.91601 4.51787C8.98167 4.42606 8.05144 4.69097 7.36309 5.24472C6.68735 5.78828 6.2998 6.56661 6.2998 7.38012H7.92488C7.92488 6.97463 8.11059 6.60187 8.44779 6.33061C8.79784 6.049 9.25647 5.92005 9.73896 5.96755C10.4832 6.04076 11.0828 6.57277 11.1647 7.23265C11.2306 7.764 10.9661 8.28194 10.4744 8.58426C9.38676 9.25303 8.73742 10.343 8.73742 11.5H10.3625C10.3625 10.8243 10.7477 10.184 11.3929 9.78733C12.3808 9.17985 12.9122 8.13913 12.7798 7.07124C12.6144 5.73863 11.41 4.66476 9.91601 4.51787Z" fill="#A2B1C6"/> d="M8.75 14.1666H10.4167V12.5H8.75V14.1666ZM9.58333 16.25C5.90833 16.25 2.91667 13.2583
2.91667 9.58333C2.91667 5.90833 5.90833 2.91667 9.58333 2.91667C13.2583 2.91667 16.25
5.90833 16.25 9.58333C16.25 13.2583 13.2583 16.25 9.58333 16.25ZM9.58333 1.25C8.48898
1.25 7.40535 1.46555 6.3943 1.88434C5.38326 2.30313 4.4646 2.91696 3.69078
3.69078C2.12797 5.25358 1.25 7.3732 1.25 9.58333C1.25 11.7935 2.12797 13.9131 3.69078
15.4759C4.4646 16.2497 5.38326 16.8635 6.3943 17.2823C7.40535 17.7011 8.48898 17.9167
9.58333 17.9167C11.7935 17.9167 13.9131 17.0387 15.4759 15.4759C17.0387 13.9131 17.9167
11.7935 17.9167 9.58333C17.9167 8.48898 17.7011 7.40535 17.2823 6.3943C16.8635 5.38326
16.2497 4.4646 15.4759 3.69078C14.7021 2.91696 13.7834 2.30313 12.7724 1.88434C11.7613
1.46555 10.6777 1.25 9.58333 1.25Z"
fill="#A2B1C6"
/>
<path
d="M9.91601 4.51787C8.98167 4.42606 8.05144 4.69097 7.36309 5.24472C6.68735 5.78828
6.2998 6.56661 6.2998 7.38012H7.92488C7.92488 6.97463 8.11059 6.60187 8.44779
6.33061C8.79784 6.049 9.25647 5.92005 9.73896 5.96755C10.4832 6.04076 11.0828 6.57277
11.1647 7.23265C11.2306 7.764 10.9661 8.28194 10.4744 8.58426C9.38676 9.25303 8.73742
10.343 8.73742 11.5H10.3625C10.3625 10.8243 10.7477 10.184 11.3929 9.78733C12.3808
9.17985 12.9122 8.13913 12.7798 7.07124C12.6144 5.73863 11.41 4.66476 9.91601 4.51787Z"
fill="#A2B1C6"
/>
</svg> </svg>
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip"> <span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip">
{{ hint }} {{ hint }}

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

@@ -5,7 +5,15 @@
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
> >
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.7661 5.13901L18.3407 9.43008H16.5161V12.8467C16.5161 13.7957 16.2783 14.6451 15.6714 15.2521C15.0645 15.859 14.215 16.0967 13.2661 16.0967H9.84942V17.9214L5.55835 15.3467L9.84942 12.7721V14.5967H13.2661C13.9838 14.5967 14.3844 14.4178 14.6108 14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z" fill="#A2B1C6"/> <path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.7661 5.13901L18.3407 9.43008H16.5161V12.8467C16.5161 13.7957 16.2783 14.6451 15.6714
15.2521C15.0645 15.859 14.215 16.0967 13.2661 16.0967H9.84942V17.9214L5.55835
15.3467L9.84942 12.7721V14.5967H13.2661C13.9838 14.5967 14.3844 14.4178 14.6108
14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z"
fill="#A2B1C6"
/>
<path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6"/> <path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6"/>
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6"/> <path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6"/>
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6"/> <path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6"/>

View File

@@ -1,5 +0,0 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686 14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231 5.51953 9 5.51953ZM9 12.9023C7.35226 12.9023 6.01172 11.5618 6.01172 9.91406C6.01172 8.26632 7.35226 6.92578 9 6.92578C10.6477 6.92578 11.9883 8.26632 11.9883 9.91406C11.9883 11.5618 10.6477 12.9023 9 12.9023Z" fill="#A2B1C6"/>
<path d="M15.8906 3.41016H13.304C13.2221 3.41016 13.1483 3.36547 13.1104 3.29319L12.3948 1.78945C12.3928 1.78534 12.3908 1.78126 12.3887 1.77718C12.1117 1.22312 11.5548 0.878906 10.9353 0.878906H7.11478C6.49529 0.878906 5.93835 1.22312 5.66135 1.77722C5.65928 1.7813 5.65731 1.78538 5.65534 1.78949L4.9397 3.2933C4.90173 3.36547 4.82797 3.41016 4.74609 3.41016H2.10938C0.946266 3.41016 0 4.35642 0 5.51953V15.0117C0 16.1748 0.946266 17.1211 2.10938 17.1211H15.8906C17.0537 17.1211 18 16.1748 18 15.0117V5.51953C18 4.35642 17.0537 3.41016 15.8906 3.41016ZM16.5938 15.0117C16.5938 15.3994 16.2783 15.7148 15.8906 15.7148H2.10938C1.72167 15.7148 1.40625 15.3994 1.40625 15.0117V5.51953C1.40625 5.13183 1.72167 4.81641 2.10938 4.81641H4.74609C5.36555 4.81641 5.92249 4.47223 6.19952 3.91816C6.2016 3.91409 6.20357 3.90997 6.20557 3.90586L6.92121 2.40205C6.95914 2.32984 7.0329 2.28516 7.11478 2.28516H10.9353C11.0172 2.28516 11.091 2.32984 11.1289 2.40202L11.8445 3.90582C11.8465 3.90994 11.8485 3.91405 11.8506 3.91813C12.1276 4.47219 12.6846 4.81637 13.304 4.81637H15.8906C16.2783 4.81637 16.5938 5.13179 16.5938 5.5195V15.0117Z" fill="#A2B1C6"/>
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -5,8 +5,31 @@
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
> >
<path d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686 14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231 5.51953 9 5.51953ZM9 12.9023C7.35226 12.9023 6.01172 11.5618 6.01172 9.91406C6.01172 8.26632 7.35226 6.92578 9 6.92578C10.6477 6.92578 11.9883 8.26632 11.9883 9.91406C11.9883 11.5618 10.6477 12.9023 9 12.9023Z" fill="#A2B1C6"/> <path
<path d="M15.8906 3.41016H13.304C13.2221 3.41016 13.1483 3.36547 13.1104 3.29319L12.3948 1.78945C12.3928 1.78534 12.3908 1.78126 12.3887 1.77718C12.1117 1.22312 11.5548 0.878906 10.9353 0.878906H7.11478C6.49529 0.878906 5.93835 1.22312 5.66135 1.77722C5.65928 1.7813 5.65731 1.78538 5.65534 1.78949L4.9397 3.2933C4.90173 3.36547 4.82797 3.41016 4.74609 3.41016H2.10938C0.946266 3.41016 0 4.35642 0 5.51953V15.0117C0 16.1748 0.946266 17.1211 2.10938 17.1211H15.8906C17.0537 17.1211 18 16.1748 18 15.0117V5.51953C18 4.35642 17.0537 3.41016 15.8906 3.41016ZM16.5938 15.0117C16.5938 15.3994 16.2783 15.7148 15.8906 15.7148H2.10938C1.72167 15.7148 1.40625 15.3994 1.40625 15.0117V5.51953C1.40625 5.13183 1.72167 4.81641 2.10938 4.81641H4.74609C5.36555 4.81641 5.92249 4.47223 6.19952 3.91816C6.2016 3.91409 6.20357 3.90997 6.20557 3.90586L6.92121 2.40205C6.95914 2.32984 7.0329 2.28516 7.11478 2.28516H10.9353C11.0172 2.28516 11.091 2.32984 11.1289 2.40202L11.8445 3.90582C11.8465 3.90994 11.8485 3.91405 11.8506 3.91813C12.1276 4.47219 12.6846 4.81637 13.304 4.81637H15.8906C16.2783 4.81637 16.5938 5.13179 16.5938 5.5195V15.0117Z" fill="#A2B1C6"/> d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686
14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231
5.51953 9 5.51953ZM9 12.9023C7.35226 12.9023 6.01172 11.5618 6.01172 9.91406C6.01172
8.26632 7.35226 6.92578 9 6.92578C10.6477 6.92578 11.9883 8.26632 11.9883
9.91406C11.9883 11.5618 10.6477 12.9023 9 12.9023Z"
fill="#A2B1C6"
/>
<path
d="M15.8906 3.41016H13.304C13.2221 3.41016 13.1483 3.36547 13.1104 3.29319L12.3948
1.78945C12.3928 1.78534 12.3908 1.78126 12.3887 1.77718C12.1117 1.22312 11.5548 0.878906
10.9353 0.878906H7.11478C6.49529 0.878906 5.93835 1.22312 5.66135 1.77722C5.65928 1.7813
5.65731 1.78538 5.65534 1.78949L4.9397 3.2933C4.90173 3.36547 4.82797 3.41016 4.74609
3.41016H2.10938C0.946266 3.41016 0 4.35642 0 5.51953V15.0117C0 16.1748 0.946266 17.1211
2.10938 17.1211H15.8906C17.0537 17.1211 18 16.1748 18 15.0117V5.51953C18 4.35642 17.0537
3.41016 15.8906 3.41016ZM16.5938 15.0117C16.5938 15.3994 16.2783 15.7148 15.8906
15.7148H2.10938C1.72167 15.7148 1.40625 15.3994 1.40625 15.0117V5.51953C1.40625 5.13183
1.72167 4.81641 2.10938 4.81641H4.74609C5.36555 4.81641 5.92249 4.47223 6.19952
3.91816C6.2016 3.91409 6.20357 3.90997 6.20557 3.90586L6.92121 2.40205C6.95914 2.32984
7.0329 2.28516 7.11478 2.28516H10.9353C11.0172 2.28516 11.091 2.32984 11.1289
2.40202L11.8445 3.90582C11.8465 3.90994 11.8485 3.91405 11.8506 3.91813C12.1276 4.47219
12.6846 4.81637 13.304 4.81637H15.8906C16.2783 4.81637 16.5938 5.13179 16.5938
5.5195V15.0117Z"
fill="#A2B1C6"
/>
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/> <path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/>
</svg> </svg>
</template> </template>

View File

@@ -6,8 +6,20 @@
viewBox="0 0 6 12" viewBox="0 0 6 12"
fill="none" fill="none"
> >
<path d="M2.99932 -3.63032e-05C2.75092 -3.63032e-05 2.54932 0.201563 2.54932 0.449964L2.54932 11.55C2.54932 11.7984 2.75092 12 2.99932 12C3.24772 12 3.44932 11.7984 3.44932 11.55L3.44932 0.449964C3.44932 0.201563 3.24772 -3.63032e-05 2.99932 -3.63032e-05Z" fill="#506784"/> <path
<path d="M2.99915 1.80492e-05C2.8839 1.80492e-05 2.76865 0.0438534 2.68109 0.132073L0.581055 2.232C0.405273 2.40789 0.405273 2.69287 0.581055 2.86865C0.756946 3.04443 1.04193 3.04443 1.21771 2.86865L2.99969 1.08667L4.78168 2.86865C4.95746 3.04443 5.24255 3.04443 5.41833 2.86865C5.59412 2.69287 5.59412 2.40789 5.41833 2.232L3.3183 0.132073C3.22953 0.0438534 3.11428 1.80492e-05 2.99915 1.80492e-05V1.80492e-05Z" fill="#506784"/> d="M2.99932 -3.63032e-05C2.75092 -3.63032e-05 2.54932 0.201563 2.54932 0.449964L2.54932
11.55C2.54932 11.7984 2.75092 12 2.99932 12C3.24772 12 3.44932 11.7984 3.44932
11.55L3.44932 0.449964C3.44932 0.201563 3.24772 -3.63032e-05 2.99932 -3.63032e-05Z"
fill="#506784"
/>
<path
d="M2.99915 1.80492e-05C2.8839 1.80492e-05 2.76865 0.0438534 2.68109 0.132073L0.581055
2.232C0.405273 2.40789 0.405273 2.69287 0.581055 2.86865C0.756946 3.04443 1.04193 3.04443
1.21771 2.86865L2.99969 1.08667L4.78168 2.86865C4.95746 3.04443 5.24255 3.04443 5.41833
2.86865C5.59412 2.69287 5.59412 2.40789 5.41833 2.232L3.3183 0.132073C3.22953 0.0438534
3.11428 1.80492e-05 2.99915 1.80492e-05V1.80492e-05Z"
fill="#506784"
/>
</svg> </svg>
</template> </template>

View File

@@ -6,8 +6,44 @@
fill="none" fill="none"
> >
<g clip-path="url(#clip0)"> <g clip-path="url(#clip0)">
<path d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3 2.18843 3.675 1.51343 4.5 1.51343Z" fill="#A2B1C6"/> <path
<path d="M4.28369 14.8127C4.28369 14.5872 4.20312 14.4114 4.04199 14.2854C3.88379 14.1594 3.604 14.0291 3.20264 13.8943C2.80127 13.7595 2.47314 13.6292 2.21826 13.5032C1.38916 13.0959 0.974609 12.5364 0.974609 11.8245C0.974609 11.47 1.07715 11.158 1.28223 10.8884C1.49023 10.616 1.7832 10.405 2.16113 10.2556C2.53906 10.1033 2.96387 10.0271 3.43555 10.0271C3.89551 10.0271 4.30713 10.1091 4.67041 10.2732C5.03662 10.4373 5.3208 10.6716 5.52295 10.9763C5.7251 11.2781 5.82617 11.6238 5.82617 12.0134H4.28809C4.28809 11.7527 4.20752 11.5505 4.04639 11.407C3.88818 11.2634 3.67285 11.1917 3.40039 11.1917C3.125 11.1917 2.90674 11.2532 2.74561 11.3762C2.5874 11.4963 2.5083 11.6501 2.5083 11.8376C2.5083 12.0017 2.59619 12.1511 2.77197 12.2859C2.94775 12.4177 3.25684 12.5554 3.69922 12.699C4.1416 12.8396 4.50488 12.9919 4.78906 13.156C5.48047 13.5544 5.82617 14.1038 5.82617 14.804C5.82617 15.3635 5.61523 15.803 5.19336 16.1223C4.77148 16.4417 4.19287 16.6013 3.45752 16.6013C2.93896 16.6013 2.46875 16.509 2.04688 16.3245C1.62793 16.137 1.31152 15.8821 1.09766 15.5598C0.886719 15.2346 0.78125 14.8611 0.78125 14.4392H2.32812C2.32812 14.782 2.41602 15.0354 2.5918 15.1995C2.77051 15.3606 3.05908 15.4412 3.45752 15.4412C3.7124 15.4412 3.91309 15.387 4.05957 15.2786C4.20898 15.1672 4.28369 15.012 4.28369 14.8127ZM12.0444 13.446C12.0444 14.0378 11.9463 14.5549 11.75 14.9973C11.5537 15.4368 11.2827 15.7898 10.937 16.0564L11.9697 16.8738L11.0161 17.6824L9.64062 16.575C9.51172 16.5925 9.38281 16.6013 9.25391 16.6013C8.70898 16.6013 8.22559 16.4753 7.80371 16.2234C7.38184 15.9714 7.05225 15.6111 6.81494 15.1423C6.58057 14.6736 6.45898 14.1345 6.4502 13.5251V13.1868C6.4502 12.5569 6.56445 12.0032 6.79297 11.5256C7.02441 11.0481 7.35254 10.679 7.77734 10.4182C8.20508 10.1575 8.69434 10.0271 9.24512 10.0271C9.78711 10.0271 10.2705 10.156 10.6953 10.4138C11.1201 10.6716 11.4497 11.0393 11.6841 11.5168C11.9214 11.9915 12.0415 12.5364 12.0444 13.1516V13.446ZM10.4756 13.178C10.4756 12.5422 10.3687 12.0603 10.1548 11.7322C9.94385 11.4011 9.64062 11.2356 9.24512 11.2356C8.83789 11.2356 8.53174 11.3982 8.32666 11.7234C8.12158 12.0486 8.01758 12.5247 8.01465 13.1516V13.446C8.01465 14.0759 8.11865 14.5593 8.32666 14.8962C8.53467 15.2302 8.84375 15.3972 9.25391 15.3972C9.64648 15.3972 9.94678 15.2317 10.1548 14.9006C10.3657 14.5696 10.4727 14.0935 10.4756 13.4724V13.178ZM14.3735 15.3269H17.0586V16.5134H12.8311V10.115H14.3735V15.3269Z" fill="#A2B1C6"/> d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
2.18843 3.675 1.51343 4.5 1.51343Z"
fill="#A2B1C6"
/>
<path
d="M4.28369 14.8127C4.28369 14.5872 4.20312 14.4114 4.04199 14.2854C3.88379 14.1594
3.604 14.0291 3.20264 13.8943C2.80127 13.7595 2.47314 13.6292 2.21826 13.5032C1.38916
13.0959 0.974609 12.5364 0.974609 11.8245C0.974609 11.47 1.07715 11.158 1.28223
10.8884C1.49023 10.616 1.7832 10.405 2.16113 10.2556C2.53906 10.1033 2.96387 10.0271
3.43555 10.0271C3.89551 10.0271 4.30713 10.1091 4.67041 10.2732C5.03662 10.4373 5.3208
10.6716 5.52295 10.9763C5.7251 11.2781 5.82617 11.6238 5.82617 12.0134H4.28809C4.28809
11.7527 4.20752 11.5505 4.04639 11.407C3.88818 11.2634 3.67285 11.1917 3.40039
11.1917C3.125 11.1917 2.90674 11.2532 2.74561 11.3762C2.5874 11.4963 2.5083
11.6501 2.5083 11.8376C2.5083 12.0017 2.59619 12.1511 2.77197 12.2859C2.94775 12.4177
3.25684 12.5554 3.69922 12.699C4.1416 12.8396 4.50488 12.9919 4.78906 13.156C5.48047
13.5544 5.82617 14.1038 5.82617 14.804C5.82617 15.3635 5.61523 15.803 5.19336
16.1223C4.77148 16.4417 4.19287 16.6013 3.45752 16.6013C2.93896 16.6013 2.46875 16.509
2.04688 16.3245C1.62793 16.137 1.31152 15.8821 1.09766 15.5598C0.886719 15.2346
0.78125 14.8611 0.78125 14.4392H2.32812C2.32812 14.782 2.41602 15.0354 2.5918
15.1995C2.77051 15.3606 3.05908 15.4412 3.45752 15.4412C3.7124 15.4412 3.91309
15.387 4.05957 15.2786C4.20898 15.1672 4.28369 15.012 4.28369 14.8127ZM12.0444
13.446C12.0444 14.0378 11.9463 14.5549 11.75 14.9973C11.5537 15.4368 11.2827 15.7898
10.937 16.0564L11.9697 16.8738L11.0161 17.6824L9.64062 16.575C9.51172 16.5925 9.38281
16.6013 9.25391 16.6013C8.70898 16.6013 8.22559 16.4753 7.80371 16.2234C7.38184 15.9714
7.05225 15.6111 6.81494 15.1423C6.58057 14.6736 6.45898 14.1345 6.4502
13.5251V13.1868C6.4502 12.5569 6.56445 12.0032 6.79297 11.5256C7.02441 11.0481 7.35254
10.679 7.77734 10.4182C8.20508 10.1575 8.69434 10.0271 9.24512 10.0271C9.78711 10.0271
10.2705 10.156 10.6953 10.4138C11.1201 10.6716 11.4497 11.0393 11.6841 11.5168C11.9214
11.9915 12.0415 12.5364 12.0444 13.1516V13.446ZM10.4756 13.178C10.4756 12.5422 10.3687
12.0603 10.1548 11.7322C9.94385 11.4011 9.64062 11.2356 9.24512 11.2356C8.83789 11.2356
8.53174 11.3982 8.32666 11.7234C8.12158 12.0486 8.01758 12.5247 8.01465
13.1516V13.446C8.01465 14.0759 8.11865 14.5593 8.32666 14.8962C8.53467 15.2302 8.84375
15.3972 9.25391 15.3972C9.64648 15.3972 9.94678 15.2317 10.1548 14.9006C10.3657
14.5696 10.4727 14.0935 10.4756 13.4724V13.178ZM14.3735
15.3269H17.0586V16.5134H12.8311V10.115H14.3735V15.3269Z"
fill="#A2B1C6"
/>
</g> </g>
<defs> <defs>
<clipPath id="clip0"> <clipPath id="clip0">

View File

@@ -5,11 +5,38 @@
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
> >
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.5 2.16512L3.5 2.16999C2.675 2.16999 2 2.84499 2 3.66999V14.3349C2 15.1599 2.675 15.8349 3.5 15.8349L14.5 15.83C15.3327 15.83 16 15.1551 16 14.33V3.66512C16 2.83511 15.33 2.16512 14.5 2.16512ZM14.5014 3.66512L3.49878 3.66999V14.33L14.5014 14.3251V3.66512Z" fill="#A2B1C6"/> <path
<path fill-rule="evenodd" clip-rule="evenodd" d="M6.27319 14.7069L6.27319 3.32135L7.77319 3.32135L7.77319 14.7069L6.27319 14.7069Z" fill="#A2B1C6"/> fill-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.2639 14.6932L10.2639 3.30772L11.7639 3.30772L11.7639 14.6932L10.2639 14.6932Z" fill="#A2B1C6"/> clip-rule="evenodd"
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5108 7.48325H2.4895V5.98325H15.5108V7.48325Z" fill="#A2B1C6"/> d="M14.5 2.16512L3.5 2.16999C2.675 2.16999 2 2.84499 2 3.66999V14.3349C2 15.1599 2.675
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.5108 11.6056H2.4895V10.1056H15.5108V11.6056Z" fill="#A2B1C6"/> 15.8349 3.5 15.8349L14.5 15.83C15.3327 15.83 16 15.1551 16 14.33V3.66512C16 2.83511 15.33
2.16512 14.5 2.16512ZM14.5014 3.66512L3.49878 3.66999V14.33L14.5014 14.3251V3.66512Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M6.27319 14.7069L6.27319 3.32135L7.77319 3.32135L7.77319 14.7069L6.27319 14.7069Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M10.2639 14.6932L10.2639 3.30772L11.7639 3.30772L11.7639 14.6932L10.2639 14.6932Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.5108 7.48325H2.4895V5.98325H15.5108V7.48325Z"
fill="#A2B1C6"
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M15.5108 11.6056H2.4895V10.1056H15.5108V11.6056Z"
fill="#A2B1C6"
/>
</svg> </svg>
</template> </template>

View File

@@ -9,7 +9,8 @@
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <path
d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992 5.79997L1.99992 11.2L0.721924 9.93097Z" d="M0.721924 9.93097L4.85292 5.79997L0.721924 1.66897L1.99992 0.399973L7.39992
5.79997L1.99992 11.2L0.721924 9.93097Z"
:fill="expanded ? '#506784' : 'rgba(80, 103, 132, 0.5)'" :fill="expanded ? '#506784' : 'rgba(80, 103, 132, 0.5)'"
/> />
</svg> </svg>

76
src/lib/chartHelper.js Normal file
View File

@@ -0,0 +1,76 @@
import dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export function getOptionsForSave (state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
const emptySources = {}
for (const key in dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
return stateCopy
}
export async function getImageDataUrl (element, type) {
const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, {
format: type,
width: null,
height: null
})
}
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 {
getOptionsFromDataSources,
getOptionsForSave,
getImageDataUrl,
getHtml,
getChartData
}

View File

@@ -42,6 +42,21 @@ export default {
return result return result
}, },
prepareForExport (resultSet) {
const columns = resultSet.columns
const rowCount = resultSet.values[columns[0]].length
const result = {
fields: columns,
data: []
}
for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) {
result.data.push(columns.map(column => resultSet.values[column][rowNumber]))
}
return result
},
parse (file, config = {}) { parse (file, config = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const defaultConfig = { const defaultConfig = {
@@ -58,21 +73,26 @@ export default {
comments: false, comments: false,
step: undefined, step: undefined,
complete: results => { complete: results => {
const res = { let res
data: this.getResult(results), try {
delimiter: results.meta.delimiter, res = {
hasErrors: false, data: this.getResult(results),
rowCount: results.data.length delimiter: results.meta.delimiter,
hasErrors: false,
rowCount: results.data.length
}
res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
if (msg.type === 'error') res.hasErrors = true
msg.hint = hintsByCode[msg.code]
return msg
})
} catch (error) {
reject(error)
} }
res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
if (msg.type === 'error') res.hasErrors = true
msg.hint = hintsByCode[msg.code]
return msg
})
resolve(res) resolve(res)
}, },
error: (error, file) => { error: error => {
reject(error) reject(error)
}, },
download: false, download: false,
@@ -90,5 +110,9 @@ export default {
Papa.parse(file, { ...defaultConfig, ...config }) Papa.parse(file, { ...defaultConfig, ...config })
}) })
},
serialize (resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
} }
} }

View File

@@ -1,5 +1,3 @@
import sqliteParser from 'sqlite-parser'
export default { export default {
* generateChunks (data, size) { * generateChunks (data, size) {
const matrix = Object.keys(data).map(col => data[col]) const matrix = Object.keys(data).map(col => data[col])
@@ -47,49 +45,5 @@ export default {
result = result.replace(/,\s$/, ');') result = result.replace(/,\s$/, ');')
return result return result
},
getAst (sql) {
// There is a bug is sqlite-parser
// It throws an error if tokenizer has an arguments:
// https://github.com/codeschool/sqlite-parser/issues/59
const fixedSql = sql
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
return sqliteParser(fixedSql)
},
/*
* Return an array of columns with name and type. E.g.:
* [
* { name: 'id', type: 'INTEGER' },
* { name: 'title', type: 'NVARCHAR(30)' },
* ]
*/
getColumns (sql) {
const columns = []
const ast = this.getAst(sql)
const columnDefinition = ast.statement[0].format === 'table'
? ast.statement[0].definition
: ast.statement[0].result.args.expression // virtual table
columnDefinition.forEach(item => {
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
let type = item.datatype ? item.datatype.variant : 'N/A'
if (item.datatype && item.datatype.args) {
type = type + '(' + item.datatype.args.expression[0].value
if (item.datatype.args.expression.length === 2) {
type = type + ', ' + item.datatype.args.expression[1].value
}
type = type + ')'
}
columns.push({ name: item.name, type: type })
}
})
return columns
} }
} }

View File

@@ -1,4 +1,3 @@
import stms from './_statements'
import fu from '@/lib/utils/fileIo' import fu from '@/lib/utils/fileIo'
// We can import workers like so because of worker-loader: // We can import workers like so because of worker-loader:
// https://webpack.js.org/loaders/worker-loader/ // https://webpack.js.org/loaders/worker-loader/
@@ -8,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,29 +77,30 @@ class Database {
} }
this.dbName = file ? fu.getFileName(file) : 'database' this.dbName = file ? fu.getFileName(file) : 'database'
this.refreshSchema() await this.refreshSchema()
events.send('database.import', file ? file.size : 0, {
from: file ? 'sqlite' : 'none',
new_db: true
})
} }
async refreshSchema () { async refreshSchema () {
const getSchemaSql = ` const getSchemaSql = `
SELECT name, sql WITH columns as (
FROM sqlite_master SELECT
WHERE type='table' AND name NOT LIKE 'sqlite_%'; a.tbl_name,
json_group_array(
json_object('name', b.name,'type', IIF(b.type = '', 'N/A', b.type))
) as column_json
FROM sqlite_master a, pragma_table_info(a.name) b
WHERE a.type in ('table','view') AND a.name NOT LIKE 'sqlite_%' group by tbl_name
)
SELECT json_group_array(json_object('name',tbl_name, 'columns', json(column_json))) objects
FROM columns;
` `
const result = await this.execute(getSchemaSql) const result = await this.execute(getSchemaSql)
// Parse DDL statements to get column names and types this.schema = JSON.parse(result.values.objects[0])
const parsedSchema = []
if (result && result.values && result.values.name) {
result.values.name.forEach((table, index) => {
parsedSchema.push({
name: table,
columns: stms.getColumns(result.values.sql[index])
})
})
}
// Refresh schema
this.schema = parsedSchema
} }
async execute (commands) { async execute (commands) {
@@ -119,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
@@ -32,17 +33,16 @@ export default {
}, },
isTabNeedName (inquiryTab) { isTabNeedName (inquiryTab) {
const isFromScratch = !inquiryTab.initName return inquiryTab.isPredefined || !inquiryTab.name
return inquiryTab.isPredefined || isFromScratch
}, },
save (inquiryTab, newName) { save (inquiryTab, newName) {
const value = { const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id, id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
query: inquiryTab.query, query: inquiryTab.query,
viewType: inquiryTab.$refs.dataView.mode, viewType: inquiryTab.dataView.mode,
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(), viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.initName name: newName || inquiryTab.name
} }
// Get inquiries from local storage // Get inquiries from local storage
@@ -103,9 +103,19 @@ export default {
importInquiries () { importInquiries () {
return fu.importFile() return fu.importFile()
.then(str => { .then(str => {
return this.deserialiseInquiries(str) const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)
return inquires
}) })
}, },
export (inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length)
},
async readPredefinedInquiries () { async readPredefinedInquiries () {
const res = await fu.readFile('./inquiries.json') const res = await fu.readFile('./inquiries.json')

59
src/lib/tab.js Normal file
View File

@@ -0,0 +1,59 @@
import { nanoid } from 'nanoid'
import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default class Tab {
constructor (state, inquiry = {}) {
this.id = inquiry.id || nanoid()
this.name = inquiry.id ? inquiry.name : null
this.tempName = inquiry.name || (state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.query = inquiry.query
this.viewOptions = inquiry.viewOptions || undefined
this.isPredefined = inquiry.isPredefined
this.viewType = inquiry.viewType || 'chart'
this.result = null
this.isGettingResults = false
this.error = null
this.time = 0
this.layout = inquiry.layout || {
sqlEditor: 'above',
table: 'bottom',
dataView: 'hidden'
}
this.maximize = inquiry.maximize
this.isSaved = !!inquiry.id
this.state = state
}
async execute () {
this.isGettingResults = true
this.result = null
this.error = null
const db = this.state.db
try {
const start = new Date()
this.result = await db.execute(this.query + ';')
this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
this.result.values[this.result.columns[0]].length
)
}
events.send('query.run', parseFloat(this.time), { status: 'success' })
} catch (err) {
this.error = {
type: 'error',
message: err
}
events.send('query.run', 0, { status: 'error' })
}
db.refreshSchema()
this.isGettingResults = false
}
}

View File

@@ -0,0 +1,38 @@
import Lib from 'plotly.js/src/lib'
import dataUrlToBlob from 'dataurl-to-blob'
export default {
async copyCsv (str) {
await navigator.clipboard.writeText(str)
Lib.notifier('CSV copied to clipboard successfully', 'long')
},
async copyImage (source) {
if (source instanceof HTMLCanvasElement) {
return this._copyCanvas(source)
} else {
return this._copyFromDataUrl(source)
}
},
async _copyBlob (blob) {
await navigator.clipboard.write([
new ClipboardItem({ // eslint-disable-line no-undef
[blob.type]: blob
})
])
},
async _copyFromDataUrl (url) {
const blob = dataUrlToBlob(url)
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
},
async _copyCanvas (canvas) {
canvas.toBlob(async (blob) => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
}, 'image/png', 1)
}
}

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

@@ -11,5 +11,11 @@ export default {
clearTimeout(timeout) clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, arguments), ms) timeout = setTimeout(() => func.apply(this, arguments), ms)
} }
},
sleep (ms) {
return new Promise(resolve => {
setTimeout(() => { resolve() }, ms)
})
} }
} }

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

@@ -4,6 +4,7 @@ import Workspace from '@/views/Main/Workspace'
import Inquiries from '@/views/Main/Inquiries' import Inquiries from '@/views/Main/Inquiries'
import Welcome from '@/views/Welcome' import Welcome from '@/views/Welcome'
import Main from '@/views/Main' import Main from '@/views/Main'
import LoadView from '@/views/LoadView'
import store from '@/store' import store from '@/store'
import database from '@/lib/database' import database from '@/lib/database'
@@ -31,6 +32,11 @@ const routes = [
component: Inquiries component: Inquiries
} }
] ]
},
{
path: '/load',
name: 'Load',
component: LoadView
} }
] ]
@@ -39,7 +45,7 @@ const router = new VueRouter({
}) })
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
if (!store.state.db) { if (!store.state.db && to.name !== 'Load') {
const newDb = database.getNewDatabase() const newDb = database.getNewDatabase()
await newDb.loadDb() await newDb.loadDb()
store.commit('setDb', newDb) store.commit('setDb', newDb)

View File

@@ -1,32 +1,17 @@
import { nanoid } from 'nanoid' import Tab from '@/lib/tab'
export default { export default {
async addTab ({ state }, data) { async addTab ({ state }, inquiry = {}) {
const tab = data ? JSON.parse(JSON.stringify(data)) : {} // add new tab only if it was not already opened
// If no data then create a new blank one... if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
// No data.id means to create new tab, but not blank, const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
// e.g. with 'select * from csv_import' inquiry after csv import
if (!data || !data.id) {
tab.id = nanoid()
tab.name = null
tab.tempName = state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled'
tab.viewType = 'chart'
tab.viewOptions = undefined
tab.isSaved = false
} else {
tab.isSaved = true
}
// add new tab only if was not already opened
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
state.tabs.push(tab) state.tabs.push(tab)
if (!tab.name) { if (!tab.name) {
state.untitledLastIndex += 1 state.untitledLastIndex += 1
} }
return tab.id
} }
return tab.id return inquiry.id
} }
} }

View File

@@ -1,5 +1,3 @@
import Vue from 'vue'
export default { export default {
setDb (state, db) { setDb (state, db) {
if (state.db) { if (state.db) {
@@ -8,8 +6,8 @@ export default {
state.db = db state.db = db
}, },
updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) { updateTab (state, { tab, newValues }) {
const tab = state.tabs[index] const { name, id, query, viewType, viewOptions, isSaved } = newValues
const oldId = tab.id const oldId = tab.id
if (id && state.currentTabId === oldId) { if (id && state.currentTabId === oldId) {
@@ -26,32 +24,41 @@ export default {
// Saved inquiry is not predefined // Saved inquiry is not predefined
delete tab.isPredefined delete tab.isPredefined
} }
Vue.set(state.tabs, index, tab)
}, },
deleteTab (state, index) { deleteTab (state, tab) {
const index = state.tabs.indexOf(tab)
// If closing tab is the current opened // If closing tab is the current opened
if (state.tabs[index].id === state.currentTabId) { if (tab.id === state.currentTabId) {
if (index < state.tabs.length - 1) { if (index < state.tabs.length - 1) {
state.currentTabId = state.tabs[index + 1].id state.currentTabId = state.tabs[index + 1].id
} else if (index > 0) { } else if (index > 0) {
state.currentTabId = state.tabs[index - 1].id state.currentTabId = state.tabs[index - 1].id
} else { } else {
state.currentTabId = null state.currentTabId = null
state.currentTab = null
state.untitledLastIndex = 0 state.untitledLastIndex = 0
} }
state.currentTab = state.currentTabId
? state.tabs.find(tab => tab.id === state.currentTabId)
: null
} }
state.tabs.splice(index, 1) state.tabs.splice(index, 1)
}, },
setCurrentTabId (state, id) { setCurrentTabId (state, id) {
state.currentTabId = id try {
}, state.currentTabId = id
setCurrentTab (state, tab) { state.currentTab = state.tabs.find(tab => tab.id === id)
state.currentTab = tab } catch (e) {
console.error('Can\'t open a tab id:' + id)
}
}, },
updatePredefinedInquiries (state, inquiries) { updatePredefinedInquiries (state, inquiries) {
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries] state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
},
setLoadingPredefinedInquiries (state, value) {
state.loadingPredefinedInquiries = value
},
setPredefinedInquiriesLoaded (state, value) {
state.predefinedInquiriesLoaded = value
} }
} }

View File

@@ -4,5 +4,7 @@ export default {
currentTabId: null, currentTabId: null,
untitledLastIndex: 0, untitledLastIndex: 0,
predefinedInquiries: [], predefinedInquiries: [],
loadingPredefinedInquiries: false,
predefinedInquiriesLoaded: false,
db: null db: null
} }

200
src/views/LoadView.vue Normal file
View File

@@ -0,0 +1,200 @@
<template>
<div>
<logs
id="logs"
:messages="messages"
/>
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')">
Open workspace
</button>
</div>
</template>
<script>
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
import Logs from '@/components/Logs'
import events from '@/lib/utils/events'
export default {
name: 'LoadView',
components: {
Logs
},
data () {
return {
newDb: null,
messages: [],
dataMsg: {},
inquiryMsg: {}
}
},
computed: {
hasErrors () {
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
}
},
async created () {
const {
data_url: dataUrl,
data_format: dataFormat,
inquiry_url: inquiryUrl,
inquiry_id: inquiryIds,
maximize
} = this.$route.query
events.send('share.load', null, {
has_data_url: !!dataUrl,
data_format: dataFormat,
has_inquiry_url: !!inquiryUrl,
inquiry_id_count: (inquiryIds || []).length,
maximize
})
await this.loadData(dataUrl, dataFormat)
const inquiries = await this.loadInquiries(inquiryUrl, inquiryIds)
if (inquiries && inquiries.length > 0) {
await this.openInquiries(inquiries, maximize)
}
if (!this.hasErrors) {
this.$router.push('/workspace?hide_schema=1')
}
},
methods: {
async loadData (dataUrl, dataFormat) {
this.newDb = database.getNewDatabase()
if (dataUrl) {
this.dataMsg = {
message: 'Preparing data...',
type: 'info'
}
this.messages.push(this.dataMsg)
// Show loading indicator after 1 second
const loadingDataIndicator = setTimeout(() => {
if (this.dataMsg.type === 'info') {
this.dataMsg.type = 'loading'
}
}, 1000)
if (dataFormat === 'sqlite') {
await this.getSqliteDb(dataUrl)
} else {
this.dataMsg.message = 'Unknown data format'
this.dataMsg.type = 'error'
}
// Loading indicator is not needed anymore
clearTimeout(loadingDataIndicator)
} else {
await this.newDb.loadDb()
}
this.$store.commit('setDb', this.newDb)
},
async getSqliteDb (dataUrl) {
try {
const filename = new URL(dataUrl).pathname.split('/').pop()
const res = await fu.readFile(dataUrl)
if (!res.ok) {
throw new Error('Fetching DB failed')
}
const file = await res.blob()
file.name = filename
await this.newDb.loadDb(file)
this.dataMsg.message = 'Data is ready'
this.dataMsg.type = 'success'
} catch (error) {
console.error(error)
this.dataMsg.message = error
this.dataMsg.type = 'error'
}
},
async loadInquiries (inquiryUrl, inquiryIds = []) {
if (!inquiryUrl) {
return []
}
// Show loading indicator after 1 second
const loadingInquiriesIndicator = setTimeout(() => {
if (this.inquiryMsg.type === 'info') {
this.inquiryMsg.type = 'loading'
}
}, 1000)
try {
this.inquiryMsg = {
message: 'Preparing inquiries...',
type: 'info'
}
this.messages.push(this.inquiryMsg)
const res = await fu.readFile(inquiryUrl)
const file = await res.json()
this.inquiryMsg.message = 'Inquiries are ready'
this.inquiryMsg.type = 'success'
return inquiryIds.length > 0
? file.inquiries.filter(inquiry => inquiryIds.includes(inquiry.id))
: file.inquiries
} catch (error) {
console.error(error)
this.inquiryMsg.message = error
this.inquiryMsg.type = 'error'
}
// Loading indicator is not needed anymore
clearTimeout(loadingInquiriesIndicator)
},
async openInquiries (inquiries, maximize) {
let tabToOpen = null
const layout = maximize ? this.getLayout(maximize) : undefined
for (const inquiry of inquiries) {
const tabId = await this.$store.dispatch('addTab', {
...inquiry,
id: undefined,
layout,
maximize
})
if (!tabToOpen) {
tabToOpen = tabId
this.$store.commit('setCurrentTabId', tabToOpen)
}
}
this.$store.state.currentTab.execute()
},
getLayout (panelToMaximize) {
if (panelToMaximize === 'dataView') {
return {
sqlEditor: 'hidden',
table: 'above',
dataView: 'bottom'
}
} else {
return {
sqlEditor: 'above',
table: 'bottom',
dataView: 'hidden'
}
}
}
}
}
</script>
<style scoped>
#logs {
margin: 8px auto;
max-width: 800px;
}
#open-workspace-btn {
margin: 16px auto;
display: block;
}
</style>

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

@@ -1,12 +1,19 @@
<template> <template>
<div> <div id="my-inquiries-container">
<div id="start-guide" v-if="showedInquiries.length === 0"> <div id="start-guide" v-if="allInquiries.length === 0">
You don't have saved inquiries so far. You don't have saved inquiries so far.
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span> <span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
the one from scratch or the one from scratch or
<span @click="importInquiries" class="link">import</span> from a file. <span @click="importInquiries" class="link">import</span> from a file.
</div> </div>
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="showedInquiries.length > 0"> <div
id="loading-predefined-status"
v-if="$store.state.loadingPredefinedInquiries"
>
<loading-indicator/>
Loading predefined inquiries...
</div>
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
<div id="my-inquiries-toolbar"> <div id="my-inquiries-toolbar">
<div id="toolbar-buttons"> <div id="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries"> <button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
@@ -33,7 +40,12 @@
<text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/> <text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/>
</div> </div>
</div> </div>
<div class="rounded-bg">
<div v-show="showedInquiries.length === 0" id="inquiries-not-found">
No inquiries found
</div>
<div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container"> <div class="header-container">
<div> <div>
<div class="fixed-header" ref="name-th"> <div class="fixed-header" ref="name-th">
@@ -79,7 +91,10 @@
<div class="second-column"> <div class="second-column">
<div class="date-container">{{ inquiry.createdAt | date }}</div> <div class="date-container">{{ inquiry.createdAt | date }}</div>
<div class="icons-container"> <div class="icons-container">
<rename-icon v-if="!inquiry.isPredefined" @click="showRenameDialog(inquiry.id)" /> <rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)"/> <copy-icon @click="duplicateInquiry(index)"/>
<export-icon <export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)" @click="exportToFile([inquiry], `${inquiry.name}.json`)"
@@ -149,9 +164,9 @@ import DeleteIcon from './svg/delete'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import fu from '@/lib/utils/fileIo'
export default { export default {
name: 'Inquiries', name: 'Inquiries',
@@ -162,7 +177,8 @@ export default {
DeleteIcon, DeleteIcon,
CloseIcon, CloseIcon,
TextField, TextField,
CheckBox CheckBox,
LoadingIndicator
}, },
mixins: [tooltipMixin], mixins: [tooltipMixin],
data () { data () {
@@ -200,6 +216,7 @@ export default {
} }
return showedInquiries return showedInquiries
}, },
allInquiries () { allInquiries () {
return this.predefinedInquiries.concat(this.inquiries) return this.predefinedInquiries.concat(this.inquiries)
}, },
@@ -224,15 +241,37 @@ export default {
return `Are you sure you want to delete ${deleteItem}?` return `Are you sure you want to delete ${deleteItem}?`
} }
}, },
created () { watch: {
storedInquiries.readPredefinedInquiries() showedInquiries () {
.then(inquiries => { this.selectedInquiriesIds = new Set(this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
.filter(id => !this.predefinedInquiriesIds.has(id))).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false
this.selectAll = false
}
}
},
async created () {
this.inquiries = storedInquiries.getStoredInquiries()
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try {
this.$store.commit('setLoadingPredefinedInquiries', true)
const inquiries = await storedInquiries.readPredefinedInquiries()
this.$store.commit('updatePredefinedInquiries', inquiries) this.$store.commit('updatePredefinedInquiries', inquiries)
}) this.$store.commit('setPredefinedInquiriesLoaded', true)
.catch(console.error) } catch (e) {
.finally(() => { console.error(e)
this.inquiries = storedInquiries.getStoredInquiries() }
}) this.$store.commit('setLoadingPredefinedInquiries', false)
}
}, },
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight) this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
@@ -264,7 +303,7 @@ export default {
}, },
methods: { methods: {
calcNameWidth () { calcNameWidth () {
const nameWidth = this.$refs['name-td'] const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width ? this.$refs['name-td'][0].getBoundingClientRect().width
: 0 : 0
this.$refs['name-th'].style = `width: ${nameWidth}px` this.$refs['name-th'].style = `width: ${nameWidth}px`
@@ -299,12 +338,14 @@ export default {
storedInquiries.updateStorage(this.inquiries) storedInquiries.updateStorage(this.inquiries)
// update tab, if renamed inquiry is opened // update tab, if renamed inquiry is opened
const tabIndex = this.findTabIndex(processedInquiry.id) const tab = this.$store.state.tabs
if (tabIndex >= 0) { .find(tab => tab.id === processedInquiry.id)
if (tab) {
this.$store.commit('updateTab', { this.$store.commit('updateTab', {
index: tabIndex, tab,
name: this.newName, newValues: {
id: processedInquiry.id name: this.newName
}
}) })
} }
// hide dialog // hide dialog
@@ -312,10 +353,6 @@ export default {
}, },
duplicateInquiry (index) { duplicateInquiry (index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index]) const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
if (this.selectAll) {
this.selectedInquiriesIds.add(newInquiry.id)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
}
this.inquiries.push(newInquiry) this.inquiries.push(newInquiry)
storedInquiries.updateStorage(this.inquiries) storedInquiries.updateStorage(this.inquiries)
}, },
@@ -332,26 +369,29 @@ export default {
this.inquiries.splice(this.processedInquiryIndex, 1) this.inquiries.splice(this.processedInquiryIndex, 1)
// Close deleted inquiry tab if it was opened // Close deleted inquiry tab if it was opened
const tabIndex = this.findTabIndex(this.processedInquiryId) const tab = this.$store.state.tabs
if (tabIndex >= 0) { .find(tab => tab.id === this.processedInquiryId)
this.$store.commit('deleteTab', tabIndex) if (tab) {
this.$store.commit('deleteTab', tab)
} }
// Clear checkboxes // Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) { if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
this.selectedInquiriesIds.delete(this.processedInquiryId) this.selectedInquiriesIds.delete(this.processedInquiryId)
} }
} else { } else {
this.inquiries = this.selectAll this.inquiries = this.inquiries.filter(
? [] inquiry => !this.selectedInquiriesIds.has(inquiry.id)
: this.inquiries.filter(inquiry => !this.selectedInquiriesIds.has(inquiry.id)) )
// Close deleted inquiries if it was opened // Close deleted inquiries if it was opened
const tabs = this.$store.state.tabs const tabs = this.$store.state.tabs
for (let i = tabs.length - 1; i >= 0; i--) { let i = tabs.length - 1
while (i > -1) {
if (this.selectedInquiriesIds.has(tabs[i].id)) { if (this.selectedInquiriesIds.has(tabs[i].id)) {
this.$store.commit('deleteTab', i) this.$store.commit('deleteTab', tabs[i])
} }
i--
} }
// Clear checkboxes // Clear checkboxes
@@ -360,45 +400,40 @@ export default {
this.selectedInquiriesCount = this.selectedInquiriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
storedInquiries.updateStorage(this.inquiries) storedInquiries.updateStorage(this.inquiries)
}, },
findTabIndex (id) {
return this.$store.state.tabs.findIndex(tab => tab.id === id)
},
exportToFile (inquiryList, fileName) { exportToFile (inquiryList, fileName) {
const jsonStr = storedInquiries.serialiseInquiries(inquiryList) storedInquiries.export(inquiryList, fileName)
fu.exportToFile(jsonStr, fileName)
}, },
exportSelectedInquiries () { exportSelectedInquiries () {
const inquiryList = this.selectAll const inquiryList = this.allInquiries.filter(
? this.allInquiries inquiry => this.selectedInquiriesIds.has(inquiry.id)
: this.allInquiries.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id)) )
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json') this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
}, },
importInquiries () { importInquiries () {
storedInquiries.importInquiries() storedInquiries.importInquiries()
.then(importedInquiries => { .then(importedInquiries => {
if (this.selectAll) {
importedInquiries.forEach(inquiry => {
this.selectedInquiriesIds.add(inquiry.id)
})
this.selectedInquiriesCount = this.selectedInquiriesIds.size
}
this.inquiries = this.inquiries.concat(importedInquiries) this.inquiries = this.inquiries.concat(importedInquiries)
storedInquiries.updateStorage(this.inquiries) storedInquiries.updateStorage(this.inquiries)
}) })
}, },
toggleSelectAll (checked) { toggleSelectAll (checked) {
this.selectAll = checked this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked }) this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.selectedInquiriesIds = checked this.selectedInquiriesIds = checked
? new Set(this.allInquiries.map(inquiry => inquiry.id)) ? new Set(this.showedInquiries.map(inquiry => inquiry.id))
: new Set() : new Set()
this.selectedInquiriesCount = this.selectedInquiriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked ? this.inquiries.length : 0 this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
.length
: 0
}, },
toggleRow (checked, id) { toggleRow (checked, id) {
const isPredefined = this.predefinedInquiriesIds.has(id) const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) { if (checked) {
@@ -407,7 +442,7 @@ export default {
this.selectedNotPredefinedCount += 1 this.selectedNotPredefinedCount += 1
} }
} else { } else {
if (this.selectedInquiriesIds.size === this.allInquiries.length) { if (this.selectedInquiriesIds.size === this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false this.$refs.mainCheckBox.checked = false
this.selectAll = false this.selectAll = false
} }
@@ -423,6 +458,21 @@ export default {
</script> </script>
<style scoped> <style scoped>
#my-inquiries-container {
position: relative;
}
#loading-predefined-status {
position: absolute;
right: 0;
display: flex;
gap: 4px;
font-size: 12px;
color: var(--color-text-light-2);
align-items: center;
padding: 8px;
}
#start-guide { #start-guide {
position: absolute; position: absolute;
top: 50%; top: 50%;
@@ -433,6 +483,15 @@ export default {
text-align: center; text-align: center;
} }
#inquiries-not-found {
padding: 35px 5px;
border-radius: 5px;
border: 1px solid var(--color-border-light);
color: var(--color-text-base);
font-size: 14px;
text-align: center;
}
#my-inquiries-content { #my-inquiries-content {
padding: 52px; padding: 52px;
height: 100%; height: 100%;

View File

@@ -11,7 +11,12 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z" d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934
4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934
16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107
16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064
15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064
0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -11,7 +11,10 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z" d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064
15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107
15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25
4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

View File

@@ -11,7 +11,10 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z" d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25
12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825
4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545
4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>

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',
@@ -76,19 +80,10 @@ export default {
return this.$store.state.currentTab return this.$store.state.currentTab
}, },
isSaved () { isSaved () {
if (!this.currentInquiry) { return this.currentInquiry && this.currentInquiry.isSaved
return false
}
const tabIndex = this.currentInquiry.tabIndex
const tab = this.$store.state.tabs[tabIndex]
return tab && tab.isSaved
}, },
isPredefined () { isPredefined () {
if (this.currentInquiry) { return this.currentInquiry && this.currentInquiry.isPredefined
return this.currentInquiry.isPredefined
} else {
return false
}
}, },
runDisabled () { runDisabled () {
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query) return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
@@ -110,6 +105,8 @@ export default {
this.$router.push('/workspace') this.$router.push('/workspace')
} }
}) })
events.send('inquiry.create', null, { auto: false })
}, },
cancelSave () { cancelSave () {
this.$modal.hide('save') this.$modal.hide('save')
@@ -139,13 +136,15 @@ export default {
// Update tab in store // Update tab in store
this.$store.commit('updateTab', { this.$store.commit('updateTab', {
index: this.currentInquiry.tabIndex, tab: this.currentInquiry,
name: value.name, newValues: {
id: value.id, name: value.name,
query: value.query, id: value.id,
viewType: value.viewType, query: value.query,
viewOptions: value.viewOptions, viewType: value.viewType,
isSaved: true viewOptions: value.viewOptions,
isSaved: true
}
}) })
// Restore data: // Restore data:
@@ -163,6 +162,7 @@ export default {
// Signal about saving // Signal about saving
this.$root.$emit('inquirySaved') this.$root.$emit('inquirySaved')
events.send('inquiry.save')
}, },
_keyListener (e) { _keyListener (e) {
if (this.$route.path === '/workspace') { if (this.$route.path === '/workspace') {
@@ -237,4 +237,12 @@ button {
#nav-buttons { #nav-buttons {
display: flex; display: flex;
} }
#nav-links {
display: flex;
align-items: center;
}
#nav-links img {
width: 32px;
}
</style> </style>

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

@@ -1,29 +0,0 @@
import dereference from 'react-chart-editor/lib/lib/dereference'
export function getOptionsFromDataSources (dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export function getOptionsForSave (state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
const emptySources = {}
for (const key in dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
return stateCopy
}
export default {
getOptionsFromDataSources,
getOptionsForSave
}

View File

@@ -28,13 +28,18 @@ import plotly from 'plotly.js'
import 'react-chart-editor/lib/react-chart-editor.min.css' import 'react-chart-editor/lib/react-chart-editor.min.css'
import PlotlyEditor from 'react-chart-editor' import PlotlyEditor from 'react-chart-editor'
import chartHelper from './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'], props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'forPivot'
],
components: { components: {
PlotlyEditor PlotlyEditor
}, },
@@ -55,6 +60,24 @@ 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 && this.state.data && this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
(value) => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
})
},
{ deep: true }
)
},
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer) this.resizeObserver.observe(this.$refs.chartContainer)
@@ -66,7 +89,9 @@ export default {
dataSources () { dataSources () {
// we need to update state.data in order to update the graph // we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948 // https://github.com/plotly/react-chart-editor/issues/948
dereference(this.state.data, this.dataSources) if (this.dataSources) {
dereference(this.state.data, this.dataSources)
}
} }
}, },
methods: { methods: {
@@ -87,10 +112,25 @@ export default {
return chartHelper.getOptionsForSave(this.state, this.dataSources) return chartHelper.getOptionsForSave(this.state, this.dataSources)
}, },
async saveAsPng () { async saveAsPng () {
const chartElement = this.$refs.plotlyEditor.$el.querySelector('.js-plotly-plot') const url = await this.prepareCopy()
const url = await plotly.toImage(chartElement, { format: 'png', width: null, height: null })
this.$emit('loadingImageCompleted') this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'chart') fIo.downloadFromUrl(url, 'chart')
},
async saveAsSvg () {
const url = await this.prepareCopy('svg')
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml () {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
} }
} }
} }

View File

@@ -139,7 +139,7 @@
import $ from 'jquery' import $ from 'jquery'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import PivotSortBtn from './PivotSortBtn' import PivotSortBtn from './PivotSortBtn'
import { renderers, aggregators, zeroValAggregators, twoValAggregators } from './pivotHelper' import { renderers, aggregators, zeroValAggregators, twoValAggregators } from '../pivotHelper'
import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart' import Chart from '@/views/Main/Workspace/Tabs/Tab/DataView/Chart'
import Vue from 'vue' import Vue from 'vue'
const ChartClass = Vue.extend(Chart) const ChartClass = Vue.extend(Chart)
@@ -165,8 +165,11 @@ export default {
colOrder: (this.value && this.value.colOrder) || 'key_a_to_z', colOrder: (this.value && this.value.colOrder) || 'key_a_to_z',
rowOrder: (this.value && this.value.rowOrder) || 'key_a_to_z', rowOrder: (this.value && this.value.rowOrder) || 'key_a_to_z',
customChartComponent: customChartComponent:
(this.value && this.value.rendererOptions && this.value.rendererOptions.customChartComponent) || (
new ChartClass() this.value &&
this.value.rendererOptions &&
this.value.rendererOptions.customChartComponent
) || new ChartClass()
} }
}, },
computed: { computed: {
@@ -222,7 +225,10 @@ export default {
}, },
created () { created () {
this.customChartComponent.$on('update', () => { this.$emit('update') }) this.customChartComponent.$on('update', () => { this.$emit('update') })
this.customChartComponent.$on('loadingImageCompleted', value => { this.$emit('loadingCustomChartImageCompleted') }) this.customChartComponent.$on(
'loadingImageCompleted',
value => { this.$emit('loadingCustomChartImageCompleted') }
)
}, },
methods: { methods: {
returnValue () { returnValue () {

View File

@@ -14,20 +14,21 @@
</template> </template>
<script> <script>
import html2canvas from 'html2canvas'
import plotly from 'plotly.js'
import fIo from '@/lib/utils/fileIo' import fIo from '@/lib/utils/fileIo'
import $ from 'jquery' 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 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 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 {
name: 'pivot', name: 'pivot',
props: ['dataSources', 'initOptions', 'importToPngEnabled'], props: ['dataSources', 'initOptions', 'importToPngEnabled', 'importToSvgEnabled'],
components: { components: {
PivotUi PivotUi
}, },
@@ -53,7 +54,9 @@ export default {
colOrder: this.initOptions.colOrder, colOrder: this.initOptions.colOrder,
rowOrder: this.initOptions.rowOrder, rowOrder: this.initOptions.rowOrder,
aggregatorName: this.initOptions.aggregatorName, aggregatorName: this.initOptions.aggregatorName,
aggregator: $.pivotUtilities.aggregators[this.initOptions.aggregatorName](this.initOptions.vals), aggregator: $.pivotUtilities.aggregators[
this.initOptions.aggregatorName
](this.initOptions.vals),
vals: this.initOptions.vals, vals: this.initOptions.vals,
rendererName: this.initOptions.rendererName, rendererName: this.initOptions.rendererName,
renderer: $.pivotUtilities.renderers[this.initOptions.rendererName], renderer: $.pivotUtilities.renderers[this.initOptions.rendererName],
@@ -68,6 +71,14 @@ export default {
computed: { computed: {
columns () { columns () {
return Object.keys(this.dataSources || {}) return Object.keys(this.dataSources || {})
},
viewStandartChart () {
return this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers
},
viewCustomChart () {
return this.pivotOptions.rendererName === 'Custom chart'
} }
}, },
watch: { watch: {
@@ -77,7 +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(
'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 () {
@@ -98,14 +119,14 @@ export default {
handleResize () { handleResize () {
// hack: plotly changes size only on window.resize event, // hack: plotly changes size only on window.resize event,
// so, we trigger it when container resizes (e.g. when move splitter) // so, we trigger it when container resizes (e.g. when move splitter)
if (this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers) { if (this.viewStandartChart) {
window.dispatchEvent(new Event('resize')) window.dispatchEvent(new Event('resize'))
} }
}, },
show () { show () {
const options = { ...this.pivotOptions } const options = { ...this.pivotOptions }
if (this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers) { if (this.viewStandartChart) {
options.rendererOptions = { options.rendererOptions = {
plotly: { plotly: {
autosize: true, autosize: true,
@@ -135,7 +156,7 @@ export default {
) )
// fix for Firefox: fit plotly renderers just after choosing it in pivotUi // fix for Firefox: fit plotly renderers just after choosing it in pivotUi
if (this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers) { if (this.viewStandartChart) {
window.dispatchEvent(new Event('resize')) window.dispatchEvent(new Event('resize'))
} }
}, },
@@ -153,23 +174,57 @@ export default {
}, },
async saveAsPng () { async saveAsPng () {
if (this.pivotOptions.rendererName === 'Custom chart') { if (this.viewCustomChart) {
this.pivotOptions.rendererOptions.customChartComponent.saveAsPng() this.pivotOptions.rendererOptions.customChartComponent.saveAsPng()
} else if (this.pivotOptions.rendererName in $.pivotUtilities.plotly_renderers) {
const chartElement = this.$refs.pivotOutput.querySelector('.js-plotly-plot')
const url = await plotly.toImage(chartElement, {
format: 'png',
width: null,
height: null
})
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'pivot')
} else { } else {
const tableElement = this.$refs.pivotOutput.querySelector('.pvtTable') const source = this.viewStandartChart
const canvas = await html2canvas(tableElement) ? await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
: (await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)).toDataURL('image/png')
this.$emit('loadingImageCompleted') this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(canvas.toDataURL('image/png'), 'pivot', 'image/png') fIo.downloadFromUrl(source, 'pivot')
} }
},
async prepareCopy () {
if (this.viewCustomChart) {
return await this.pivotOptions.rendererOptions.customChartComponent.prepareCopy()
}
if (this.viewStandartChart) {
return await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'png')
}
return await pivotHelper.getPivotCanvas(this.$refs.pivotOutput)
},
async saveAsSvg () {
if (this.viewCustomChart) {
this.pivotOptions.rendererOptions.customChartComponent.saveAsSvg()
} else if (this.viewStandartChart) {
const url = await chartHelper.getImageDataUrl(this.$refs.pivotOutput, 'svg')
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

@@ -2,6 +2,7 @@ import $ from 'jquery'
import 'pivottable' import 'pivottable'
import 'pivottable/dist/export_renderers.js' import 'pivottable/dist/export_renderers.js'
import 'pivottable/dist/plotly_renderers.js' import 'pivottable/dist/plotly_renderers.js'
import html2canvas from 'html2canvas'
export const zeroValAggregators = [ export const zeroValAggregators = [
'Count', 'Count',
@@ -50,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)
@@ -75,3 +78,45 @@ export const aggregators = Object.keys($.pivotUtilities.aggregators).map(key =>
fun: $.pivotUtilities.aggregators[key] fun: $.pivotUtilities.aggregators[key]
} }
}) })
export async function getPivotCanvas (pivotOutput) {
const tableElement = pivotOutput.querySelector('.pvtTable')
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

@@ -6,6 +6,7 @@
:init-options="mode === initMode ? initOptions : undefined" :init-options="mode === initMode ? initOptions : undefined"
:data-sources="dataSource" :data-sources="dataSource"
:import-to-png-enabled.sync="importToPngEnabled" :import-to-png-enabled.sync="importToPngEnabled"
:import-to-svg-enabled.sync="importToSvgEnabled"
@loadingImageCompleted="loadingImage = false" @loadingImageCompleted="loadingImage = false"
ref="viewComponent" ref="viewComponent"
@update="$emit('update')" @update="$emit('update')"
@@ -40,7 +41,43 @@
> >
<png-icon /> <png-icon />
</icon-button> </icon-button>
<icon-button
:disabled="!importToSvgEnabled"
tooltip="Save as SVG"
tooltip-position="top-left"
@click="saveAsSvg"
>
<export-to-svg-icon />
</icon-button>
<icon-button
tooltip="Save as HTML"
tooltip-position="top-left"
@click="saveAsHtml"
>
<HtmlIcon />
</icon-button>
<icon-button
:loading="copyingImage"
tooltip="Copy visualisation to clipboard"
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
</icon-button>
</side-tool-bar> </side-tool-bar>
<loading-dialog
loadingMsg="Rendering the visualisation..."
successMsg="Image is ready"
actionBtnName="Copy"
name="prepareCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
</div> </div>
</template> </template>
@@ -51,7 +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 PngIcon from '@/components/svg/png' import PngIcon from '@/components/svg/png'
import ClipboardIcon from '@/components/svg/clipboard'
import cIo from '@/lib/utils/clipboardIo'
import loadingDialog from '@/components/LoadingDialog'
import time from '@/lib/utils/time'
import events from '@/lib/utils/events'
export default { export default {
name: 'DataView', name: 'DataView',
@@ -63,13 +107,26 @@ export default {
IconButton, IconButton,
ChartIcon, ChartIcon,
PivotIcon, PivotIcon,
PngIcon ExportToSvgIcon,
PngIcon,
HtmlIcon,
ClipboardIcon,
loadingDialog
}, },
data () { data () {
return { return {
mode: this.initMode || 'chart', mode: this.initMode || 'chart',
importToPngEnabled: true, importToPngEnabled: true,
loadingImage: false importToSvgEnabled: true,
loadingImage: false,
copyingImage: false,
preparingCopy: false,
dataToCopy: null
}
},
computed: {
plotlyInPivot () {
return this.mode === 'pivot' && this.$refs.viewComponent.viewCustomChart
} }
}, },
watch: { watch: {
@@ -82,19 +139,81 @@ export default {
async saveAsPng () { async saveAsPng () {
this.loadingImage = true this.loadingImage = true
/* /*
setTimeout does its thing by putting its callback on the callback queue. The callback queue is only called by the browser after both the call stack and the render queue are done. So our animation (which is on the call stack) gets done, the render queue renders it, and then the browser is ready for the callback queue and calls the long-calculation. setTimeout does its thing by putting its callback on the callback queue.
The callback queue is only called by the browser after both the call stack
and the render queue are done. So our animation (which is on the call stack) gets done,
the render queue renders it, and then the browser is ready for the callback queue
and calls the long-calculation.
nextTick allows you to do something after you have changed the data and VueJS has updated the DOM based on your data change, but before the browser has rendered those changed on the page. nextTick allows you to do something after you have changed the data
and VueJS has updated the DOM based on your data change,
but before the browser has rendered those changed on the page.
Lees meer van Katinka Hesselink: http://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/ http://www.hesselinkwebdesign.nl/2019/nexttick-vs-settimeout-in-vue/
*/ */
setTimeout(() => { await time.sleep(0)
this.$refs.viewComponent.saveAsPng() this.$refs.viewComponent.saveAsPng()
}, 0) this.exportSignal('png')
}, },
getOptionsForSave () { getOptionsForSave () {
return this.$refs.viewComponent.getOptionsForSave() return this.$refs.viewComponent.getOptionsForSave()
},
async prepareCopy () {
if ('ClipboardItem' in window) {
this.preparingCopy = true
this.$modal.show('prepareCopy')
const t0 = performance.now()
await time.sleep(0)
this.dataToCopy = await this.$refs.viewComponent.prepareCopy()
const t1 = performance.now()
if ((t1 - t0) < 950) {
this.$modal.hide('prepareCopy')
this.copyToClipboard()
} else {
this.preparingCopy = false
}
} else {
alert(
"Your browser doesn't support copying images into the clipboard. " +
'If you use Firefox you can enable it ' +
'by setting dom.events.asyncClipboard.clipboardItem to true.'
)
}
},
async copyToClipboard () {
cIo.copyImage(this.dataToCopy)
this.$modal.hide('prepareCopy')
this.exportSignal('clipboard')
},
cancelCopy () {
this.dataToCopy = null
this.$modal.hide('prepareCopy')
},
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
)
} }
} }
} }
@@ -115,4 +234,19 @@ export default {
height: 100%; height: 100%;
overflow: auto; overflow: auto;
} }
>>>.vm--container {
animation: show-modal 1s linear 0s 1;
}
@keyframes show-modal {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style> </style>

View File

@@ -26,7 +26,36 @@
class="straight" class="straight"
/> />
</div> </div>
<side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table"/> <side-tool-bar @switchTo="$emit('switchTo', $event)" panel="table">
<icon-button
:disabled="!result"
tooltip="Export result set to CSV file"
tooltip-position="top-left"
@click="exportToCsv"
>
<export-to-csv-icon/>
</icon-button>
<icon-button
:disabled="!result"
tooltip="Copy result set to clipboard"
tooltip-position="top-left"
@click="prepareCopy"
>
<clipboard-icon/>
</icon-button>
</side-tool-bar>
<loading-dialog
loadingMsg="Building CSV..."
successMsg="CSV is ready"
actionBtnName="Copy"
name="prepareCSVCopy"
title="Copy to clipboard"
:loading="preparingCopy"
@action="copyToClipboard"
@cancel="cancelCopy"
/>
</div> </div>
</template> </template>
@@ -35,6 +64,15 @@ 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 ExportToCsvIcon from '@/components/svg/exportToCsv'
import ClipboardIcon from '@/components/svg/clipboard'
import IconButton from '@/components/IconButton'
import csv from '@/lib/csv'
import fIo from '@/lib/utils/fileIo'
import cIo from '@/lib/utils/clipboardIo'
import time from '@/lib/utils/time'
import loadingDialog from '@/components/LoadingDialog'
import events from '@/lib/utils/events'
export default { export default {
name: 'RunResult', name: 'RunResult',
@@ -42,14 +80,20 @@ export default {
data () { data () {
return { return {
resizeObserver: null, resizeObserver: null,
pageSize: 20 pageSize: 20,
preparingCopy: false,
dataToCopy: null
} }
}, },
components: { components: {
SqlTable, SqlTable,
LoadingIndicator, LoadingIndicator,
Logs, Logs,
SideToolBar SideToolBar,
ExportToCsvIcon,
IconButton,
ClipboardIcon,
loadingDialog
}, },
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize) this.resizeObserver = new ResizeObserver(this.handleResize)
@@ -63,6 +107,7 @@ export default {
handleResize () { handleResize () {
this.calculatePageSize() this.calculatePageSize()
}, },
calculatePageSize () { calculatePageSize () {
const runResultPanel = this.$refs.runResultPanel const runResultPanel = this.$refs.runResultPanel
// 27 - table footer hight // 27 - table footer hight
@@ -70,6 +115,58 @@ export default {
// 35 - height of table header // 35 - height of table header
const freeSpace = runResultPanel.offsetHeight - 27 - 5 - 35 const freeSpace = runResultPanel.offsetHeight - 27 - 5 - 35
this.pageSize = Math.max(Math.floor(freeSpace / 35), 20) this.pageSize = Math.max(Math.floor(freeSpace / 35), 20)
},
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')
},
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) {
this.preparingCopy = true
this.$modal.show('prepareCSVCopy')
const t0 = performance.now()
await time.sleep(0)
this.dataToCopy = csv.serialize(this.result)
const t1 = performance.now()
if ((t1 - t0) < 950) {
this.$modal.hide('prepareCSVCopy')
this.copyToClipboard()
} else {
this.preparingCopy = false
}
} else {
alert(
"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.'
)
}
},
copyToClipboard () {
cIo.copyCsv(this.dataToCopy)
this.$modal.hide('prepareCSVCopy')
},
cancelCopy () {
this.dataToCopy = null
this.$modal.hide('prepareCSVCopy')
} }
} }
} }
@@ -127,4 +224,20 @@ export default {
opacity: 1; opacity: 1;
} }
} }
>>>.vm--container {
animation: show-modal 1s linear 0s 1;
}
@keyframes show-modal {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
</style> </style>

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

@@ -3,44 +3,45 @@
<splitpanes <splitpanes
class="query-results-splitter" class="query-results-splitter"
horizontal horizontal
:before="{ size: 50, max: 100 }" :before="{ size: topPaneSize, max: 100 }"
:after="{ size: 50, max: 100 }" :after="{ size: 100 - topPaneSize, max: 100 }"
:default="{ before: 50, after: 50 }"
> >
<template #left-pane> <template #left-pane>
<div :id="'above-' + tabIndex" class="above" /> <div :id="'above-' + tab.id" class="above" />
</template> </template>
<template #right-pane> <template #right-pane>
<div :id="'bottom-'+ tabIndex" ref="bottomPane" class="bottomPane" /> <div :id="'bottom-'+ tab.id" ref="bottomPane" class="bottomPane" />
</template> </template>
</splitpanes> </splitpanes>
<div :id="'hidden-'+ tabIndex" class="hidden-part" /> <div :id="'hidden-'+ tab.id" class="hidden-part" />
<teleport :to="`#${layout.sqlEditor}-${tabIndex}`"> <teleport :to="`#${tab.layout.sqlEditor}-${tab.id}`">
<sql-editor <sql-editor
ref="sqlEditor" ref="sqlEditor"
v-model="query" v-model="tab.query"
:is-getting-results="isGettingResults" :is-getting-results="tab.isGettingResults"
@switchTo="onSwitchView('sqlEditor', $event)" @switchTo="onSwitchView('sqlEditor', $event)"
@run="execute" @run="tab.execute()"
/> />
</teleport> </teleport>
<teleport :to="`#${layout.table}-${tabIndex}`"> <teleport :to="`#${tab.layout.table}-${tab.id}`">
<run-result <run-result
:result="result" :result="tab.result"
:is-getting-results="isGettingResults" :is-getting-results="tab.isGettingResults"
:error="error" :error="tab.error"
:time="time" :time="tab.time"
@switchTo="onSwitchView('table', $event)" @switchTo="onSwitchView('table', $event)"
/> />
</teleport> </teleport>
<teleport :to="`#${layout.dataView}-${tabIndex}`"> <teleport :to="`#${tab.layout.dataView}-${tab.id}`">
<data-view <data-view
:data-source="(result && result.values) || null" :data-source="(tab.result && tab.result.values) || null"
:init-options="initViewOptions" :init-options="tab.viewOptions"
:init-mode="initViewType" :init-mode="tab.viewType"
ref="dataView" ref="dataView"
@switchTo="onSwitchView('dataView', $event)" @switchTo="onSwitchView('dataView', $event)"
@update="onDataViewUpdate" @update="onDataViewUpdate"
@@ -54,12 +55,15 @@ import Splitpanes from '@/components/Splitpanes'
import SqlEditor from './SqlEditor' import SqlEditor from './SqlEditor'
import DataView from './DataView' import DataView from './DataView'
import RunResult from './RunResult' import RunResult from './RunResult'
import time from '@/lib/utils/time'
import Teleport from 'vue2-teleport' import Teleport from 'vue2-teleport'
import events from '@/lib/utils/events'
export default { export default {
name: 'Tab', name: 'Tab',
props: ['id', 'initName', 'initQuery', 'initViewOptions', 'tabIndex', 'isPredefined', 'initViewType'], props: {
tab: Object
},
components: { components: {
SqlEditor, SqlEditor,
DataView, DataView,
@@ -69,21 +73,14 @@ export default {
}, },
data () { data () {
return { return {
query: this.initQuery, topPaneSize: this.tab.maximize
result: null, ? this.tab.layout[this.tab.maximize] === 'above' ? 100 : 0
isGettingResults: false, : 50
error: null,
time: 0,
layout: {
sqlEditor: 'above',
table: 'bottom',
dataView: 'hidden'
}
} }
}, },
computed: { computed: {
isActive () { isActive () {
return this.id === this.$store.state.currentTabId return this.tab.id === this.$store.state.currentTabId
} }
}, },
watch: { watch: {
@@ -91,42 +88,34 @@ export default {
immediate: true, immediate: true,
async handler () { async handler () {
if (this.isActive) { if (this.isActive) {
this.$store.commit('setCurrentTab', this)
await this.$nextTick() await this.$nextTick()
this.$refs.sqlEditor.focus() this.$refs.sqlEditor.focus()
} }
} }
}, },
query () { 'tab.query' () {
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false }) this.$store.commit('updateTab', {
tab: this.tab,
newValues: { isSaved: false }
})
} }
}, },
mounted () {
this.tab.dataView = this.$refs.dataView
},
methods: { methods: {
onSwitchView (from, to) { onSwitchView (from, to) {
const fromPosition = this.layout[from] const fromPosition = this.tab.layout[from]
this.layout[from] = this.layout[to] this.tab.layout[from] = this.tab.layout[to]
this.layout[to] = fromPosition this.tab.layout[to] = fromPosition
events.send('inquiry.panel', null, { panel: to })
}, },
onDataViewUpdate () { onDataViewUpdate () {
this.$store.commit('updateTab', { index: this.tabIndex, isSaved: false }) this.$store.commit('updateTab', {
}, tab: this.tab,
async execute () { newValues: { isSaved: false }
this.isGettingResults = true })
this.result = null
this.error = null
const state = this.$store.state
try {
const start = new Date()
this.result = await state.db.execute(this.query + ';')
this.time = time.getPeriod(start, new Date())
} catch (err) {
this.error = {
type: 'error',
message: err
}
}
state.db.refreshSchema()
this.isGettingResults = false
} }
} }
} }

View File

@@ -5,7 +5,7 @@
v-for="(tab, index) in tabs" v-for="(tab, index) in tabs"
:key="index" :key="index"
@click="selectTab(tab.id)" @click="selectTab(tab.id)"
:class="[{'tab-selected': (tab.id === selectedIndex)}, 'tab']" :class="[{'tab-selected': (tab.id === selectedTabId)}, 'tab']"
> >
<div class="tab-name"> <div class="tab-name">
<span v-show="!tab.isSaved" class="star">*</span> <span v-show="!tab.isSaved" class="star">*</span>
@@ -13,20 +13,14 @@
<span v-else class="tab-untitled">{{ tab.tempName }}</span> <span v-else class="tab-untitled">{{ tab.tempName }}</span>
</div> </div>
<div> <div>
<close-icon class="close-icon" :size="10" @click="beforeCloseTab(index)"/> <close-icon class="close-icon" :size="10" @click="beforeCloseTab(tab)"/>
</div> </div>
</div> </div>
</div> </div>
<tab <tab
v-for="(tab, index) in tabs" v-for="tab in tabs"
:key="tab.id" :key="tab.id"
:id="tab.id" :tab="tab"
:init-name="tab.name"
:init-query="tab.query"
:init-view-options="tab.viewOptions"
:init-view-type="tab.viewType"
:is-predefined="tab.isPredefined"
:tab-index="index"
/> />
<div v-show="tabs.length === 0" id="start-guide"> <div v-show="tabs.length === 0" id="start-guide">
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span> <span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
@@ -38,25 +32,25 @@
<modal name="close-warn" classes="dialog" height="auto"> <modal name="close-warn" classes="dialog" height="auto">
<div class="dialog-header"> <div class="dialog-header">
Close tab {{ Close tab {{
closingTabIndex !== null closingTab !== null
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`) ? (closingTab.name || `[${closingTab.tempName}]`)
: '' : ''
}} }}
<close-icon @click="$modal.hide('close-warn')"/> <close-icon @click="$modal.hide('close-warn')"/>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
You have unsaved changes. Save changes in {{ You have unsaved changes. Save changes in {{
closingTabIndex !== null closingTab !== null
? (tabs[closingTabIndex].name || `[${tabs[closingTabIndex].tempName}]`) ? (closingTab.name || `[${closingTab.tempName}]`)
: '' : ''
}} before closing? }} before closing?
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button class="secondary" @click="closeTab(closingTabIndex)"> <button class="secondary" @click="closeTab(closingTab)">
Close without saving Close without saving
</button> </button>
<button class="secondary" @click="$modal.hide('close-warn')">Cancel</button> <button class="secondary" @click="$modal.hide('close-warn')">Cancel</button>
<button class="primary" @click="saveAndClose(closingTabIndex)">Save and close</button> <button class="primary" @click="saveAndClose(closingTab)">Save and close</button>
</div> </div>
</modal> </modal>
</div> </div>
@@ -73,14 +67,14 @@ export default {
}, },
data () { data () {
return { return {
closingTabIndex: null closingTab: null
} }
}, },
computed: { computed: {
tabs () { tabs () {
return this.$store.state.tabs return this.$store.state.tabs
}, },
selectedIndex () { selectedTabId () {
return this.$store.state.currentTabId return this.$store.state.currentTabId
} }
}, },
@@ -97,25 +91,24 @@ export default {
selectTab (id) { selectTab (id) {
this.$store.commit('setCurrentTabId', id) this.$store.commit('setCurrentTabId', id)
}, },
beforeCloseTab (index) { beforeCloseTab (tab) {
this.closingTabIndex = index this.closingTab = tab
if (!this.tabs[index].isSaved) { if (!tab.isSaved) {
this.$modal.show('close-warn') this.$modal.show('close-warn')
} else { } else {
this.closeTab(index) this.closeTab(tab)
} }
}, },
closeTab (index) { closeTab (tab) {
this.$modal.hide('close-warn') this.$modal.hide('close-warn')
this.closingTabIndex = null this.$store.commit('deleteTab', tab)
this.$store.commit('deleteTab', index)
}, },
saveAndClose (index) { saveAndClose (tab) {
this.$root.$on('inquirySaved', () => { this.$root.$on('inquirySaved', () => {
this.closeTab(index) this.closeTab(tab)
this.$root.$off('inquirySaved') this.$root.$off('inquirySaved')
}) })
this.selectTab(this.tabs[index].id) this.selectTab(tab.id)
this.$modal.hide('close-warn') this.$modal.hide('close-warn')
this.$nextTick(() => { this.$nextTick(() => {
this.$root.$emit('saveInquiry') this.$root.$emit('saveInquiry')

View File

@@ -2,8 +2,9 @@
<div> <div>
<splitpanes <splitpanes
class="schema-tabs-splitter" class="schema-tabs-splitter"
:before="{ size: 20, max: 30 }" :before="{ size: schemaWidth, max: 30 }"
:after="{ size: 80, max: 100 }" :after="{ size: 100 - schemaWidth, max: 100 }"
:default="{ before: 20, after: 80 }"
> >
<template #left-pane> <template #left-pane>
<schema/> <schema/>
@@ -19,6 +20,7 @@
import Splitpanes from '@/components/Splitpanes' import Splitpanes from '@/components/Splitpanes'
import Schema from './Schema' import Schema from './Schema'
import Tabs from './Tabs' import Tabs from './Tabs'
import events from '@/lib/utils/events'
export default { export default {
name: 'Workspace', name: 'Workspace',
@@ -27,9 +29,14 @@ export default {
Splitpanes, Splitpanes,
Tabs Tabs
}, },
data () {
return {
schemaWidth: this.$route.query.hide_schema === '1' ? 0 : 20
}
},
async beforeCreate () { async beforeCreate () {
const schema = this.$store.state.db.schema const schema = this.$store.state.db.schema
if (!schema || schema.length === 0) { if ((!schema || schema.length === 0) && this.$store.state.tabs.length === 0) {
const stmt = [ const stmt = [
'/*', '/*',
' * Your database is empty. In order to start building charts', ' * Your database is empty. In order to start building charts',
@@ -49,6 +56,8 @@ export default {
const tabId = await this.$store.dispatch('addTab', { query: stmt }) const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId) this.$store.commit('setCurrentTabId', tabId)
events.send('inquiry.create', null, { auto: true })
} }
} }
} }

View File

@@ -3,7 +3,7 @@ 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 CsvImport from '@/components/CsvImport'
import csv from '@/components/CsvImport/csv' import csv from '@/lib/csv'
describe('CsvImport.vue', () => { describe('CsvImport.vue', () => {
let state = {} let state = {}
@@ -167,7 +167,10 @@ describe('CsvImport.vue', () => {
expect(rows.at(0).findAll('td').at(0).text()).to.equal('baz') expect(rows.at(0).findAll('td').at(0).text()).to.equal('baz')
expect(rows.at(0).findAll('td').at(1).text()).to.equal('3') expect(rows.at(0).findAll('td').at(1).text()).to.equal('3')
expect(wrapper.findComponent({ name: 'logs' }).text()) expect(wrapper.findComponent({ name: 'logs' }).text())
.to.contain('Error in row 0. Quote is missed. Edit your CSV so that the field has a closing quote char.') .to.contain(
'Error in row 0. Quote is missed. ' +
'Edit your CSV so that the field has a closing quote char.'
)
expect(wrapper.findComponent({ name: 'logs' }).text()) expect(wrapper.findComponent({ name: 'logs' }).text())
.to.not.contain('Preview parsing is completed in') .to.not.contain('Preview parsing is completed in')
@@ -222,8 +225,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1 rowCount: 1
}) })
@@ -275,8 +281,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -286,8 +295,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: false, hasErrors: false,
@@ -330,8 +342,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -341,8 +356,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: false, hasErrors: false,
@@ -392,8 +410,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -403,8 +424,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: true, hasErrors: true,
@@ -448,8 +472,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -459,8 +486,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: false, hasErrors: false,
@@ -515,8 +545,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -526,8 +559,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: false, hasErrors: false,
@@ -564,8 +600,11 @@ describe('CsvImport.vue', () => {
parse.onCall(0).resolves({ parse.onCall(0).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -575,8 +614,11 @@ describe('CsvImport.vue', () => {
parse.onCall(1).resolves({ parse.onCall(1).resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1, 2], columns: ['col1', 'col2'],
col2: ['foo', 'bar'] values: {
col1: [1, 2],
col2: ['foo', 'bar']
}
}, },
rowCount: 2, rowCount: 2,
hasErrors: false, hasErrors: false,
@@ -615,8 +657,11 @@ describe('CsvImport.vue', () => {
sinon.stub(csv, 'parse').resolves({ sinon.stub(csv, 'parse').resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,
@@ -643,8 +688,11 @@ describe('CsvImport.vue', () => {
sinon.stub(csv, 'parse').resolves({ sinon.stub(csv, 'parse').resolves({
delimiter: '|', delimiter: '|',
data: { data: {
col1: [1], columns: ['col1', 'col2'],
col2: ['foo'] values: {
col1: [1],
col2: ['foo']
}
}, },
rowCount: 1, rowCount: 1,
hasErrors: false, hasErrors: false,

View File

@@ -19,6 +19,7 @@ describe('LoadingIndicator.vue', () => {
}) })
// The lendth of circle in the component is 50.24. If progress is 50% then resulting arc // The lendth of circle in the component is 50.24. If progress is 50% then resulting arc
// should be 25.12 // should be 25.12
expect(wrapper.find('.loader-svg.front').element.style.strokeDasharray).to.equal('25.12px, 25.12px') expect(wrapper.find('.loader-svg.front').element.style.strokeDasharray)
.to.equal('25.12px, 25.12px')
}) })
}) })

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

@@ -40,7 +40,7 @@ describe('Splitpanes.vue', () => {
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%') expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.height).to.equal('40%')
}) })
it('toggles correctly', async () => { it('toggles correctly - no maximized initially', async () => {
// mount the component // mount the component
const wrapper = shallowMount(Splitpanes, { const wrapper = shallowMount(Splitpanes, {
slots: { slots: {
@@ -70,6 +70,64 @@ describe('Splitpanes.vue', () => {
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%') expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('40%')
}) })
it('toggles correctly - with maximized initially', async () => {
// mount the component
let wrapper = shallowMount(Splitpanes, {
slots: {
leftPane: '<div />',
rightPane: '<div />'
},
propsData: {
before: { size: 0, max: 100 },
after: { size: 100, max: 100 },
default: { before: 20, after: 80 }
}
})
await wrapper.find('.toggle-btn').trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
await wrapper.find('.toggle-btn').trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('20%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('80%')
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
wrapper = shallowMount(Splitpanes, {
slots: {
leftPane: '<div />',
rightPane: '<div />'
},
propsData: {
before: { size: 100, max: 100 },
after: { size: 0, max: 100 }
}
})
await wrapper.find('.toggle-btn').trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
await wrapper.findAll('.toggle-btn').at(0).trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('0%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('100%')
await wrapper.find('.toggle-btn').trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('50%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('50%')
await wrapper.findAll('.toggle-btn').at(1).trigger('click')
expect(wrapper.findAll('.splitpanes-pane').at(0).element.style.width).to.equal('100%')
expect(wrapper.findAll('.splitpanes-pane').at(1).element.style.width).to.equal('0%')
})
it('drag - vertical', async () => { it('drag - vertical', async () => {
const root = document.createElement('div') const root = document.createElement('div')
const place = document.createElement('div') const place = document.createElement('div')

View File

@@ -0,0 +1,103 @@
import { expect } from 'chai'
import sinon from 'sinon'
import * as chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
describe('chartHelper.js', () => {
afterEach(() => {
sinon.restore()
})
it('getOptionsFromDataSources', () => {
const dataSources = {
id: [1, 2],
name: ['foo', 'bar']
}
const ds = chartHelper.getOptionsFromDataSources(dataSources)
expect(ds).to.eql([
{ value: 'id', label: 'id' },
{ value: 'name', label: 'name' }
])
})
it('getOptionsForSave', () => {
const state = {
data: {
foo: {},
bar: {}
},
layout: {},
frames: {}
}
const dataSources = {
id: [1, 2],
name: ['foo', 'bar']
}
sinon.stub(dereference, 'default')
sinon.spy(JSON, 'parse')
const ds = chartHelper.getOptionsForSave(state, dataSources)
expect(dereference.default.calledOnce).to.equal(true)
const args = dereference.default.firstCall.args
expect(args[0]).to.eql({
foo: {},
bar: {}
})
expect(args[1]).to.eql({
id: [],
name: []
})
expect(ds).to.equal(JSON.parse.returnValues[0])
})
it('getImageDataUrl returns dataUrl', async () => {
const element = document.createElement('div')
const child = document.createElement('div')
element.append(child)
child.classList.add('js-plotly-plot')
let url = await chartHelper.getImageDataUrl(element, 'png')
expect(/^data:image\/png/.test(url)).to.equal(true)
url = await chartHelper.getImageDataUrl(element, 'svg')
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

@@ -1,6 +1,6 @@
import { expect } from 'chai' import { expect } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
import csv from '@/components/CsvImport/csv' import csv from '@/lib/csv'
import Papa from 'papaparse' import Papa from 'papaparse'
describe('csv.js', () => { describe('csv.js', () => {
@@ -115,4 +115,59 @@ describe('csv.js', () => {
const file = {} const file = {}
await expect(csv.parse(file)).to.be.rejectedWith(err) await expect(csv.parse(file)).to.be.rejectedWith(err)
}) })
it('parse rejects when getResult failed', async () => {
let err
try {
new Date('invalid date').toISOString()
} catch (e) {
err = e // get error message, it's different depending on browser
}
sinon.stub(Papa, 'parse').callsFake((file, config) => {
config.complete({
data: [
[1, new Date('invalid date')],
[2, new Date('2023-05-05T15:30:00Z')]
],
errors: [],
meta: {
delimiter: ',',
linebreak: '\n',
aborted: false,
truncated: true
}
})
})
const file = {}
await expect(csv.parse(file)).to.be.rejectedWith(err.message)
})
it('prepareForExport', () => {
const resultSet = {
columns: ['id', 'name'],
values: {
id: [1, 2],
name: ['foo', 'bar']
}
}
expect(csv.prepareForExport(resultSet)).to.eql({
fields: ['id', 'name'],
data: [
[1, 'foo'],
[2, 'bar']
]
})
})
it('serialize returns tsv text', () => {
const resultSet = {
columns: ['id', 'name'],
values: {
id: [1, 2],
name: ['foo', 'bar']
}
}
expect(csv.serialize(resultSet)).to.eql('id\tname\r\n1\tfoo\r\n2\tbar')
})
}) })

View File

@@ -35,32 +35,4 @@ describe('_statements.js', () => {
'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);' 'CREATE table "foo"("id" REAL, "name" TEXT, "isAdmin" INTEGER, "startDate" TEXT);'
) )
}) })
it('getColumns', () => {
const sql = `CREATE TABLE test (
col1,
col2 integer,
col3 decimal(5,2),
col4 varchar(30)
)`
expect(stmts.getColumns(sql)).to.eql([
{ name: 'col1', type: 'N/A' },
{ name: 'col2', type: 'integer' },
{ name: 'col3', type: 'decimal(5, 2)' },
{ name: 'col4', type: 'varchar(30)' }
])
})
it('getColumns with virtual table', async () => {
const sql = `
CREATE VIRTUAL TABLE test_virtual USING fts4(
col1, col2,
notindexed=col1, notindexed=col2,
tokenize=unicode61 "tokenchars=.+#")
`
expect(stmts.getColumns(sql)).to.eql([
{ name: 'col1', type: 'N/A' },
{ name: 'col2', type: 'N/A' }
])
})
}) })

View File

@@ -44,7 +44,37 @@ 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 () => {
await db.loadDb()
await db.execute(`
CREATE TABLE test (col1, col2 integer);
CREATE VIEW test_view AS SELECT col2 as amount FROM test;
`)
await db.refreshSchema()
const schema = db.schema
expect(db.dbName).to.equal('database')
expect(schema).to.have.lengthOf(2)
expect(schema[0].name).to.equal('test')
expect(schema[1].name).to.equal('test_view')
expect(schema[0].columns[0]).to.eql({
name: 'col1',
type: 'N/A'
})
expect(schema[0].columns[1]).to.eql({
name: 'col2',
type: 'INTEGER'
})
expect(schema[1].columns).to.eql([{
name: 'amount',
type: 'INTEGER'
}])
}) })
it('creates empty db with name database', async () => { it('creates empty db with name database', async () => {
@@ -133,9 +163,9 @@ describe('database.js', () => {
expect(db.schema).to.have.lengthOf(1) expect(db.schema).to.have.lengthOf(1)
expect(db.schema[0].name).to.equal('foo') expect(db.schema[0].name).to.equal('foo')
expect(db.schema[0].columns).to.have.lengthOf(3) expect(db.schema[0].columns).to.have.lengthOf(3)
expect(db.schema[0].columns[0]).to.eql({ name: 'id', type: 'real' }) expect(db.schema[0].columns[0]).to.eql({ name: 'id', type: 'REAL' })
expect(db.schema[0].columns[1]).to.eql({ name: 'name', type: 'text' }) expect(db.schema[0].columns[1]).to.eql({ name: 'name', type: 'TEXT' })
expect(db.schema[0].columns[2]).to.eql({ name: 'faculty', type: 'text' }) expect(db.schema[0].columns[2]).to.eql({ name: 'faculty', type: 'TEXT' })
const result = await db.execute('SELECT * from foo') const result = await db.execute('SELECT * from foo')
expect(result).to.eql(data) expect(result).to.eql(data)

View File

@@ -160,39 +160,39 @@ describe('SQLite extensions', function () {
it('supports transitive_closure', async function () { it('supports transitive_closure', async function () {
const actual = await db.execute(` const actual = await db.execute(`
CREATE TABLE node( CREATE TABLE node(
node_id INTEGER NOT NULL PRIMARY KEY, node_id INTEGER NOT NULL PRIMARY KEY,
parent_id INTEGER, parent_id INTEGER,
name VARCHAR(127), name VARCHAR(127),
FOREIGN KEY (parent_id) REFERENCES node(node_id) FOREIGN KEY (parent_id) REFERENCES node(node_id)
);
CREATE INDEX node_parent_id_idx ON node(parent_id);
CREATE VIRTUAL TABLE node_closure USING transitive_closure(
tablename = "node",
idcolumn = "node_id",
parentcolumn = "parent_id"
); );
CREATE INDEX node_parent_id_idx ON node(parent_id);
INSERT INTO node VALUES CREATE VIRTUAL TABLE node_closure USING transitive_closure(
(1, NULL, 'tests'), tablename = "node",
(2, 1, 'lib'), idcolumn = "node_id",
(3, 2, 'database'), parentcolumn = "parent_id"
(4, 2, 'utils'), );
(5, 2, 'storedQueries.spec.js'),
(6, 3, '_sql.spec.js'),
(7, 3, '_statements.spec.js'),
(8, 3, 'database.spec.js'),
(9, 3, 'sqliteExtensions.spec.js'),
(10, 4, 'fileIo.spec.js'),
(11, 4, 'time.spec.js');
SELECT name INSERT INTO node VALUES
FROM node (1, NULL, 'tests'),
WHERE node_id IN ( (2, 1, 'lib'),
SELECT nc.id (3, 2, 'database'),
FROM node_closure AS nc (4, 2, 'utils'),
WHERE nc.root = 2 AND nc.depth = 2 (5, 2, 'storedQueries.spec.js'),
); (6, 3, '_sql.spec.js'),
(7, 3, '_statements.spec.js'),
(8, 3, 'database.spec.js'),
(9, 3, 'sqliteExtensions.spec.js'),
(10, 4, 'fileIo.spec.js'),
(11, 4, 'time.spec.js');
SELECT name
FROM node
WHERE node_id IN (
SELECT nc.id
FROM node_closure AS nc
WHERE nc.root = 2 AND nc.depth = 2
);
`) `)
expect(actual.values).to.eql({ expect(actual.values).to.eql({
name: [ name: [
@@ -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,121 @@ 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'
]
})
})
it('supports pearson', async function () {
const actual = await db.execute(`
CREATE TABLE dataset(x REAL, y REAL, z REAL);
INSERT INTO dataset VALUES
(5,3,3.2), (5,6,4.3), (5,9,5.4),
(10,3,4), (10,6,3.8), (10,9,3.6),
(15,3,4.8), (15,6,4), (15,9,3.5);
SELECT
pearson(x, x) xx,
pearson(x, y) xy,
abs(-0.12666 - pearson(x, z)) < 0.00001 xz,
pearson(y, x) yx,
pearson(y, y) yy,
abs(0.10555 - pearson(y, z)) < 0.00001 yz,
abs(-0.12666 - pearson(z, x)) < 0.00001 zx,
abs(0.10555 - pearson(z, y)) < 0.00001 zy,
pearson(z, z) zz
FROM dataset;
`)
expect(actual.values).to.eql({
xx: [1], xy: [0], xz: [1], yx: [0], yy: [1], yz: [1], zx: [1], zy: [1], zz: [1]
})
})
}) })

View File

@@ -87,14 +87,14 @@ describe('storedInquiries.js', () => {
it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => { it('isTabNeedName returns false when the inquiry has a name and is not predefined', () => {
const tab = { const tab = {
initName: 'foo' name: 'foo'
} }
expect(storedInquiries.isTabNeedName(tab)).to.equal(false) expect(storedInquiries.isTabNeedName(tab)).to.equal(false)
}) })
it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => { it('isTabNeedName returns true when the inquiry has no name and is not predefined', () => {
const tab = { const tab = {
initName: null, name: null,
tempName: 'Untitled' tempName: 'Untitled'
} }
expect(storedInquiries.isTabNeedName(tab)).to.equal(true) expect(storedInquiries.isTabNeedName(tab)).to.equal(true)
@@ -102,7 +102,7 @@ describe('storedInquiries.js', () => {
it('isTabNeedName returns true when the inquiry is predefined', () => { it('isTabNeedName returns true when the inquiry is predefined', () => {
const tab = { const tab = {
initName: 'foo', name: 'foo',
isPredefined: true isPredefined: true
} }
@@ -351,14 +351,13 @@ describe('storedInquiries.js', () => {
query: 'select * from foo', query: 'select * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: [],
initName: null, name: null,
$refs: { dataView: {
dataView: { getOptionsForSave () {
getOptionsForSave () { return ['chart']
return ['chart']
}
} }
} }
} }
const value = storedInquiries.save(tab, 'foo') const value = storedInquiries.save(tab, 'foo')
expect(value.id).to.equal(tab.id) expect(value.id).to.equal(tab.id)
@@ -376,19 +375,18 @@ describe('storedInquiries.js', () => {
query: 'select * from foo', query: 'select * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: [],
initName: null, name: null,
$refs: { dataView: {
dataView: { getOptionsForSave () {
getOptionsForSave () { return ['chart']
return ['chart']
}
} }
} }
} }
const first = storedInquiries.save(tab, 'foo') const first = storedInquiries.save(tab, 'foo')
tab.initName = 'foo' tab.name = 'foo'
tab.query = 'select * from foo' tab.query = 'select * from foo'
storedInquiries.save(tab) storedInquiries.save(tab)
const inquiries = storedInquiries.getStoredInquiries() const inquiries = storedInquiries.getStoredInquiries()
@@ -409,12 +407,10 @@ describe('storedInquiries.js', () => {
query: 'select * from foo', query: 'select * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: [], viewOptions: [],
initName: 'foo predefined', name: 'foo predefined',
$refs: { dataView: {
dataView: { getOptionsForSave () {
getOptionsForSave () { return ['chart']
return ['chart']
}
} }
}, },
isPredefined: true isPredefined: true

189
tests/lib/tab.spec.js Normal file
View File

@@ -0,0 +1,189 @@
import { expect } from 'chai'
import sinon from 'sinon'
import Tab from '@/lib/tab.js'
describe('tab.js', () => {
it('Creates a tab for new inquiry', () => {
const state = {
untitledLastIndex: 5
}
const newTab = new Tab(state)
expect(newTab).to.include({
name: null,
tempName: 'Untitled 5',
query: undefined,
viewOptions: undefined,
isPredefined: undefined,
viewType: 'chart',
result: null,
isGettingResults: false,
error: null,
time: 0,
isSaved: false,
state: state
})
expect(newTab.layout).to.include({
sqlEditor: 'above',
table: 'bottom',
dataView: 'hidden'
})
expect(newTab.id).to.have.lengthOf(21)
})
it('Creates a tab for existing inquiry', () => {
const state = {
untitledLastIndex: 5
}
const inquiry = {
id: 'qwerty',
query: 'SELECT * from foo',
viewType: 'pivot',
viewOptions: 'this is view options object',
name: 'Foo inquiry',
createdAt: '2022-12-05T18:30:30'
}
const newTab = new Tab(state, inquiry)
expect(newTab).to.include({
id: 'qwerty',
name: 'Foo inquiry',
tempName: 'Foo inquiry',
query: 'SELECT * from foo',
viewOptions: 'this is view options object',
isPredefined: undefined,
viewType: 'pivot',
result: null,
isGettingResults: false,
error: null,
time: 0,
isSaved: true,
state: state
})
expect(newTab.layout).to.include({
sqlEditor: 'above',
table: 'bottom',
dataView: 'hidden'
})
})
it('Set isGettingResults true when execute', async () => {
let resolveQuering
// mock store state
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
execute: sinon.stub().returns(new Promise(resolve => {
resolveQuering = resolve
})),
refreshSchema: sinon.stub().resolves()
}
}
const newTab = new Tab(state, {
id: 'qwerty',
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
viewType: 'cart',
viewOptions: 'this is view options object',
name: 'Foo inquiry',
createdAt: '2022-12-05T18:30:30'
})
expect(newTab.isGettingResults).to.equal(false)
newTab.execute()
expect(newTab.isGettingResults).to.equal(true)
resolveQuering()
})
it('Updates result with query execution result', async () => {
const result = {
columns: ['id', 'name'],
values: {
id: [1, 2],
name: ['Harry', 'Drako']
}
}
// mock store state
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
execute: sinon.stub().resolves(result),
refreshSchema: sinon.stub().resolves()
}
}
const newTab = new Tab(state, {
id: 'qwerty',
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
viewType: 'cart',
viewOptions: 'this is view options object',
name: 'Foo inquiry',
createdAt: '2022-12-05T18:30:30'
})
await newTab.execute()
expect(newTab.isGettingResults).to.equal(false)
expect(newTab.result).to.eql(result)
})
it('Updates error with query execution error', async () => {
// mock store state
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
execute: sinon.stub().rejects(new Error('No such table')),
refreshSchema: sinon.stub().resolves()
}
}
const newTab = new Tab(state, {
id: 'qwerty',
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
viewType: 'cart',
viewOptions: 'this is view options object',
name: 'Foo inquiry',
createdAt: '2022-12-05T18:30:30'
})
await newTab.execute()
expect(newTab.error.type).to.eql('error')
expect(newTab.error.message.toString()).to.equal('Error: No such table')
})
it('Updates schema after query execution', async () => {
const result = {
columns: ['id', 'name'],
values: {
id: [],
name: []
}
}
// mock store state
const state = {
currentTabId: 1,
dbName: 'fooDb',
db: {
execute: sinon.stub().resolves(result),
refreshSchema: sinon.stub().resolves()
}
}
const newTab = new Tab(state, {
id: 'qwerty',
query: 'SELECT * FROM foo; CREATE TABLE bar(a,b);',
viewType: 'cart',
viewOptions: 'this is view options object',
name: 'Foo inquiry',
createdAt: '2022-12-05T18:30:30'
})
await newTab.execute()
expect(state.db.refreshSchema.calledOnce).to.equal(true)
})
})

View File

@@ -0,0 +1,30 @@
import { expect } from 'chai'
import cIo from '@/lib/utils/clipboardIo'
import sinon from 'sinon'
describe('clipboardIo.js', async () => {
afterEach(() => {
sinon.restore()
})
it('copyCsv', async () => {
sinon.stub(navigator.clipboard, 'writeText').resolves(true)
await cIo.copyCsv('id\tname\r\n1\t2')
expect(navigator.clipboard.writeText.calledOnceWith('id\tname\r\n1\t2'))
})
it('copyImage for canvas calls _copyCanvas', async () => {
sinon.stub(cIo, '_copyCanvas').resolves(true)
const canvas = document.createElement('canvas')
await cIo.copyImage(canvas)
expect(cIo._copyCanvas.calledOnceWith(canvas))
})
it('copyImage for dataUrl calls _copyFromDataUrl', async () => {
sinon.stub(cIo, '_copyFromDataUrl').resolves(true)
const url = document.createElement('canvas').toDataURL()
await cIo.copyImage(url)
expect(cIo._copyFromDataUrl.calledOnceWith(url))
})
})

View File

@@ -27,4 +27,14 @@ describe('time.js', () => {
expect(time.getPeriod(start, end)).to.equal('0.045s') expect(time.getPeriod(start, end)).to.equal('0.045s')
}) })
it('sleep resolves after n ms', async () => {
let before = Date.now()
await time.sleep(10)
expect(Date.now() - before).to.be.least(10)
before = Date.now()
await time.sleep(30)
expect(Date.now() - before).to.be.least(30)
})
}) })

View File

@@ -11,7 +11,7 @@ describe('actions', () => {
} }
let id = await addTab({ state }) let id = await addTab({ state })
expect(state.tabs[0]).to.eql({ expect(state.tabs[0]).to.include({
id: id, id: id,
name: null, name: null,
tempName: 'Untitled', tempName: 'Untitled',
@@ -22,7 +22,7 @@ describe('actions', () => {
expect(state.untitledLastIndex).to.equal(1) expect(state.untitledLastIndex).to.equal(1)
id = await addTab({ state }) id = await addTab({ state })
expect(state.tabs[1]).to.eql({ expect(state.tabs[1]).to.include({
id: id, id: id,
name: null, name: null,
tempName: 'Untitled 1', tempName: 'Untitled 1',
@@ -41,14 +41,13 @@ describe('actions', () => {
const tab = { const tab = {
id: 1, id: 1,
name: 'test', name: 'test',
tempName: null,
query: 'SELECT * from foo', query: 'SELECT * from foo',
viewType: 'chart', viewType: 'chart',
viewOptions: {}, viewOptions: 'an object with view options',
isSaved: true isSaved: true
} }
await addTab({ state }, tab) await addTab({ state }, tab)
expect(state.tabs[0]).to.eql(tab) expect(state.tabs[0]).to.include(tab)
expect(state.untitledLastIndex).to.equal(0) expect(state.untitledLastIndex).to.equal(0)
}) })

View File

@@ -5,9 +5,10 @@ const {
updateTab, updateTab,
deleteTab, deleteTab,
setCurrentTabId, setCurrentTabId,
setCurrentTab,
updatePredefinedInquiries, updatePredefinedInquiries,
setDb setDb,
setLoadingPredefinedInquiries,
setPredefinedInquiriesLoaded
} = mutations } = mutations
describe('mutations', () => { describe('mutations', () => {
@@ -35,8 +36,7 @@ describe('mutations', () => {
isPredefined: false isPredefined: false
} }
const newTab = { const newValues = {
index: 0,
id: 1, id: 1,
name: 'new test', name: 'new test',
query: 'SELECT * from bar', query: 'SELECT * from bar',
@@ -49,7 +49,7 @@ describe('mutations', () => {
tabs: [tab] tabs: [tab]
} }
updateTab(state, newTab) updateTab(state, { tab, newValues })
expect(state.tabs[0]).to.eql({ expect(state.tabs[0]).to.eql({
id: 1, id: 1,
name: 'new test', name: 'new test',
@@ -73,8 +73,7 @@ describe('mutations', () => {
isPredefined: true isPredefined: true
} }
const newTab = { const newValues = {
index: 0,
id: 2, id: 2,
name: 'new test', name: 'new test',
query: 'SELECT * from bar', query: 'SELECT * from bar',
@@ -88,7 +87,7 @@ describe('mutations', () => {
currentTabId: 1 currentTabId: 1
} }
updateTab(state, newTab) updateTab(state, { tab, newValues })
expect(state.tabs).to.have.lengthOf(1) expect(state.tabs).to.have.lengthOf(1)
expect(state.currentTabId).to.equal(2) expect(state.currentTabId).to.equal(2)
expect(state.tabs[0].id).to.equal(2) expect(state.tabs[0].id).to.equal(2)
@@ -109,8 +108,7 @@ describe('mutations', () => {
isSaved: false isSaved: false
} }
const newTab = { const newValues = {
index: 0,
id: 1, id: 1,
name: 'new test' name: 'new test'
} }
@@ -119,7 +117,7 @@ describe('mutations', () => {
tabs: [tab] tabs: [tab]
} }
updateTab(state, newTab) updateTab(state, { tab, newValues })
expect(state.tabs).to.have.lengthOf(1) expect(state.tabs).to.have.lengthOf(1)
expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].id).to.equal(1)
expect(state.tabs[0].name).to.equal('new test') expect(state.tabs[0].name).to.equal('new test')
@@ -139,8 +137,7 @@ describe('mutations', () => {
isPredefined: true isPredefined: true
} }
const newTab = { const newValues = {
index: 0,
isSaved: false isSaved: false
} }
@@ -148,7 +145,7 @@ describe('mutations', () => {
tabs: [tab] tabs: [tab]
} }
updateTab(state, newTab) updateTab(state, { tab, newValues })
expect(state.tabs).to.have.lengthOf(1) expect(state.tabs).to.have.lengthOf(1)
expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].id).to.equal(1)
expect(state.tabs[0].name).to.equal('test') expect(state.tabs[0].name).to.equal('test')
@@ -179,13 +176,15 @@ describe('mutations', () => {
const state = { const state = {
tabs: [tab1, tab2], tabs: [tab1, tab2],
currentTabId: 1 currentTabId: 1,
currentTab: tab1
} }
deleteTab(state, 0) deleteTab(state, tab1)
expect(state.tabs).to.have.lengthOf(1) expect(state.tabs).to.have.lengthOf(1)
expect(state.tabs[0].id).to.equal(2) expect(state.tabs[0].id).to.equal(2)
expect(state.currentTabId).to.equal(2) expect(state.currentTabId).to.equal(2)
expect(state.currentTab).to.eql(tab2)
}) })
it('deleteTab - opened, last', () => { it('deleteTab - opened, last', () => {
@@ -211,13 +210,15 @@ describe('mutations', () => {
const state = { const state = {
tabs: [tab1, tab2], tabs: [tab1, tab2],
currentTabId: 2 currentTabId: 2,
currentTab: tab2
} }
deleteTab(state, 1) deleteTab(state, tab2)
expect(state.tabs).to.have.lengthOf(1) expect(state.tabs).to.have.lengthOf(1)
expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].id).to.equal(1)
expect(state.currentTabId).to.equal(1) expect(state.currentTabId).to.equal(1)
expect(state.currentTab).to.eql(tab1)
}) })
it('deleteTab - opened, in the middle', () => { it('deleteTab - opened, in the middle', () => {
@@ -253,14 +254,16 @@ describe('mutations', () => {
const state = { const state = {
tabs: [tab1, tab2, tab3], tabs: [tab1, tab2, tab3],
currentTabId: 2 currentTabId: 2,
currentTab: tab2
} }
deleteTab(state, 1) deleteTab(state, tab2)
expect(state.tabs).to.have.lengthOf(2) expect(state.tabs).to.have.lengthOf(2)
expect(state.tabs[0].id).to.equal(1) expect(state.tabs[0].id).to.equal(1)
expect(state.tabs[1].id).to.equal(3) expect(state.tabs[1].id).to.equal(3)
expect(state.currentTabId).to.equal(3) expect(state.currentTabId).to.equal(3)
expect(state.currentTab).to.eql(tab3)
}) })
it('deleteTab - opened, single', () => { it('deleteTab - opened, single', () => {
@@ -276,48 +279,19 @@ describe('mutations', () => {
const state = { const state = {
tabs: [tab1], tabs: [tab1],
currentTabId: 1 currentTabId: 1,
currentTab: tab1
} }
deleteTab(state, 0) deleteTab(state, tab1)
expect(state.tabs).to.have.lengthOf(0) expect(state.tabs).to.have.lengthOf(0)
expect(state.currentTabId).to.equal(null) expect(state.currentTabId).to.equal(null)
}) expect(state.currentTab).to.equal(null)
it('deleteTab - not opened', () => {
const tab1 = {
id: 1,
name: 'foo',
tempName: null,
query: 'SELECT * from foo',
viewType: 'chart',
viewOptions: {},
isSaved: true
}
const tab2 = {
id: 2,
name: 'bar',
tempName: null,
query: 'SELECT * from bar',
viewType: 'chart',
viewOptions: {},
isSaved: true
}
const state = {
tabs: [tab1, tab2],
currentTabId: 1
}
deleteTab(state, 1)
expect(state.tabs).to.have.lengthOf(1)
expect(state.tabs[0].id).to.equal(1)
expect(state.currentTabId).to.equal(1)
}) })
it('setCurrentTabId', () => { it('setCurrentTabId', () => {
const state = { const state = {
tabs: [{ id: 1 }, { id: 2 }],
currentTabId: 1 currentTabId: 1
} }
@@ -325,15 +299,6 @@ describe('mutations', () => {
expect(state.currentTabId).to.equal(2) expect(state.currentTabId).to.equal(2)
}) })
it('setCurrentTab', () => {
const state = {
currentTab: { id: 1 }
}
setCurrentTab(state, { id: 2 })
expect(state.currentTab).to.eql({ id: 2 })
})
it('updatePredefinedInquiries - single', () => { it('updatePredefinedInquiries - single', () => {
const inquiry = { const inquiry = {
id: 1, id: 1,
@@ -377,4 +342,22 @@ describe('mutations', () => {
updatePredefinedInquiries(state, inquiries) updatePredefinedInquiries(state, inquiries)
expect(state.predefinedInquiries).to.eql(inquiries) expect(state.predefinedInquiries).to.eql(inquiries)
}) })
it('setLoadingPredefinedInquiries', () => {
const state = {
loadingPredefinedInquiries: false
}
setLoadingPredefinedInquiries(state, true)
expect(state.loadingPredefinedInquiries).to.equal(true)
})
it('setPredefinedInquiriesLoaded', () => {
const state = {
predefinedInquiriesLoaded: false
}
setPredefinedInquiriesLoaded(state, true)
expect(state.predefinedInquiriesLoaded).to.equal(true)
})
}) })

View File

@@ -0,0 +1,147 @@
import { expect } from 'chai'
import sinon from 'sinon'
import { mount } from '@vue/test-utils'
import Vuex from 'vuex'
import LoadView from '@/views/LoadView'
import fu from '@/lib/utils/fileIo'
import database from '@/lib/database'
import realMutations from '@/store/mutations'
import realActions from '@/store/actions'
import flushPromises from 'flush-promises'
import Tab from '@/lib/tab'
describe('LoadView.vue', () => {
afterEach(() => {
sinon.restore()
})
it('Loads db and inquiries and redirects to workspace if no errors', async () => {
const state = {
tabs: []
}
const mutations = {
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
setDb: sinon.stub().callsFake(realMutations.setDb)
}
const actions = {
addTab: sinon.stub().callsFake(realActions.addTab)
}
const store = new Vuex.Store({ state, mutations, actions })
const $route = {
path: '/workspace',
query: {
data_url: 'https://my-url/test.db',
data_format: 'sqlite',
inquiry_url: 'https://my-url/test_inquiries.json',
inquiry_id: [1],
maximize: 'dataView'
}
}
const $router = { push: sinon.stub() }
const readFile = sinon.stub(fu, 'readFile')
const dataRes = new Response()
dataRes.blob = sinon.stub().resolves({})
readFile.onCall(0).returns(Promise.resolve(dataRes))
const inquiriesRes = new Response()
inquiriesRes.json = sinon.stub().resolves({
version: 2,
inquiries: [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]
})
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
const db = {
loadDb: sinon.stub().resolves()
}
sinon.stub(database, 'getNewDatabase').returns(db)
Tab.prototype.execute = sinon.stub()
const wrapper = mount(LoadView, {
store,
mocks: { $route, $router },
stubs: ['router-link']
})
await flushPromises()
// DB file is read
expect(fu.readFile.firstCall.args[0]).to.equal('https://my-url/test.db')
// Db is loaded
expect(db.loadDb.firstCall.args[0]).to.equal(await dataRes.blob.returnValues[0])
// Inquiries file is read
expect(fu.readFile.secondCall.args[0])
.to.equal('https://my-url/test_inquiries.json')
// Tab for inquiry is created
expect(actions.addTab.calledOnce).to.equal(true)
expect(actions.addTab.firstCall.args[1]).eql({
id: undefined,
name: 'foo',
layout: {
dataView: 'bottom',
sqlEditor: 'hidden',
table: 'above'
},
maximize: 'dataView'
})
const executedTab = Tab.prototype.execute.firstCall.thisValue
expect(executedTab.tempName).to.equal('foo')
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(false)
expect($router.push.called).to.equal(true)
})
it('Doesn\'t redirect and show the button if there is an error', async () => {
const state = {
tabs: []
}
const mutations = {
setCurrentTabId: sinon.stub().callsFake(realMutations.setCurrentTabId),
setDb: sinon.stub().callsFake(realMutations.setDb)
}
const actions = {
addTab: sinon.stub().callsFake(realActions.addTab)
}
const store = new Vuex.Store({ state, mutations, actions })
const $route = {
path: '/workspace',
query: {
data_url: 'https://my-url/test.db',
data_format: 'sqlite',
inquiry_url: 'https://my-url/test_inquiries.json',
inquiry_id: [1],
maximize: 'dataView'
}
}
const $router = { push: sinon.stub() }
const readFile = sinon.stub(fu, 'readFile')
const dataRes = new Response()
dataRes.blob = sinon.stub().rejects(new Error('Something is wrong'))
readFile.onCall(0).returns(Promise.resolve(dataRes))
const inquiriesRes = new Response()
inquiriesRes.json = sinon.stub().resolves({
version: 2,
inquiries: [{ id: 1 }]
})
readFile.onCall(1).returns(Promise.resolve(inquiriesRes))
sinon.stub(database, 'getNewDatabase').returns({
loadDb: sinon.stub().resolves()
})
const wrapper = mount(LoadView, {
store,
mocks: { $route, $router },
stubs: ['router-link']
})
await flushPromises()
expect(wrapper.find('#open-workspace-btn').exists()).to.equal(true)
expect($router.push.called).to.equal(false)
expect(wrapper.find('#logs').text()).to.include('Something is wrong')
})
})

View File

@@ -19,7 +19,9 @@ describe('Inquiries.vue', () => {
predefinedInquiries: [] predefinedInquiries: []
} }
const mutations = { const mutations = {
updatePredefinedInquiries: sinon.stub() setPredefinedInquiriesLoaded: sinon.stub(),
updatePredefinedInquiries: sinon.stub(),
setLoadingPredefinedInquiries: sinon.stub()
} }
const store = new Vuex.Store({ state, mutations }) const store = new Vuex.Store({ state, mutations })
const wrapper = shallowMount(Inquiries, { store }) const wrapper = shallowMount(Inquiries, { store })
@@ -27,7 +29,7 @@ describe('Inquiries.vue', () => {
expect(wrapper.find('#start-guide').exists()).to.equal(true) expect(wrapper.find('#start-guide').exists()).to.equal(true)
}) })
it('Renders the list on saved or predefined inquiries', async () => { it('Renders the list of saved and predefined inquiries', async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([ sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([
{ {
id: 0, id: 0,
@@ -125,6 +127,49 @@ describe('Inquiries.vue', () => {
expect(rows.at(0).findAll('td').at(1).text()).to.contains('3 November 2020 20:57') expect(rows.at(0).findAll('td').at(1).text()).to.contains('3 November 2020 20:57')
}) })
it('Shows No found message when filter returns nothing', async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([
{
id: 0,
name: 'hello_world',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-03-08T19:57:56.299Z'
}
])
sinon.stub(storedInquiries, 'getStoredInquiries').returns([
{
id: 1,
name: 'foo',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-11-03T19:57:56.299Z'
},
{
id: 2,
name: 'bar',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-12-04T18:53:56.299Z'
}
])
const state = {
predefinedInquiries: []
}
const store = new Vuex.Store({ state, mutations })
const wrapper = mount(Inquiries, { store })
await wrapper.find('#toolbar-search input').setValue('baz')
await wrapper.vm.$nextTick()
expect(wrapper.find('#inquiries-not-found').text()).to.equal('No inquiries found')
expect(wrapper.find('#start-guide').exists()).to.equal(false)
expect(wrapper.find('tbody').isVisible()).to.equal(false)
})
it('Predefined inquiry has a badge', async () => { it('Predefined inquiry has a badge', async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([ sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([
{ {
@@ -231,7 +276,7 @@ describe('Inquiries.vue', () => {
).to.equals(true) ).to.equals(true)
}) })
it('Makes the copy of the inquiry selected if all inquiries were selected before duplication', it('The copy of the inquiry is not selected if all inquiries were selected before duplication',
async () => { async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([]) sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
const inquiryInStorage = { const inquiryInStorage = {
@@ -262,12 +307,13 @@ describe('Inquiries.vue', () => {
await storedInquiries.readPredefinedInquiries.returnValues[0] await storedInquiries.readPredefinedInquiries.returnValues[0]
await storedInquiries.getStoredInquiries.returnValues[0] await storedInquiries.getStoredInquiries.returnValues[0]
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container').trigger('click') await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container')
.trigger('click')
await wrapper.findComponent({ name: 'CopyIcon' }).find('svg').trigger('click') await wrapper.findComponent({ name: 'CopyIcon' }).find('svg').trigger('click')
const checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' }) const checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' })
expect(checkboxes.at(0).vm.checked).to.equals(true) expect(checkboxes.at(0).vm.checked).to.equals(true)
expect(checkboxes.at(1).vm.checked).to.equals(true) expect(checkboxes.at(1).vm.checked).to.equals(false)
}) })
it('Opens an inquiry', async () => { it('Opens an inquiry', async () => {
@@ -283,6 +329,7 @@ describe('Inquiries.vue', () => {
sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage]) sinon.stub(storedInquiries, 'getStoredInquiries').returns([inquiryInStorage])
const state = { const state = {
tabs: [],
predefinedInquiries: [] predefinedInquiries: []
} }
const actions = { addTab: sinon.stub().resolves(1) } const actions = { addTab: sinon.stub().resolves(1) }
@@ -478,7 +525,7 @@ describe('Inquiries.vue', () => {
)).to.equals(true) )).to.equals(true)
}) })
it('Imported inquiries are selected if master check box is checked', async () => { it('Imported inquiries are not selected if master check box was checked', async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([]) sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([])
const inquiryInStorage = { const inquiryInStorage = {
id: 1, id: 1,
@@ -510,14 +557,16 @@ describe('Inquiries.vue', () => {
await wrapper.vm.$nextTick() await wrapper.vm.$nextTick()
// click on master checkbox // click on master checkbox
await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container').trigger('click') await wrapper.findComponent({ ref: 'mainCheckBox' }).find('.checkbox-container')
.trigger('click')
// click Import // click Import
await wrapper.find('#toolbar-btns-import').trigger('click') await wrapper.find('#toolbar-btns-import').trigger('click')
const checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' }) const checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' })
expect(wrapper.findComponent({ ref: 'mainCheckBox' }).vm.checked).to.equals(false)
expect(checkboxes.at(0).vm.checked).to.equals(true) expect(checkboxes.at(0).vm.checked).to.equals(true)
expect(checkboxes.at(1).vm.checked).to.equals(true) expect(checkboxes.at(1).vm.checked).to.equals(false)
}) })
it('Deletion is not available for predefined inquiries', async () => { it('Deletion is not available for predefined inquiries', async () => {
@@ -1045,4 +1094,93 @@ describe('Inquiries.vue', () => {
expect(checkboxes.at(0).vm.checked).to.equals(false) expect(checkboxes.at(0).vm.checked).to.equals(false)
expect(checkboxes.at(0).vm.checked).to.equals(false) expect(checkboxes.at(0).vm.checked).to.equals(false)
}) })
it('Selection and filter', async () => {
sinon.stub(storedInquiries, 'readPredefinedInquiries').resolves([
{
id: 0,
name: 'hello_world',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-03-08T19:57:56.299Z'
}
])
const foo = {
id: 1,
name: 'foo',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-03-08T19:57:56.299Z'
}
const bar = {
id: 2,
name: 'bar',
query: '',
viewType: 'chart',
viewOptions: [],
createdAt: '2020-03-08T19:57:56.299Z'
}
sinon.stub(storedInquiries, 'getStoredInquiries').returns([foo, bar])
const state = {
predefinedInquiries: []
}
const store = new Vuex.Store({ state, mutations })
const wrapper = mount(Inquiries, { store })
await storedInquiries.readPredefinedInquiries.returnValues[0]
await storedInquiries.getStoredInquiries.returnValues[0]
await wrapper.vm.$nextTick()
const mainCheckBox = wrapper.findComponent({ ref: 'mainCheckBox' })
// Select all with main checkbox
await mainCheckBox.find('.checkbox-container').trigger('click')
expect([...wrapper.vm.selectedInquiriesIds]).to.eql([0, 1, 2])
expect(wrapper.vm.selectedNotPredefinedCount).to.eql(2)
let checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' })
expect(checkboxes.at(0).vm.checked).to.equals(true)
expect(checkboxes.at(1).vm.checked).to.equals(true)
expect(checkboxes.at(2).vm.checked).to.equals(true)
// Filter
await wrapper.find('#toolbar-search input').setValue('foo')
await wrapper.vm.$nextTick()
expect([...wrapper.vm.selectedInquiriesIds]).to.eql([1])
expect(wrapper.vm.selectedNotPredefinedCount).to.eql(1)
checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' })
expect(checkboxes.at(0).vm.checked).to.equals(true)
// Clear filter
await wrapper.find('#toolbar-search input').setValue('')
await wrapper.vm.$nextTick()
expect([...wrapper.vm.selectedInquiriesIds]).to.eql([1])
expect(wrapper.vm.selectedNotPredefinedCount).to.eql(1)
checkboxes = wrapper.findAll('tr .checkbox-container')
expect(checkboxes.at(0).classes()).to.not.include('checked')
expect(checkboxes.at(1).classes()).to.include('checked')
expect(checkboxes.at(2).classes()).to.not.include('checked')
// Select also first inquiry
wrapper.find('tbody tr .checkbox-container').trigger('click')
expect([...wrapper.vm.selectedInquiriesIds]).to.eql([1, 0])
expect(wrapper.vm.selectedNotPredefinedCount).to.eql(1)
// Filter
await wrapper.find('#toolbar-search input').setValue('hello')
await wrapper.vm.$nextTick()
expect([...wrapper.vm.selectedInquiriesIds]).to.eql([0])
expect(wrapper.vm.selectedNotPredefinedCount).to.eql(0)
checkboxes = wrapper.findAllComponents({ ref: 'rowCheckBox' })
expect(checkboxes.at(0).vm.checked).to.equals(true)
// Select all with main checkbox
await mainCheckBox.find('.checkbox-container').trigger('click')
// Clear filter - main checkbox bocomes not checked
await wrapper.find('#toolbar-search input').setValue('')
await wrapper.vm.$nextTick()
expect(mainCheckBox.vm.checked).to.equals(false)
})
}) })

View File

@@ -45,7 +45,7 @@ describe('MainMenu.vue', () => {
it('Save is not visible if there is no tabs', () => { it('Save is not visible if there is no tabs', () => {
const state = { const state = {
currentTab: null, currentTab: null,
tabs: [{}], tabs: [],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -62,13 +62,15 @@ describe('MainMenu.vue', () => {
}) })
it('Save is disabled if current tab.isSaved is true', async () => { it('Save is disabled if current tab.isSaved is true', async () => {
const tab = {
id: 1,
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -83,17 +85,19 @@ describe('MainMenu.vue', () => {
expect(wrapper.find('#save-btn').element.disabled).to.equal(false) expect(wrapper.find('#save-btn').element.disabled).to.equal(false)
await vm.$set(state.tabs[0], 'isSaved', true) await vm.$set(state.tabs[0], 'isSaved', true)
await vm.$nextTick()
expect(wrapper.find('#save-btn').element.disabled).to.equal(true) expect(wrapper.find('#save-btn').element.disabled).to.equal(true)
}) })
it('Creates a tab', async () => { it('Creates a tab', async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const newInquiryId = 1 const newInquiryId = 1
@@ -121,13 +125,14 @@ describe('MainMenu.vue', () => {
}) })
it('Creates a tab and redirects to workspace', async () => { it('Creates a tab and redirects to workspace', async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const newInquiryId = 1 const newInquiryId = 1
@@ -156,13 +161,14 @@ describe('MainMenu.vue', () => {
it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"', it('Ctrl R calls currentTab.execute if running is enabled and route.path is "/workspace"',
async () => { async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -201,13 +207,14 @@ describe('MainMenu.vue', () => {
it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"', it('Ctrl Enter calls currentTab.execute if running is enabled and route.path is "/workspace"',
async () => { async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -245,13 +252,14 @@ describe('MainMenu.vue', () => {
}) })
it('Ctrl B calls createNewInquiry', async () => { it('Ctrl B calls createNewInquiry', async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -280,13 +288,14 @@ describe('MainMenu.vue', () => {
it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace', it('Ctrl S calls checkInquiryBeforeSave if the tab is unsaved and route path is /workspace',
async () => { async () => {
const tab = {
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ isSaved: false }],
db: {} db: {}
} }
const store = new Vuex.Store({ state }) const store = new Vuex.Store({ state })
@@ -325,13 +334,16 @@ describe('MainMenu.vue', () => {
it('Saves the inquiry when no need the new name', it('Saves the inquiry when no need the new name',
async () => { async () => {
const tab = {
id: 1,
name: 'foo',
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ id: 1, name: 'foo', isSaved: false }],
db: {} db: {}
} }
const mutations = { const mutations = {
@@ -364,13 +376,15 @@ describe('MainMenu.vue', () => {
// check that the tab was updated // check that the tab was updated
expect(mutations.updateTab.calledOnceWith(state, sinon.match({ expect(mutations.updateTab.calledOnceWith(state, sinon.match({
index: 0, tab,
name: 'foo', newValues: {
id: 1, name: 'foo',
query: 'SELECT * FROM foo', id: 1,
viewType: 'chart', query: 'SELECT * FROM foo',
viewOptions: [], viewType: 'chart',
isSaved: true viewOptions: [],
isSaved: true
}
}))).to.equal(true) }))).to.equal(true)
// check that 'inquirySaved' event was triggered on $root // check that 'inquirySaved' event was triggered on $root
@@ -378,13 +392,17 @@ describe('MainMenu.vue', () => {
}) })
it('Shows en error when the new name is needed but not specifyied', async () => { it('Shows en error when the new name is needed but not specifyied', async () => {
const tab = {
id: 1,
name: null,
tempName: 'Untitled',
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
db: {} db: {}
} }
const mutations = { const mutations = {
@@ -424,13 +442,17 @@ describe('MainMenu.vue', () => {
}) })
it('Saves the inquiry with a new name', async () => { it('Saves the inquiry with a new name', async () => {
const tab = {
id: 1,
name: null,
tempName: 'Untitled',
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
db: {} db: {}
} }
const mutations = { const mutations = {
@@ -475,13 +497,15 @@ describe('MainMenu.vue', () => {
// check that the tab was updated // check that the tab was updated
expect(mutations.updateTab.calledOnceWith(state, sinon.match({ expect(mutations.updateTab.calledOnceWith(state, sinon.match({
index: 0, tab: tab,
name: 'foo', newValues: {
id: 1, name: 'foo',
query: 'SELECT * FROM foo', id: 1,
viewType: 'chart', query: 'SELECT * FROM foo',
viewOptions: [], viewType: 'chart',
isSaved: true viewOptions: [],
isSaved: true
}
}))).to.equal(true) }))).to.equal(true)
// check that 'inquirySaved' event was triggered on $root // check that 'inquirySaved' event was triggered on $root
@@ -489,23 +513,26 @@ describe('MainMenu.vue', () => {
}) })
it('Saves a predefined inquiry with a new name', async () => { it('Saves a predefined inquiry with a new name', async () => {
const state = { const tab = {
currentTab: { id: 1,
query: 'SELECT * FROM foo', name: 'foo',
execute: sinon.stub(), query: 'SELECT * FROM foo',
tabIndex: 0, execute: sinon.stub(),
isPredefined: true, isPredefined: true,
result: { result: {
columns: ['id', 'name'], columns: ['id', 'name'],
values: [ values: [
[1, 'Harry Potter'], [1, 'Harry Potter'],
[2, 'Drako Malfoy'] [2, 'Drako Malfoy']
] ]
},
viewType: 'chart',
viewOptions: []
}, },
tabs: [{ id: 1, name: 'foo', isSaved: false, isPredefined: true }], viewType: 'chart',
viewOptions: [],
isSaved: false
}
const state = {
currentTab: tab,
tabs: [tab],
db: {} db: {}
} }
const mutations = { const mutations = {
@@ -553,13 +580,15 @@ describe('MainMenu.vue', () => {
// check that the tab was updated // check that the tab was updated
expect(mutations.updateTab.calledOnceWith(state, sinon.match({ expect(mutations.updateTab.calledOnceWith(state, sinon.match({
index: 0, tab,
name: 'bar', newValues: {
id: 2, name: 'bar',
query: 'SELECT * FROM foo', id: 2,
viewType: 'chart', query: 'SELECT * FROM foo',
viewOptions: [], viewType: 'chart',
isSaved: true viewOptions: [],
isSaved: true
}
}))).to.equal(true) }))).to.equal(true)
// check that 'inquirySaved' event was triggered on $root // check that 'inquirySaved' event was triggered on $root
@@ -580,13 +609,17 @@ describe('MainMenu.vue', () => {
}) })
it('Cancel saving', async () => { it('Cancel saving', async () => {
const tab = {
id: 1,
name: null,
tempName: 'Untitled',
query: 'SELECT * FROM foo',
execute: sinon.stub(),
isSaved: false
}
const state = { const state = {
currentTab: { currentTab: tab,
query: 'SELECT * FROM foo', tabs: [tab],
execute: sinon.stub(),
tabIndex: 0
},
tabs: [{ id: 1, name: null, tempName: 'Untitled', isSaved: false }],
db: {} db: {}
} }
const mutations = { const mutations = {

View File

@@ -1,23 +0,0 @@
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import actions from '@/store/actions'
import Vuex from 'vuex'
import Workspace from '@/views/Main/Workspace'
describe('Workspace.vue', () => {
it('Creates a tab with example if schema is empty', () => {
const state = {
db: {},
tabs: []
}
const store = new Vuex.Store({ state, actions })
mount(Workspace, { store })
expect(state.tabs[0].query).to.include('Your database is empty.')
expect(state.tabs[0].tempName).to.equal('Untitled')
expect(state.tabs[0].name).to.equal(null)
expect(state.tabs[0].viewType).to.equal('chart')
expect(state.tabs[0].viewOptions).to.equal(undefined)
expect(state.tabs[0].isSaved).to.equal(false)
})
})

Some files were not shown because too many files have changed in this diff Show More