mirror of
https://github.com/lana-k/sqliteviz.git
synced 2025-12-07 02:28:54 +08:00
Compare commits
138 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a05b27400 | ||
|
|
108d96a753 | ||
|
|
f55a8caa92 | ||
|
|
87f9f9eb01 | ||
|
|
d6408bdd85 | ||
|
|
378b9fb580 | ||
|
|
244ba9eb08 | ||
|
|
53e5194295 | ||
|
|
04274ef19a | ||
|
|
3893a66f4e | ||
|
|
1b6b7c71e9 | ||
|
|
3f6427ff0e | ||
|
|
a2464d839f | ||
|
|
316e603c3c | ||
|
|
88466eca5e | ||
|
|
5123e39a60 | ||
|
|
4c8401f32f | ||
|
|
d949629ee4 | ||
|
|
7a18e415c8 | ||
|
|
878689b3f7 | ||
|
|
42f040975d | ||
|
|
78e9ca2120 | ||
|
|
96af391f20 | ||
|
|
f58b62eb0c | ||
|
|
b17040d3ef | ||
|
|
bc6154b9ad | ||
|
|
3aea8c951b | ||
|
|
1e982a1196 | ||
|
|
6ecbde7fd3 | ||
|
|
5ee881432a | ||
|
|
735e4ec7f6 | ||
|
|
07d31dbfe9 | ||
|
|
ac1f7de62c | ||
|
|
96877de532 | ||
|
|
b60fc28e47 | ||
|
|
bec3d9c737 | ||
|
|
8aac7af481 | ||
|
|
6982204e68 | ||
|
|
41e0ae7332 | ||
|
|
ebb5af4f10 | ||
|
|
ae26358b25 | ||
|
|
d9ee702b8e | ||
|
|
446045fa55 | ||
|
|
1a9d1b308b | ||
|
|
014ecf145e | ||
|
|
0044d82b6f | ||
|
|
998e8d66f7 | ||
|
|
db3dbdf993 | ||
|
|
4e13a16e33 | ||
|
|
9c0103fd05 | ||
|
|
e4b117ffb9 | ||
|
|
6320f818cb | ||
|
|
3c456ef135 | ||
|
|
c713c713b7 | ||
|
|
babf0074c0 | ||
|
|
e71e6700c1 | ||
|
|
84e66b8167 | ||
|
|
9e84cf269e | ||
|
|
e897b4913b | ||
|
|
0646f58ca0 | ||
|
|
c674bf11e3 | ||
|
|
2d8a91675e | ||
|
|
45b1021559 | ||
|
|
7216e996d1 | ||
|
|
6eae9a0f2d | ||
|
|
7486b32bd1 | ||
|
|
2c564767f8 | ||
|
|
289a727cbe | ||
|
|
5f2b8ba5a9 | ||
|
|
f0a4212e2b | ||
|
|
c8deff32c1 | ||
|
|
d56604a7d6 | ||
|
|
48e311bff8 | ||
|
|
518b22b489 | ||
|
|
a20dd7f849 | ||
|
|
310a939109 | ||
|
|
bb9ba08902 | ||
|
|
c7c727ff78 | ||
|
|
8669a6a9e5 | ||
|
|
c1cc5bb95e | ||
|
|
9c55e76a41 | ||
|
|
70a9edf57e | ||
|
|
b2c2344951 | ||
|
|
cbec91e78a | ||
|
|
816b0e6218 | ||
|
|
4ed93bbea7 | ||
|
|
3e3a70430f | ||
|
|
bce3854792 | ||
|
|
307cac06e6 | ||
|
|
c4b0bdc870 | ||
|
|
69e0b2129b | ||
|
|
4ebb3715d6 | ||
|
|
858e32c9fd | ||
|
|
42cce95ed0 | ||
|
|
61ffcc82d6 | ||
|
|
4716bcf258 | ||
|
|
23d2421584 | ||
|
|
4730afc68a | ||
|
|
6b902d5f00 | ||
|
|
a484c1e022 | ||
|
|
1ed5bf2fae | ||
|
|
da8dc71f23 | ||
|
|
ebac3d8f6c | ||
|
|
0336168bdc | ||
|
|
e38f482933 | ||
|
|
bcd98fe1ad | ||
|
|
ec3ec6f57a | ||
|
|
ae9e2bc03c | ||
|
|
fe11d446f3 | ||
|
|
e6bef139f1 | ||
|
|
d7e0e52da5 | ||
|
|
09cec13a9c | ||
|
|
0cae368350 | ||
|
|
88c62a1dfb | ||
|
|
ead861b610 | ||
|
|
949e6b626e | ||
|
|
6dc81f7be6 | ||
|
|
bb39dcbc2f | ||
|
|
131efb55df | ||
|
|
b33974c26c | ||
|
|
e9c97beb5a | ||
|
|
cabc4f8acd | ||
|
|
679a785d70 | ||
|
|
a91add40cc | ||
|
|
53b2d8372f | ||
|
|
f3e8448851 | ||
|
|
4213e9df5c | ||
|
|
9f32323a80 | ||
|
|
2ed5160f65 | ||
|
|
fe8ab3f3e8 | ||
|
|
848112979b | ||
|
|
2830df2adc | ||
|
|
5017b55944 | ||
|
|
8d0bc6affe | ||
|
|
d07506266c | ||
|
|
cea1d40797 | ||
|
|
0f2dc9f11e | ||
|
|
23250259eb |
@@ -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: [
|
||||||
{
|
{
|
||||||
|
|||||||
25
.github/workflows/main.yml
vendored
25
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Deploy to GitHub Pages and create release
|
name: Create release
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
@@ -7,27 +7,28 @@ 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
|
||||||
- name: Use Node.js
|
- name: Use Node.js
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 16.x
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@7
|
run: npm install -g npm@8
|
||||||
|
|
||||||
- name: npm install and build
|
- name: npm install and build
|
||||||
run: |
|
run: |
|
||||||
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"
|
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
|
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -11,13 +11,13 @@ 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
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v1
|
||||||
with:
|
with:
|
||||||
node-version: 10.x
|
node-version: 16.x
|
||||||
- name: Install browsers
|
- name: Install browsers
|
||||||
run: |
|
run: |
|
||||||
export DEBIAN_FRONTEND=noninteractive
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
sudo apt-get install -y chromium-browser firefox
|
sudo apt-get install -y chromium-browser firefox
|
||||||
|
|
||||||
- name: Update npm
|
- name: Update npm
|
||||||
run: npm install -g npm@7
|
run: npm install -g npm@8
|
||||||
|
|
||||||
- name: Install the project
|
- name: Install the project
|
||||||
run: npm install
|
run: npm install
|
||||||
|
|||||||
24
Dockerfile.test
Normal file
24
Dockerfile.test
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# An easy way to run tests locally without Nodejs installed:
|
||||||
|
#
|
||||||
|
# docker build -t sqliteviz/test -f Dockerfile.test .
|
||||||
|
#
|
||||||
|
|
||||||
|
FROM node:12.22-buster
|
||||||
|
|
||||||
|
RUN set -ex; \
|
||||||
|
apt update; \
|
||||||
|
apt install -y chromium firefox-esr; \
|
||||||
|
npm install -g npm@7
|
||||||
|
|
||||||
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
COPY lib lib
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN set -ex; \
|
||||||
|
sed -i 's/browsers: \[.*\],/browsers: ['"'FirefoxHeadlessTouch'"'],/' karma.conf.js
|
||||||
|
|
||||||
|
RUN npm run lint -- --no-fix && npm run test
|
||||||
25
README.md
25
README.md
@@ -4,38 +4,41 @@
|
|||||||
|
|
||||||
# sqliteviz
|
# sqliteviz
|
||||||
|
|
||||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
|
||||||
|
of SQLite databases, CSV, JSON or NDJSON files.
|
||||||
|
|
||||||
With sqliteviz you can:
|
With sqliteviz you can:
|
||||||
- run SQL queries against a SQLite database and create [Plotly][11] charts based on the result sets
|
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
|
||||||
- import a CSV file into a SQLite database and visualize imported data
|
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
|
||||||
- manage queries and chart settings and run them against different databases
|
- export result set to CSV file
|
||||||
- import/export queries and chart settings to/from a JSON file
|
- manage inquiries and run them against different databases
|
||||||
|
- 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/117355518-fa332680-aeb2-11eb-8a69-fbcea4f7aeb0.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].
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
|
||||||
|
|
||||||
[1]: https://github.com/plotly/falcon
|
[1]: https://github.com/plotly/falcon
|
||||||
[2]: https://github.com/getredash/redash
|
[2]: https://github.com/getredash/redash
|
||||||
[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
|
||||||
[11]: https://github.com/plotly/plotly.js
|
[11]: https://github.com/plotly/plotly.js
|
||||||
|
[12]: https://github.com/nicolaskruchten/pivottable
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
2
lib/sql-js/.dockerignore
Normal file
2
lib/sql-js/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
benchmark
|
||||||
|
dist
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM emscripten/emsdk:2.0.24
|
FROM emscripten/emsdk:3.0.1
|
||||||
|
|
||||||
WORKDIR /tmp/build
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
|
|||||||
@@ -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,24 @@ 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])
|
||||||
|
3. [sqlitelua][22] -- a virtual table `luafunctions` which allows to define custom scalar,
|
||||||
|
aggregate and table-valued functions in Lua
|
||||||
|
|
||||||
To ease the step to have working clone locally, the build is committed into
|
To ease the step to have working clone locally, the build is committed into
|
||||||
the repository.
|
the repository.
|
||||||
|
|
||||||
|
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 +96,13 @@ 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
|
||||||
|
[22]: https://github.com/kev82/sqlitelua
|
||||||
|
|||||||
4
lib/sql-js/benchmark/.gitignore
vendored
Normal file
4
lib/sql-js/benchmark/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/lib/build-*
|
||||||
|
/lib/dist
|
||||||
|
/build-*-result.json
|
||||||
|
/sample.csv
|
||||||
17
lib/sql-js/benchmark/Dockerfile
Normal file
17
lib/sql-js/benchmark/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
FROM node:20.14-bookworm
|
||||||
|
|
||||||
|
RUN set -ex; \
|
||||||
|
apt-get update; \
|
||||||
|
apt-get install -y firefox-esr; \
|
||||||
|
apt-get install -y chromium
|
||||||
|
|
||||||
|
WORKDIR /tmp/build
|
||||||
|
|
||||||
|
COPY package.json ./
|
||||||
|
COPY lib/dist lib/dist
|
||||||
|
COPY lib/package.json lib/package.json
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD npm run benchmark
|
||||||
25
lib/sql-js/benchmark/README.md
Normal file
25
lib/sql-js/benchmark/README.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# SQLite WebAssembly build micro-benchmark
|
||||||
|
|
||||||
|
This directory contains a micro-benchmark for evaluating SQLite WebAssembly
|
||||||
|
builds performance on read and write SQL queries, run from `make.sh` script. If
|
||||||
|
the script has permission to `nice` processes and [Procpath][1] is installed,
|
||||||
|
e.g. it is run with `sudo -E env PATH=$PATH ./make.sh`, it'll `renice` all
|
||||||
|
processes running inside the benchmark containers. It can also serve as a smoke
|
||||||
|
test (e.g. for memory leaks).
|
||||||
|
|
||||||
|
The benchmark operates on a set of SQLite WebAssembly builds expected in
|
||||||
|
`lib/build-$NAME` directories each containing `sql-wasm.js` and
|
||||||
|
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs the
|
||||||
|
benchmark in Firefox and Chromium using Karma in the container.
|
||||||
|
|
||||||
|
After successful run, the benchmark produces the following per each build:
|
||||||
|
|
||||||
|
- `build-$NAME-result.json`
|
||||||
|
- `build-$NAME.sqlite` (if Procpath is installed)
|
||||||
|
- `build-$NAME.svg` (if Procpath is installed)
|
||||||
|
|
||||||
|
These files can be analysed using `result-analysis.ipynb` Jupyter notebook.
|
||||||
|
The SVG is a chart with CPU and RSS usage of each test container (i.e. Chromium
|
||||||
|
run, then Firefox run per container).
|
||||||
|
|
||||||
|
[1]: https://pypi.org/project/Procpath/
|
||||||
52
lib/sql-js/benchmark/karma.conf.js
Normal file
52
lib/sql-js/benchmark/karma.conf.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
module.exports = function (config) {
|
||||||
|
const timeout = 15 * 60 * 1000
|
||||||
|
config.set({
|
||||||
|
|
||||||
|
frameworks: ['mocha'],
|
||||||
|
|
||||||
|
files: [
|
||||||
|
'suite.js',
|
||||||
|
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm', served: true, included: false },
|
||||||
|
{ pattern: 'sample.csv', served: true, included: false }
|
||||||
|
],
|
||||||
|
|
||||||
|
reporters: ['progress', 'json-to-file'],
|
||||||
|
|
||||||
|
singleRun: true,
|
||||||
|
|
||||||
|
customLaunchers: {
|
||||||
|
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] }
|
||||||
|
},
|
||||||
|
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
|
||||||
|
concurrency: 1,
|
||||||
|
|
||||||
|
browserDisconnectTimeout: timeout,
|
||||||
|
browserNoActivityTimeout: timeout,
|
||||||
|
captureTimeout: timeout,
|
||||||
|
browserSocketTimeout: timeout,
|
||||||
|
pingTimeout: timeout,
|
||||||
|
client: {
|
||||||
|
captureConsole: true,
|
||||||
|
mocha: { timeout: timeout }
|
||||||
|
},
|
||||||
|
|
||||||
|
logLevel: config.LOG_INFO,
|
||||||
|
browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
|
||||||
|
|
||||||
|
preprocessors: { 'suite.js': [ 'webpack' ] },
|
||||||
|
webpack: {
|
||||||
|
mode: 'development',
|
||||||
|
module: {
|
||||||
|
noParse: [ __dirname + '/node_modules/benchmark/benchmark.js' ]
|
||||||
|
},
|
||||||
|
node: { fs: 'empty' }
|
||||||
|
},
|
||||||
|
|
||||||
|
proxies: {
|
||||||
|
'/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
|
||||||
|
},
|
||||||
|
|
||||||
|
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
5
lib/sql-js/benchmark/lib/package.json
Normal file
5
lib/sql-js/benchmark/lib/package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name": "sql.js",
|
||||||
|
"main": "./dist/sql-wasm.js",
|
||||||
|
"private": true
|
||||||
|
}
|
||||||
47
lib/sql-js/benchmark/make.sh
Executable file
47
lib/sql-js/benchmark/make.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/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
|
||||||
|
wget --header="accept-encoding: gzip" -q -O- \
|
||||||
|
https://github.com/plotly/datasets/raw/547090bd/wellspublic.csv \
|
||||||
|
| gunzip -c > sample.csv
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLAYBOOK=procpath/karma_docker.procpath
|
||||||
|
|
||||||
|
# for renice to work run like "sudo -E env PATH=$PATH ./make.sh"
|
||||||
|
test_ni=$(nice -n -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
|
||||||
|
rm -rf lib/dist
|
||||||
|
cp -r $d lib/dist
|
||||||
|
sample_name=$(basename $d)
|
||||||
|
|
||||||
|
docker build -t sqliteviz/sqljs-benchmark .
|
||||||
|
docker rm sqljs-benchmark-run 2> /dev/null || true
|
||||||
|
docker run -d -it --cpus 2 --name sqljs-benchmark-run sqliteviz/sqljs-benchmark
|
||||||
|
{
|
||||||
|
rm -f ${sample_name}.sqlite
|
||||||
|
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite track:record
|
||||||
|
procpath play -f $PLAYBOOK -o database_file=${sample_name}.sqlite \
|
||||||
|
-o plot_file=${sample_name}.svg track:plot
|
||||||
|
} &
|
||||||
|
|
||||||
|
docker attach sqljs-benchmark-run
|
||||||
|
docker cp sqljs-benchmark-run:/tmp/build/suite-result.json ${sample_name}-result.json
|
||||||
|
docker rm sqljs-benchmark-run
|
||||||
|
done
|
||||||
23
lib/sql-js/benchmark/package.json
Normal file
23
lib/sql-js/benchmark/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "sqlite-webassembly-microbenchmark",
|
||||||
|
"private": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/core" : "^7.14.8",
|
||||||
|
"babel-loader": "^8.2.2",
|
||||||
|
"benchmark": "^2.1.4",
|
||||||
|
"lodash": "^4.17.4",
|
||||||
|
"papaparse": "^5.3.1",
|
||||||
|
"mocha": "^9.0.3",
|
||||||
|
"karma": "^6.3.4",
|
||||||
|
"karma-chrome-launcher": "^3.1.0",
|
||||||
|
"karma-firefox-launcher": "^2.1.1",
|
||||||
|
"karma-json-to-file-reporter" : "^1.0.1",
|
||||||
|
"karma-mocha": "^2.0.1",
|
||||||
|
"karma-webpack": "^4.0.2",
|
||||||
|
"webpack": "^4.46.0",
|
||||||
|
"sql.js": "file:./lib"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"benchmark": "karma start karma.conf.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
28
lib/sql-js/benchmark/procpath/karma_docker.procpath
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# This command may run when "sqljs-benchmark-run" does not yet exist or run
|
||||||
|
[renice:watch]
|
||||||
|
interval: 2
|
||||||
|
repeat: 30
|
||||||
|
environment:
|
||||||
|
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run 2> /dev/null || true
|
||||||
|
query:
|
||||||
|
PIDS=$..children[?(@.stat.pid in [$ROOT_PID])]..pid
|
||||||
|
command:
|
||||||
|
echo $PIDS | tr , '\n' | xargs --no-run-if-empty -I{} -- renice -n -5 -p {}
|
||||||
|
|
||||||
|
# Expected input arguments: database_file
|
||||||
|
[track:record]
|
||||||
|
interval: 1
|
||||||
|
stop_without_result: 1
|
||||||
|
environment:
|
||||||
|
ROOT_PID=docker inspect -f "{{.State.Pid}}" sqljs-benchmark-run
|
||||||
|
query:
|
||||||
|
$..children[?(@.stat.pid == $ROOT_PID)]
|
||||||
|
pid_list: $ROOT_PID
|
||||||
|
|
||||||
|
# Expected input arguments: database_file, plot_file
|
||||||
|
[track:plot]
|
||||||
|
moving_average_window: 5
|
||||||
|
title: Chromium vs Firefox (№1 RSS, №2 CPU)
|
||||||
|
custom_query_file:
|
||||||
|
procpath/top2_rss.sql
|
||||||
|
procpath/top2_cpu.sql
|
||||||
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
29
lib/sql-js/benchmark/procpath/top2_cpu.sql
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
WITH diff_all AS (
|
||||||
|
SELECT
|
||||||
|
record_id,
|
||||||
|
ts,
|
||||||
|
stat_pid,
|
||||||
|
stat_utime + stat_stime - LAG(stat_utime + stat_stime) OVER (
|
||||||
|
PARTITION BY stat_pid
|
||||||
|
ORDER BY record_id
|
||||||
|
) tick_diff,
|
||||||
|
ts - LAG(ts) OVER (
|
||||||
|
PARTITION BY stat_pid
|
||||||
|
ORDER BY record_id
|
||||||
|
) ts_diff
|
||||||
|
FROM record
|
||||||
|
), diff AS (
|
||||||
|
SELECT * FROM diff_all WHERE tick_diff IS NOT NULL
|
||||||
|
), one_time_pid_condition AS (
|
||||||
|
SELECT stat_pid
|
||||||
|
FROM record
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY SUM(stat_utime + stat_stime) DESC
|
||||||
|
LIMIT 2
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ts,
|
||||||
|
stat_pid pid,
|
||||||
|
100.0 * tick_diff / (SELECT value FROM meta WHERE key = 'clock_ticks') / ts_diff value
|
||||||
|
FROM diff
|
||||||
|
JOIN one_time_pid_condition USING(stat_pid)
|
||||||
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
13
lib/sql-js/benchmark/procpath/top2_rss.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
WITH one_time_pid_condition AS (
|
||||||
|
SELECT stat_pid
|
||||||
|
FROM record
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY SUM(stat_rss) DESC
|
||||||
|
LIMIT 2
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
ts,
|
||||||
|
stat_pid pid,
|
||||||
|
stat_rss / 1024.0 / 1024 * (SELECT value FROM meta WHERE key = 'page_size') value
|
||||||
|
FROM record
|
||||||
|
JOIN one_time_pid_condition USING(stat_pid)
|
||||||
218
lib/sql-js/benchmark/result-analysis.ipynb
Normal file
218
lib/sql-js/benchmark/result-analysis.ipynb
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"import json\n",
|
||||||
|
"from pathlib import Path\n",
|
||||||
|
"\n",
|
||||||
|
"import pandas\n",
|
||||||
|
"from IPython.display import display, IFrame, Markdown\n",
|
||||||
|
"from plotly import express"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false,
|
||||||
|
"outputHidden": false,
|
||||||
|
"inputHidden": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"render_format = 'svg'\n",
|
||||||
|
"benchmark_path = Path()\n",
|
||||||
|
"df_dict = {}\n",
|
||||||
|
"stat_dict = {}\n",
|
||||||
|
"for p in benchmark_path.glob('build-*-result.json'):\n",
|
||||||
|
" build_name = p.stem.split('-', 2)[1]\n",
|
||||||
|
" for browser_data in json.loads(p.read_text()):\n",
|
||||||
|
" browser_name = f'{browser_data[\"browser\"][\"name\"]} {browser_data[\"browser\"][\"major\"]}'\n",
|
||||||
|
" browser_name = browser_name.lower().replace('chrome headless', 'chromium')\n",
|
||||||
|
" for result in (r for i, r in browser_data['result'].items() if i.isdigit()):\n",
|
||||||
|
" key = build_name, browser_name, result['name']\n",
|
||||||
|
" df_dict[key] = result['stats']['sample']\n",
|
||||||
|
" stat_dict[key] = result['stats']\n",
|
||||||
|
"\n",
|
||||||
|
"min_sample_size = min(len(v) for v in df_dict.values())\n",
|
||||||
|
"df_dict = {k: v[:min_sample_size] for k, v in df_dict.items()}\n",
|
||||||
|
" \n",
|
||||||
|
"wide_df = pandas.DataFrame(df_dict).reset_index()\n",
|
||||||
|
"df = pandas.melt(\n",
|
||||||
|
" wide_df, \n",
|
||||||
|
" id_vars='index', \n",
|
||||||
|
" var_name=['build', 'browser', 'test'], \n",
|
||||||
|
" value_name='duration',\n",
|
||||||
|
")\n",
|
||||||
|
"df = df.rename(columns={'index': 'run'})\n",
|
||||||
|
"df.sort_values(['build', 'run'], inplace=True)"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"source": [
|
||||||
|
"# sql.js build comparison\n",
|
||||||
|
"\n",
|
||||||
|
"<style>\n",
|
||||||
|
"@page {\n",
|
||||||
|
" size: 215mm 297mm;\n",
|
||||||
|
" margin: 0;\n",
|
||||||
|
"}\n",
|
||||||
|
"</style>"
|
||||||
|
],
|
||||||
|
"metadata": {}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"!du -b lib | head -n 2"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false,
|
||||||
|
"outputHidden": false,
|
||||||
|
"inputHidden": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"stat_df = pandas.DataFrame(stat_dict)\n",
|
||||||
|
"stat_df = stat_df.loc[['mean', 'rme']].transpose()\n",
|
||||||
|
"stat_df.index = stat_df.index.set_names(['build', 'browser', 'test'])\n",
|
||||||
|
"stat_df = stat_df.reset_index().sort_values(['test', 'browser'], ascending=False)\n",
|
||||||
|
"for index, row in stat_df.iterrows():\n",
|
||||||
|
" print('\\t'.join([\n",
|
||||||
|
" row['build'],\n",
|
||||||
|
" row['browser'],\n",
|
||||||
|
" row['test'],\n",
|
||||||
|
" f'{row[\"mean\"]:.2f} s ± {row[\"rme\"]:.1f}%'\n",
|
||||||
|
" ]))"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"fig = express.box(df, x='browser', y='duration', points='all', color='build', facet_row='test')\n",
|
||||||
|
"fig.update_yaxes(matches=None)\n",
|
||||||
|
"fig.show(render_format)"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"fig = express.line(\n",
|
||||||
|
" df, x='run', y='duration', color='build', facet_col='browser', facet_row='test'\n",
|
||||||
|
")\n",
|
||||||
|
"fig.update_yaxes(matches=None)\n",
|
||||||
|
"fig.show(render_format)"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"plot_df = df.groupby(['browser', 'build', 'test'])['duration'].mean().reset_index()\n",
|
||||||
|
"plot_df['pct'] = (\n",
|
||||||
|
" plot_df\n",
|
||||||
|
" .groupby(['browser', 'test'])['duration']\n",
|
||||||
|
" .pct_change()\n",
|
||||||
|
" .fillna('')\n",
|
||||||
|
" .map(lambda v: '{:.2%}'.format(v) if v else v)\n",
|
||||||
|
")\n",
|
||||||
|
"fig = express.bar(\n",
|
||||||
|
" plot_df, \n",
|
||||||
|
" x='browser', \n",
|
||||||
|
" y='duration', \n",
|
||||||
|
" color='build', \n",
|
||||||
|
" text='pct', \n",
|
||||||
|
" barmode='overlay', \n",
|
||||||
|
" facet_row='test',\n",
|
||||||
|
")\n",
|
||||||
|
"fig.update_yaxes(matches=None)\n",
|
||||||
|
"fig.show(render_format)"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"source": [
|
||||||
|
"for p in sorted(benchmark_path.glob('build-*.svg')):\n",
|
||||||
|
" display(Markdown(p.stem))\n",
|
||||||
|
" display(IFrame(p, 640, 480))"
|
||||||
|
],
|
||||||
|
"outputs": [],
|
||||||
|
"execution_count": null,
|
||||||
|
"metadata": {
|
||||||
|
"collapsed": false,
|
||||||
|
"outputHidden": false,
|
||||||
|
"inputHidden": true,
|
||||||
|
"outputExpanded": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"name": "stats",
|
||||||
|
"language": "python",
|
||||||
|
"display_name": "Stats (Python 3.10)"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"name": "python",
|
||||||
|
"version": "3.10.14",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"file_extension": ".py"
|
||||||
|
},
|
||||||
|
"widgets": {
|
||||||
|
"application/vnd.jupyter.widget-state+json": {
|
||||||
|
"state": {},
|
||||||
|
"version_major": 2,
|
||||||
|
"version_minor": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"kernel_info": {
|
||||||
|
"name": "stats"
|
||||||
|
},
|
||||||
|
"nteract": {
|
||||||
|
"version": "0.14.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
140
lib/sql-js/benchmark/suite.js
Normal file
140
lib/sql-js/benchmark/suite.js
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
import benchmark from 'benchmark'
|
||||||
|
import initSqlJs from 'sql.js'
|
||||||
|
import lodash from 'lodash'
|
||||||
|
import Papa from 'papaparse'
|
||||||
|
import useragent from 'ua-parser-js'
|
||||||
|
|
||||||
|
|
||||||
|
describe('SQLite build benchmark', function () {
|
||||||
|
let parsedCsv
|
||||||
|
let sqlModule
|
||||||
|
let selectDb
|
||||||
|
|
||||||
|
before(async function () {
|
||||||
|
parsedCsv = await parseCsv('http://localhost:9876/base/sample.csv')
|
||||||
|
sqlModule = await initSqlJs()
|
||||||
|
|
||||||
|
selectDb = new sqlModule.Database()
|
||||||
|
importToTable(selectDb, parsedCsv)
|
||||||
|
})
|
||||||
|
|
||||||
|
function benchmarkImport () {
|
||||||
|
const db = new sqlModule.Database()
|
||||||
|
try {
|
||||||
|
importToTable(db, parsedCsv)
|
||||||
|
} finally {
|
||||||
|
db.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function benchmarkSelect () {
|
||||||
|
const result = selectDb.exec(`
|
||||||
|
SELECT county, AVG(avg_depth) avg_depth_c
|
||||||
|
FROM (
|
||||||
|
SELECT s.county, s.town, COUNT(*) cnt, AVG(s.DrilledDepth) avg_depth
|
||||||
|
FROM csv_import s
|
||||||
|
JOIN csv_import USING(hole)
|
||||||
|
WHERE s.town IS NOT NULL
|
||||||
|
GROUP BY 1, 2
|
||||||
|
ORDER BY 4 DESC
|
||||||
|
)
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 2 DESC
|
||||||
|
`)
|
||||||
|
console.assert(result.values.length == 56, 'Unexpected size of result set')
|
||||||
|
}
|
||||||
|
|
||||||
|
it('run', async function () {
|
||||||
|
const suite = createSuite()
|
||||||
|
suite.add('import', { initCount: 3, minSamples: 50, fn: benchmarkImport })
|
||||||
|
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
|
||||||
|
await run(suite)
|
||||||
|
})
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
function importToTable (db, parsedCsv, chunkSize = 1024) {
|
||||||
|
const columnListString = parsedCsv.meta.fields.join(', ')
|
||||||
|
db.exec(`CREATE TABLE csv_import(${columnListString})`)
|
||||||
|
|
||||||
|
const params = parsedCsv.meta.fields.map(name => '?').join(', ')
|
||||||
|
const insertStmt = db.prepare(`INSERT INTO csv_import VALUES(${params})`)
|
||||||
|
chunkArray(parsedCsv.data, chunkSize).map(function (chunk) {
|
||||||
|
db.exec('BEGIN')
|
||||||
|
chunk.map(row => insertStmt.run(Object.values(row)))
|
||||||
|
db.exec('COMMIT')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class PromiseWrapper {
|
||||||
|
constructor() {
|
||||||
|
this.promise = new Promise((resolve, reject) => {
|
||||||
|
this.reject = reject
|
||||||
|
this.resolve = resolve
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCsv(url) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
Papa.parse(url, {
|
||||||
|
header: true,
|
||||||
|
download: true,
|
||||||
|
skipEmptyLines: 'greedy',
|
||||||
|
complete: results => resolve(results),
|
||||||
|
error: (error, file) => reject(error)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function chunkArray (arr, size) {
|
||||||
|
return arr.reduce(function (result, value, index) {
|
||||||
|
const chunkIndex = Math.floor(index / size)
|
||||||
|
|
||||||
|
if(!(chunkIndex in result)) {
|
||||||
|
result[chunkIndex] = []
|
||||||
|
}
|
||||||
|
result[chunkIndex].push(value)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function createSuite () {
|
||||||
|
// Combined workaround from:
|
||||||
|
// - https://github.com/bestiejs/benchmark.js/issues/106
|
||||||
|
// - https://github.com/bestiejs/benchmark.js/issues/237
|
||||||
|
|
||||||
|
// Benchmark could not pick up lodash otherwise
|
||||||
|
const bm = benchmark.runInContext({ _: lodash })
|
||||||
|
// Avoid `ReferenceError: Benchmark is not defined` error because Benchmark is assumed
|
||||||
|
// to be in window
|
||||||
|
window.Benchmark = bm
|
||||||
|
|
||||||
|
return new bm.Suite()
|
||||||
|
}
|
||||||
|
|
||||||
|
function run (suite) {
|
||||||
|
const suiteResult = new PromiseWrapper()
|
||||||
|
suite
|
||||||
|
.on('cycle', function (event) {
|
||||||
|
console.info(String(event.target))
|
||||||
|
})
|
||||||
|
.on('complete', function () {
|
||||||
|
console.log(JSON.stringify({
|
||||||
|
browser: useragent(navigator.userAgent).browser,
|
||||||
|
result: this.filter('successful')
|
||||||
|
}))
|
||||||
|
suiteResult.resolve()
|
||||||
|
})
|
||||||
|
.on('error', function (event) {
|
||||||
|
console.error('Benchmark failed', String(event.target))
|
||||||
|
suiteResult.reject()
|
||||||
|
})
|
||||||
|
.run({async: true})
|
||||||
|
|
||||||
|
return suiteResult.promise
|
||||||
|
}
|
||||||
@@ -2,35 +2,41 @@ 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_OMIT_LOAD_EXTENSION',
|
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
|
||||||
|
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
||||||
|
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
|
||||||
'-DSQLITE_DISABLE_LFS',
|
'-DSQLITE_DISABLE_LFS',
|
||||||
|
'-DSQLITE_DQS=0',
|
||||||
'-DSQLITE_ENABLE_FTS3',
|
'-DSQLITE_ENABLE_FTS3',
|
||||||
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
|
||||||
'-DSQLITE_ENABLE_FTS5',
|
'-DSQLITE_ENABLE_FTS5',
|
||||||
'-DSQLITE_ENABLE_JSON1',
|
|
||||||
'-DSQLITE_THREADSAFE=0',
|
|
||||||
'-DSQLITE_ENABLE_NORMALIZE',
|
'-DSQLITE_ENABLE_NORMALIZE',
|
||||||
'-DSQLITE_EXTRA_INIT=extra_init',
|
'-DSQLITE_EXTRA_INIT=extra_init',
|
||||||
'-DSQLITE_DEFAULT_MEMSTATUS=0',
|
'-DSQLITE_OMIT_DEPRECATED',
|
||||||
'-DSQLITE_USE_ALLOCA',
|
'-DSQLITE_OMIT_LOAD_EXTENSION',
|
||||||
|
'-DSQLITE_OMIT_SHARED_CACHE',
|
||||||
|
'-DSQLITE_THREADSAFE=0',
|
||||||
|
# Compile-time optimisation
|
||||||
|
'-Os', # reduces the code size about in half comparing to -O2
|
||||||
|
'-flto',
|
||||||
|
'-Isrc', '-Isrc/lua',
|
||||||
)
|
)
|
||||||
emflags = (
|
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',
|
||||||
'--closure', '1',
|
|
||||||
# sql.js
|
# sql.js
|
||||||
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
|
||||||
'-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
|
'-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
|
||||||
@@ -47,22 +53,32 @@ 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 LLVM bitcode for SQLite Lua extension')
|
||||||
|
subprocess.check_call([
|
||||||
|
'emcc',
|
||||||
|
*cflags,
|
||||||
|
'-shared',
|
||||||
|
*(src / 'lua').glob('*.c'),
|
||||||
|
*(src / 'sqlitelua').glob('*.c'),
|
||||||
|
'-o', out / 'sqlitelua.o',
|
||||||
])
|
])
|
||||||
|
|
||||||
logging.info('Building WASM from bitcode')
|
logging.info('Building WASM from bitcode')
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
'emcc',
|
'emcc',
|
||||||
*emflags,
|
*emflags,
|
||||||
out / 'sqlite3.bc',
|
out / 'sqlite3.o',
|
||||||
out / 'extension-functions.bc',
|
out / 'extension-functions.o',
|
||||||
|
out / 'sqlitelua.o',
|
||||||
'-o', out / 'sql-wasm.js',
|
'-o', out / 'sql-wasm.js',
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
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 +22,25 @@ 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'),
|
||||||
|
# Third-party extension with own dependencies
|
||||||
|
# ===========================================
|
||||||
|
('https://github.com/kev82/sqlitelua/raw/db479510/src/main.c', 'sqlite3_luafunctions_init'),
|
||||||
)
|
)
|
||||||
|
|
||||||
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip'
|
lua_url = 'http://www.lua.org/ftp/lua-5.3.5.tar.gz'
|
||||||
|
sqlitelua_url = 'https://github.com/kev82/sqlitelua/archive/db479510.zip'
|
||||||
|
|
||||||
|
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.7.0.zip'
|
||||||
|
|
||||||
|
|
||||||
def _generate_extra_init_c_function(init_function_names):
|
def _generate_extra_init_c_function(init_function_names):
|
||||||
@@ -56,6 +67,38 @@ def _get_amalgamation(tgt: Path):
|
|||||||
shutil.copyfileobj(fr, fw)
|
shutil.copyfileobj(fr, fw)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_lua(tgt: Path):
|
||||||
|
# Library definitions from lua/Makefile
|
||||||
|
lib_str = '''
|
||||||
|
CORE_O= lapi.o lcode.o lctype.o ldebug.o ldo.o ldump.o lfunc.o lgc.o llex.o \
|
||||||
|
lmem.o lobject.o lopcodes.o lparser.o lstate.o lstring.o ltable.o \
|
||||||
|
ltm.o lundump.o lvm.o lzio.o
|
||||||
|
LIB_O= lauxlib.o lbaselib.o lbitlib.o lcorolib.o ldblib.o liolib.o \
|
||||||
|
lmathlib.o loslib.o lstrlib.o ltablib.o lutf8lib.o loadlib.o linit.o
|
||||||
|
LUA_O= lua.o
|
||||||
|
'''
|
||||||
|
header_only_files = {'lprefix', 'luaconf', 'llimits', 'lualib'}
|
||||||
|
lib_names = set(re.findall(r'(\w+)\.o', lib_str)) | header_only_files
|
||||||
|
|
||||||
|
logging.info('Downloading and extracting Lua %s', lua_url)
|
||||||
|
archive = tarfile.open(fileobj=BytesIO(request.urlopen(lua_url).read()))
|
||||||
|
(tgt / 'lua').mkdir()
|
||||||
|
for tarinfo in archive:
|
||||||
|
tarpath = Path(tarinfo.name)
|
||||||
|
if tarpath.match('src/*') and tarpath.stem in lib_names:
|
||||||
|
with (tgt / 'lua' / tarpath.name).open('wb') as fw:
|
||||||
|
shutil.copyfileobj(archive.extractfile(tarinfo), fw)
|
||||||
|
|
||||||
|
logging.info('Downloading and extracting SQLite Lua extension %s', sqlitelua_url)
|
||||||
|
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqlitelua_url).read()))
|
||||||
|
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
|
||||||
|
(tgt / 'sqlitelua').mkdir()
|
||||||
|
for zpath in (archive_root_dir / 'src').iterdir():
|
||||||
|
if zpath.name != 'main.c':
|
||||||
|
with zpath.open() as fr, (tgt / 'sqlitelua' / zpath.name).open('wb') as fw:
|
||||||
|
shutil.copyfileobj(fr, fw)
|
||||||
|
|
||||||
|
|
||||||
def _get_contrib_functions(tgt: Path):
|
def _get_contrib_functions(tgt: Path):
|
||||||
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
|
||||||
|
|
||||||
@@ -67,6 +110,7 @@ def _get_extensions(tgt: Path):
|
|||||||
for url, init_fn in extension_urls:
|
for url, init_fn in extension_urls:
|
||||||
logging.info('Downloading and appending to amalgamation %s', url)
|
logging.info('Downloading and appending to amalgamation %s', url)
|
||||||
with request.urlopen(url) as resp:
|
with request.urlopen(url) as resp:
|
||||||
|
f.write(b'\n')
|
||||||
shutil.copyfileobj(resp, f)
|
shutil.copyfileobj(resp, f)
|
||||||
init_functions.append(init_fn)
|
init_functions.append(init_fn)
|
||||||
|
|
||||||
@@ -87,6 +131,7 @@ def _get_sqljs(tgt: Path):
|
|||||||
def configure(tgt: Path):
|
def configure(tgt: Path):
|
||||||
_get_amalgamation(tgt)
|
_get_amalgamation(tgt)
|
||||||
_get_contrib_functions(tgt)
|
_get_contrib_functions(tgt)
|
||||||
|
_get_lua(tgt)
|
||||||
_get_extensions(tgt)
|
_get_extensions(tgt)
|
||||||
_get_sqljs(tgt)
|
_get_sqljs(tgt)
|
||||||
|
|
||||||
|
|||||||
109
lib/sql-js/dist/sql-wasm.js
vendored
109
lib/sql-js/dist/sql-wasm.js
vendored
File diff suppressed because one or more lines are too long
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Normal file → Executable file
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Normal file → Executable file
Binary file not shown.
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "sql.js",
|
"name": "sql.js",
|
||||||
"main": "./dist/sql-wasm.js"
|
"main": "./dist/sql-wasm.js",
|
||||||
}
|
"private": true
|
||||||
|
}
|
||||||
|
|||||||
32058
package-lock.json
generated
32058
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
22
package.json
22
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sqliteviz",
|
"name": "sqliteviz",
|
||||||
"version": "0.14.0",
|
"version": "0.25.1",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,21 +10,26 @@
|
|||||||
"lint": "vue-cli-service lint"
|
"lint": "vue-cli-service lint"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"codemirror": "^5.57.0",
|
"codemirror": "^5.65.18",
|
||||||
"core-js": "^3.6.5",
|
"core-js": "^3.6.5",
|
||||||
|
"dataurl-to-blob": "^0.0.1",
|
||||||
|
"html2canvas": "^1.1.4",
|
||||||
|
"jquery": "^3.6.0",
|
||||||
"nanoid": "^3.1.12",
|
"nanoid": "^3.1.12",
|
||||||
"papaparse": "^5.3.1",
|
"papaparse": "^5.4.1",
|
||||||
"plotly.js": "^1.58.4",
|
"pivottable": "^2.23.0",
|
||||||
|
"plotly.js": "^2.35.2",
|
||||||
"promise-worker": "^2.0.1",
|
"promise-worker": "^2.0.1",
|
||||||
"react": "^16.13.1",
|
"react": "^16.14.0",
|
||||||
"react-chart-editor": "^0.45.0",
|
"react-chart-editor": "^0.46.1",
|
||||||
"react-dom": "^16.13.1",
|
"react-dom": "^16.14.0",
|
||||||
"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",
|
||||||
|
"vue-multiselect": "^2.1.6",
|
||||||
"vue-router": "^3.2.0",
|
"vue-router": "^3.2.0",
|
||||||
|
"vue2-teleport": "^1.0.1",
|
||||||
"vuejs-paginate": "^2.1.0",
|
"vuejs-paginate": "^2.1.0",
|
||||||
"vuera": "^0.2.7",
|
"vuera": "^0.2.7",
|
||||||
"vuex": "^3.4.0"
|
"vuex": "^3.4.0"
|
||||||
@@ -46,6 +51,7 @@
|
|||||||
"eslint-plugin-promise": "^4.2.1",
|
"eslint-plugin-promise": "^4.2.1",
|
||||||
"eslint-plugin-standard": "^4.0.0",
|
"eslint-plugin-standard": "^4.0.0",
|
||||||
"eslint-plugin-vue": "^6.2.2",
|
"eslint-plugin-vue": "^6.2.2",
|
||||||
|
"flush-promises": "^1.0.2",
|
||||||
"karma": "^3.1.4",
|
"karma": "^3.1.4",
|
||||||
"karma-firefox-launcher": "^2.1.0",
|
"karma-firefox-launcher": "^2.1.0",
|
||||||
"karma-webpack": "^4.0.2",
|
"karma-webpack": "^4.0.2",
|
||||||
|
|||||||
@@ -69,6 +69,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<!-- head extention slot start -->
|
||||||
|
<!-- head extention slot end -->
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>
|
<noscript>
|
||||||
@@ -93,6 +95,8 @@
|
|||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- extention slot start -->
|
||||||
|
<!-- extention slot end -->
|
||||||
<!-- built files will be auto injected -->
|
<!-- built files will be auto injected -->
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"background_color": "white",
|
"background_color": "white",
|
||||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases, CSV, JSON or NDJSON.",
|
||||||
"display": "fullscreen",
|
"display": "fullscreen",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
|
|||||||
23
src/App.vue
23
src/App.vue
@@ -4,6 +4,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import storedInquiries from '@/lib/storedInquiries'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
created () {
|
||||||
|
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
inquiries () {
|
||||||
|
return this.$store.state.inquiries
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
inquiries: {
|
||||||
|
deep: true,
|
||||||
|
handler () {
|
||||||
|
storedInquiries.updateStorage(this.inquiries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: "Open Sans";
|
font-family: "Open Sans";
|
||||||
|
|||||||
3
src/assets/images/arrow-hover.svg
Normal file
3
src/assets/images/arrow-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.51581 7.54801C4.95181 7.10201 5.55881 7.06701 6.09181 7.54801L9.99981 11.295L13.9078 7.54801C14.4408 7.06701 15.0488 7.10201 15.4818 7.54801C15.9178 7.99301 15.8898 8.74501 15.4818 9.16301C15.0758 9.58101 10.7868 13.665 10.7868 13.665C10.5698 13.888 10.2848 14 9.99981 14C9.71481 14 9.42981 13.888 9.21081 13.665C9.21081 13.665 4.92381 9.58101 4.51581 9.16301C4.10781 8.74501 4.07981 7.99301 4.51581 7.54801V7.54801Z" fill="#119DFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 550 B |
3
src/assets/images/arrow.svg
Normal file
3
src/assets/images/arrow.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.51581 7.54801C4.95181 7.10201 5.55881 7.06701 6.09181 7.54801L9.99981 11.295L13.9078 7.54801C14.4408 7.06701 15.0488 7.10201 15.4818 7.54801C15.9178 7.99301 15.8898 8.74501 15.4818 9.16301C15.0758 9.58101 10.7868 13.665 10.7868 13.665C10.5698 13.888 10.2848 14 9.99981 14C9.71481 14 9.42981 13.888 9.21081 13.665C9.21081 13.665 4.92381 9.58101 4.51581 9.16301C4.10781 8.74501 4.07981 7.99301 4.51581 7.54801V7.54801Z" fill="#C8D4E3"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 550 B |
3
src/assets/images/delete-tag-hover.svg
Normal file
3
src/assets/images/delete-tag-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.0436 10.3943C9.7153 10.7226 9.1833 10.7226 8.8557 10.3943L7 8.27329L5.1443 10.3936C4.816 10.7219 4.284 10.7219 3.9564 10.3936C3.6281 10.0653 3.6281 9.53329 3.9564 9.20569L5.887 7.00069L3.9557 4.79429C3.6274 4.46599 3.6274 3.93469 3.9557 3.60639C4.284 3.27809 4.8153 3.27809 5.1436 3.60639L7 5.72809L8.8557 3.60639C9.184 3.27809 9.7153 3.27809 10.0436 3.60639C10.3719 3.93469 10.3719 4.46669 10.0436 4.79429L8.113 7.00069L10.0436 9.20569C10.3719 9.53399 10.3719 10.066 10.0436 10.3943V10.3943Z" fill="#DE350B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 627 B |
3
src/assets/images/delete-tag.svg
Normal file
3
src/assets/images/delete-tag.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M10.0436 10.3945C9.7153 10.7228 9.1833 10.7228 8.8557 10.3945L7 8.27348L5.1443 10.3938C4.816 10.7221 4.284 10.7221 3.9564 10.3938C3.6281 10.0655 3.6281 9.53348 3.9564 9.20588L5.887 7.00088L3.9557 4.79448C3.6274 4.46618 3.6274 3.93488 3.9557 3.60658C4.284 3.27828 4.8153 3.27828 5.1436 3.60658L7 5.72828L8.8557 3.60658C9.184 3.27828 9.7153 3.27828 10.0436 3.60658C10.3719 3.93488 10.3719 4.46688 10.0436 4.79448L8.113 7.00088L10.0436 9.20588C10.3719 9.53418 10.3719 10.0662 10.0436 10.3945V10.3945Z" fill="#506784"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 628 B |
3
src/assets/images/logo_simple.svg
Normal file
3
src/assets/images/logo_simple.svg
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M26.8311 34.6554C25.4675 33.8178 24.177 32.8655 22.9735 31.8086V14.3616H30.5728V36.753C29.3146 36.0982 28.0673 35.399 26.8311 34.6554ZM41.4669 25.8486H33.8675V38.1514C36.3477 39.3055 38.884 40.3334 41.4669 41.2313V25.8486ZM22.9735 35.3046L22.4768 34.9051C21.7152 34.2725 21.0033 33.6232 20.3245 32.9739L2.2947 30.8763L5.60596 37.3024L28.7848 39.2002C26.7511 38.0537 24.8082 36.7513 22.9735 35.3046ZM41.0695 44.6441C38.4829 43.7946 35.9458 42.7997 33.4702 41.6641L32.543 41.198L17.2616 40.1825L19.8444 45.593L46.5 46.209C44.6788 45.7761 42.8411 45.2434 41.0695 44.6441ZM9.34768 14.3616C12.2649 19.4905 15.735 24.2807 19.6954 28.6455V11.2651L2.99007 2.99115L1.5 22.3859L18.702 31.2592C14.1919 26.5283 10.9703 20.7087 9.34768 14.3616Z" fill="#119DFF"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 862 B |
11
src/assets/images/sort.svg
Normal file
11
src/assets/images/sort.svg
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<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 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"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="6" height="12" fill="white" transform="matrix(1 0 0 -1 0 12)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 964 B |
136
src/assets/styles/multiselect.css
Normal file
136
src/assets/styles/multiselect.css
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
.sqliteviz-select,
|
||||||
|
.sqliteviz-select .multiselect__tags {
|
||||||
|
min-height: 36px;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__select {
|
||||||
|
height: 34px;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 6px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__tags {
|
||||||
|
border-radius: var(--border-radius-medium-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
padding: 4px 32px 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select,
|
||||||
|
.sqliteviz-select .multiselect__input,
|
||||||
|
.sqliteviz-select .multiselect__single,
|
||||||
|
.sqliteviz-select .multiselect__placeholder {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__single,
|
||||||
|
.sqliteviz-select .multiselect__placeholder,
|
||||||
|
.sqliteviz-select .multiselect__input {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__input {
|
||||||
|
width: 0 !important;
|
||||||
|
color: var(--color-text-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--active .multiselect__input {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__placeholder,
|
||||||
|
.sqliteviz-select .multiselect__input::placeholder {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__option.multiselect__option--highlight {
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
color: var(--color-text-active);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__tag {
|
||||||
|
background-color: var(--color-bg-light-4);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
color: var(--color-text-active);
|
||||||
|
font-size: 11.05px;
|
||||||
|
margin: 2px;
|
||||||
|
}
|
||||||
|
.sqliteviz-select .multiselect__tag-icon:after {
|
||||||
|
content: url('~@/assets/images/delete-tag.svg');
|
||||||
|
height: 14px;
|
||||||
|
width: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__tag-icon:focus:after,
|
||||||
|
.sqliteviz-select .multiselect__tag-icon:hover:after {
|
||||||
|
content: url('~@/assets/images/delete-tag-hover.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__tag-icon:focus,
|
||||||
|
.sqliteviz-select .multiselect__tag-icon:hover {
|
||||||
|
background-color: var(--color-bg-danger);
|
||||||
|
border-radius: var(--border-radius-small);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__option {
|
||||||
|
min-height: 29px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
line-height: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__option:after {
|
||||||
|
line-height: 29px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__content-wrapper {
|
||||||
|
border-radius: var(--border-radius-medium-2);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
box-shadow: var(--shadow-1);
|
||||||
|
top: calc(100% - 1px);
|
||||||
|
max-height: 292px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--above .multiselect__content-wrapper {
|
||||||
|
top: unset;
|
||||||
|
bottom: calc(100% - 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__select:before {
|
||||||
|
content: url('~@/assets/images/arrow.svg');
|
||||||
|
border: none;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--active .multiselect__select {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select:hover .multiselect__tags {
|
||||||
|
border-color: var(--color-border-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__select:hover:before {
|
||||||
|
content: url('~@/assets/images/arrow-hover.svg');
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--active .multiselect__tags {
|
||||||
|
border-radius: var(--border-radius-medium-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select .multiselect__option .no-results {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--disabled {
|
||||||
|
opacity: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sqliteviz-select.multiselect--disabled .multiselect__select {
|
||||||
|
background: unset;
|
||||||
|
}
|
||||||
@@ -6,6 +6,12 @@
|
|||||||
border: 1px solid var(--color-border-light);
|
border: 1px solid var(--color-border-light);
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.straight .rounded-bg {
|
||||||
|
border-radius: 0;
|
||||||
|
border-width: 0 0 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.header-container {
|
.header-container {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -18,6 +24,19 @@
|
|||||||
border-radius: 5px 5px 0 0;
|
border-radius: 5px 5px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.straight .header-container {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.straight {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.straight .rounded-bg {
|
||||||
|
/* 27 - height of table footer */
|
||||||
|
height: calc(100% - 27px);
|
||||||
|
}
|
||||||
|
|
||||||
@supports (-moz-appearance:none) {
|
@supports (-moz-appearance:none) {
|
||||||
.header-container {
|
.header-container {
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -32,22 +51,25 @@
|
|||||||
}
|
}
|
||||||
.table-container {
|
.table-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
table {
|
table.sqliteviz-table {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-top: -35px;
|
margin-top: -35px;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
}
|
}
|
||||||
thead th, .fixed-header {
|
.sqliteviz-table thead th, .fixed-header {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background-color: var(--color-bg-dark);
|
background-color: var(--color-bg-dark);
|
||||||
color: var(--color-text-light);
|
color: var(--color-text-light);
|
||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
tbody td {
|
.sqliteviz-table tbody td {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
background-color:white;
|
background-color:white;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
@@ -55,18 +77,20 @@ tbody td {
|
|||||||
border-bottom: 1px solid var(--color-border-light);
|
border-bottom: 1px solid var(--color-border-light);
|
||||||
border-right: 1px solid var(--color-border-light);
|
border-right: 1px solid var(--color-border-light);
|
||||||
}
|
}
|
||||||
td, th, .fixed-header {
|
.sqliteviz-table td,
|
||||||
|
.sqliteviz-table th,
|
||||||
|
.fixed-header {
|
||||||
padding: 8px 24px;
|
padding: 8px 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tbody tr td:last-child,
|
.sqliteviz-table tbody tr td:last-child,
|
||||||
thead tr th:last-child,
|
.sqliteviz-table thead tr th:last-child,
|
||||||
.header-container div .fixed-header:last-child {
|
.header-container div .fixed-header:last-child {
|
||||||
border-right: none;
|
border-right: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
td > div.cell-data {
|
.sqliteviz-table td > div.cell-data {
|
||||||
width: -webkit-max-content;
|
width: -webkit-max-content;
|
||||||
width: -moz-max-content;
|
width: -moz-max-content;
|
||||||
width: max-content;
|
width: max-content;
|
||||||
@@ -83,3 +107,9 @@ td > div.cell-data {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: var(--color-text-base);
|
color: var(--color-text-base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sqliteviz-table tbody td[data-isNull="true"],
|
||||||
|
.sqliteviz-table tbody td[data-isBlob="true"] {
|
||||||
|
color: var(--color-text-light-2);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
--color-blue-dark: #0D76BF;
|
--color-blue-dark: #0D76BF;
|
||||||
--color-blue-dark-2: #2A3F5F;
|
--color-blue-dark-2: #2A3F5F;
|
||||||
--color-red: #EF553B;
|
--color-red: #EF553B;
|
||||||
|
--color-red-2: #DE350B;
|
||||||
|
--color-red-light: #FFBDAD;
|
||||||
--color-yellow: #FBEFCB;
|
--color-yellow: #FBEFCB;
|
||||||
|
|
||||||
|
|
||||||
@@ -18,13 +20,16 @@
|
|||||||
--color-bg-light: var(--color-gray-light);
|
--color-bg-light: var(--color-gray-light);
|
||||||
--color-bg-light-2: var(--color-gray-light-2);
|
--color-bg-light-2: var(--color-gray-light-2);
|
||||||
--color-bg-light-3: var(--color-gray-light-5);
|
--color-bg-light-3: var(--color-gray-light-5);
|
||||||
|
--color-bg-light-4: var(--color-gray-light-4);
|
||||||
--color-bg-dark: var(--color-gray-dark);
|
--color-bg-dark: var(--color-gray-dark);
|
||||||
--color-bg-warning: var(--color-yellow);
|
--color-bg-warning: var(--color-yellow);
|
||||||
--color-danger: var(--color-red);
|
--color-bg-danger: var(--color-red-light);
|
||||||
|
--color-danger: var(--color-red-2);
|
||||||
--color-accent: var(--color-blue-medium);
|
--color-accent: var(--color-blue-medium);
|
||||||
--color-accent-shade: var(--color-blue-dark);
|
--color-accent-shade: var(--color-blue-dark);
|
||||||
--color-border-light: var(--color-gray-light-2);
|
--color-border-light: var(--color-gray-light-2);
|
||||||
--color-border: var(--color-gray-light-3);
|
--color-border: var(--color-gray-light-3);
|
||||||
|
--color-border-dark: var(--color-gray-medium);
|
||||||
--color-text-light: var(--color-white);
|
--color-text-light: var(--color-white);
|
||||||
--color-text-light-2: var(--color-gray-medium);
|
--color-text-light-2: var(--color-gray-medium);
|
||||||
--color-text-base: var(--color-gray-dark);
|
--color-text-base: var(--color-gray-dark);
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
import Papa from 'papaparse'
|
|
||||||
|
|
||||||
const hintsByCode = {
|
|
||||||
MissingQuotes: 'Edit your CSV so that the field has a closing quote char.',
|
|
||||||
TooFewFields: 'Add fields or try another delimiter.',
|
|
||||||
TooManyFields: 'Edit your CSV or try another delimiter.'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getResult (source) {
|
|
||||||
const result = {}
|
|
||||||
if (source.meta.fields) {
|
|
||||||
result.columns = source.meta.fields.map(col => col.trim())
|
|
||||||
result.values = source.data.map(row => {
|
|
||||||
const resultRow = []
|
|
||||||
source.meta.fields.forEach(col => {
|
|
||||||
let value = row[col]
|
|
||||||
if (value instanceof Date) {
|
|
||||||
value = value.toISOString()
|
|
||||||
}
|
|
||||||
resultRow.push(value)
|
|
||||||
})
|
|
||||||
|
|
||||||
return resultRow
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
result.values = source.data
|
|
||||||
result.columns = []
|
|
||||||
for (let i = 1; i <= source.data[0].length; i++) {
|
|
||||||
result.columns.push(`col${i}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
},
|
|
||||||
|
|
||||||
parse (file, config = {}) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const defaultConfig = {
|
|
||||||
delimiter: '', // auto-detect
|
|
||||||
newline: '', // auto-detect
|
|
||||||
quoteChar: '"',
|
|
||||||
escapeChar: '"',
|
|
||||||
header: false,
|
|
||||||
transformHeader: undefined,
|
|
||||||
dynamicTyping: true,
|
|
||||||
preview: 0,
|
|
||||||
encoding: 'UTF-8',
|
|
||||||
worker: true,
|
|
||||||
comments: false,
|
|
||||||
step: undefined,
|
|
||||||
complete: results => {
|
|
||||||
const res = {
|
|
||||||
data: this.getResult(results),
|
|
||||||
delimiter: results.meta.delimiter,
|
|
||||||
hasErrors: false
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
error: (error, file) => {
|
|
||||||
reject(error)
|
|
||||||
},
|
|
||||||
download: false,
|
|
||||||
downloadRequestHeaders: undefined,
|
|
||||||
downloadRequestBody: undefined,
|
|
||||||
skipEmptyLines: 'greedy',
|
|
||||||
chunk: undefined,
|
|
||||||
chunkSize: undefined,
|
|
||||||
fastMode: undefined,
|
|
||||||
beforeFirstChunk: undefined,
|
|
||||||
withCredentials: undefined,
|
|
||||||
transform: undefined,
|
|
||||||
delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
|
|
||||||
}
|
|
||||||
|
|
||||||
Papa.parse(file, { ...defaultConfig, ...config })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
:clickToClose="false"
|
:clickToClose="false"
|
||||||
>
|
>
|
||||||
<div class="dialog-header">
|
<div class="dialog-header">
|
||||||
CSV import
|
{{ typeName }} import
|
||||||
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
|
<close-icon @click="cancelImport" :disabled="disableDialog"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-body">
|
<div class="dialog-body">
|
||||||
<text-field
|
<text-field
|
||||||
@@ -18,15 +18,15 @@
|
|||||||
width="484px"
|
width="484px"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
:error-msg="tableNameError"
|
:error-msg="tableNameError"
|
||||||
id="csv-table-name"
|
id="csv-json-table-name"
|
||||||
/>
|
/>
|
||||||
<div class="chars">
|
<div v-if="!isJson && !isNdJson" class="chars">
|
||||||
<delimiter-selector
|
<delimiter-selector
|
||||||
v-model="delimiter"
|
v-model="delimiter"
|
||||||
width="210px"
|
width="210px"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
@input="previewCsv"
|
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Quote char"
|
label="Quote char"
|
||||||
@@ -36,61 +36,66 @@
|
|||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
id="quote-char"
|
id="quote-char"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
<text-field
|
<text-field
|
||||||
label="Escape char"
|
label="Escape char"
|
||||||
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"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
class="char-input"
|
class="char-input"
|
||||||
id="escape-char"
|
id="escape-char"
|
||||||
|
@input="preview"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<check-box
|
<check-box
|
||||||
@click="header = $event"
|
v-if="!isJson && !isNdJson"
|
||||||
:init="true"
|
:init="header"
|
||||||
label="Use first row as column headers"
|
label="Use first row as column headers"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
|
@click="changeHeaderDisplaying"
|
||||||
/>
|
/>
|
||||||
<sql-table
|
<sql-table
|
||||||
v-if="previewData && (previewData.values.length > 0 || previewData.columns.length > 0)"
|
v-if="previewData && previewData.rowCount > 0"
|
||||||
:data-set="previewData"
|
:data-set="previewData"
|
||||||
height="160"
|
|
||||||
class="preview-table"
|
|
||||||
:preview="true"
|
:preview="true"
|
||||||
|
class="preview-table"
|
||||||
/>
|
/>
|
||||||
<div v-else class="no-data">No data</div>
|
<div v-else class="no-data">No data</div>
|
||||||
<logs
|
<logs
|
||||||
class="import-csv-errors"
|
class="import-errors"
|
||||||
:messages="importCsvMessages"
|
:messages="importMessages"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="dialog-buttons-container">
|
<div class="dialog-buttons-container">
|
||||||
<button
|
<button
|
||||||
class="secondary"
|
class="secondary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
@click="cancelCsvImport"
|
@click="cancelImport"
|
||||||
id="csv-cancel"
|
id="import-cancel"
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-show="!importCsvCompleted"
|
v-show="!importCompleted"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog || disableImport"
|
||||||
@click="loadFromCsv(file)"
|
@click="loadToDb(file)"
|
||||||
id="csv-import"
|
id="import-start"
|
||||||
>
|
>
|
||||||
Import
|
Import
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-show="importCsvCompleted"
|
v-show="importCompleted"
|
||||||
class="primary"
|
class="primary"
|
||||||
:disabled="disableDialog"
|
:disabled="disableDialog"
|
||||||
@click="finish"
|
@click="finish"
|
||||||
id="csv-finish"
|
id="import-finish"
|
||||||
>
|
>
|
||||||
Finish
|
Finish
|
||||||
</button>
|
</button>
|
||||||
@@ -99,7 +104,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'
|
||||||
@@ -108,9 +113,10 @@ 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: 'CsvJsonImport',
|
||||||
components: {
|
components: {
|
||||||
CloseIcon,
|
CloseIcon,
|
||||||
TextField,
|
TextField,
|
||||||
@@ -119,33 +125,50 @@ export default {
|
|||||||
SqlTable,
|
SqlTable,
|
||||||
Logs
|
Logs
|
||||||
},
|
},
|
||||||
props: ['file', 'db', 'dialogName'],
|
props: {
|
||||||
|
file: File,
|
||||||
|
db: Object,
|
||||||
|
dialogName: String
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
disableDialog: false,
|
disableDialog: false,
|
||||||
|
disableImport: false,
|
||||||
tableName: '',
|
tableName: '',
|
||||||
delimiter: '',
|
delimiter: '',
|
||||||
quoteChar: '"',
|
quoteChar: '"',
|
||||||
escapeChar: '"',
|
escapeChar: '"',
|
||||||
header: true,
|
header: true,
|
||||||
importCsvCompleted: false,
|
importCompleted: false,
|
||||||
importCsvMessages: [],
|
importMessages: [],
|
||||||
previewData: null,
|
previewData: null,
|
||||||
addedTable: null,
|
addedTable: null,
|
||||||
tableNameError: ''
|
tableNameError: ''
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
isJson () {
|
||||||
|
return fIo.isJSON(this.file)
|
||||||
|
},
|
||||||
|
isNdJson () {
|
||||||
|
return fIo.isNDJSON(this.file)
|
||||||
|
},
|
||||||
|
typeName () {
|
||||||
|
return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
quoteChar () {
|
isJson () {
|
||||||
this.previewCsv()
|
if (this.isJson) {
|
||||||
|
this.delimiter = '\u001E'
|
||||||
|
this.header = false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
isNdJson () {
|
||||||
escapeChar () {
|
if (this.isNdJson) {
|
||||||
this.previewCsv()
|
this.delimiter = '\u001E'
|
||||||
},
|
this.header = false
|
||||||
|
}
|
||||||
header () {
|
|
||||||
this.previewCsv()
|
|
||||||
},
|
},
|
||||||
tableName: time.debounce(function () {
|
tableName: time.debounce(function () {
|
||||||
this.tableNameError = ''
|
this.tableNameError = ''
|
||||||
@@ -159,7 +182,11 @@ export default {
|
|||||||
}, 400)
|
}, 400)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
changeHeaderDisplaying (e) {
|
||||||
|
this.header = e
|
||||||
|
this.preview()
|
||||||
|
},
|
||||||
|
cancelImport () {
|
||||||
if (!this.disableDialog) {
|
if (!this.disableDialog) {
|
||||||
if (this.addedTable) {
|
if (this.addedTable) {
|
||||||
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
this.db.execute(`DROP TABLE "${this.addedTable}"`)
|
||||||
@@ -170,14 +197,15 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
reset () {
|
reset () {
|
||||||
this.header = true
|
this.header = !this.isJson && !this.isNdJson
|
||||||
this.quoteChar = '"'
|
this.quoteChar = '"'
|
||||||
this.escapeChar = '"'
|
this.escapeChar = '"'
|
||||||
this.delimiter = ''
|
this.delimiter = !this.isJson && !this.isNdJson ? '' : '\u001E'
|
||||||
this.tableName = ''
|
this.tableName = ''
|
||||||
this.disableDialog = false
|
this.disableDialog = false
|
||||||
this.importCsvCompleted = false
|
this.disableImport = false
|
||||||
this.importCsvMessages = []
|
this.importCompleted = false
|
||||||
|
this.importMessages = []
|
||||||
this.previewData = null
|
this.previewData = null
|
||||||
this.addedTable = null
|
this.addedTable = null
|
||||||
this.tableNameError = ''
|
this.tableNameError = ''
|
||||||
@@ -186,39 +214,69 @@ export default {
|
|||||||
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
|
||||||
this.$modal.show(this.dialogName)
|
this.$modal.show(this.dialogName)
|
||||||
},
|
},
|
||||||
async previewCsv () {
|
async preview () {
|
||||||
this.importCsvCompleted = false
|
this.disableImport = false
|
||||||
|
if (!this.file) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.importCompleted = false
|
||||||
const config = {
|
const config = {
|
||||||
preview: 3,
|
preview: 3,
|
||||||
quoteChar: this.quoteChar || '"',
|
quoteChar: this.quoteChar || '"',
|
||||||
escapeChar: this.escapeChar,
|
escapeChar: this.escapeChar,
|
||||||
header: this.header,
|
header: this.header,
|
||||||
delimiter: this.delimiter
|
delimiter: this.delimiter,
|
||||||
|
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const start = new Date()
|
const start = new Date()
|
||||||
const parseResult = await csv.parse(this.file, config)
|
const parseResult = this.isJson
|
||||||
|
? await this.getJsonParseResult(this.file)
|
||||||
|
: await csv.parse(this.file, config)
|
||||||
const end = new Date()
|
const end = new Date()
|
||||||
this.previewData = parseResult.data
|
this.previewData = parseResult.data
|
||||||
|
this.previewData.rowCount = parseResult.rowCount
|
||||||
this.delimiter = parseResult.delimiter
|
this.delimiter = parseResult.delimiter
|
||||||
|
|
||||||
// In parseResult.messages we can get parse errors
|
// In parseResult.messages we can get parse errors
|
||||||
this.importCsvMessages = parseResult.messages || []
|
this.importMessages = parseResult.messages || []
|
||||||
|
|
||||||
|
if (this.previewData.rowCount === 0) {
|
||||||
|
this.disableImport = true
|
||||||
|
this.importMessages.push({
|
||||||
|
type: 'info',
|
||||||
|
message: 'No rows to import.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
if (!parseResult.hasErrors) {
|
||||||
this.importCsvMessages.push({
|
this.importMessages.push({
|
||||||
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
|
||||||
type: 'success'
|
type: 'success'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.importCsvMessages = [{
|
console.error(err)
|
||||||
|
this.importMessages = [{
|
||||||
message: err,
|
message: err,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async loadFromCsv (file) {
|
async getJsonParseResult (file) {
|
||||||
|
const jsonContent = await fIo.getFileContent(file)
|
||||||
|
const isEmpty = !jsonContent.trim()
|
||||||
|
return {
|
||||||
|
data: {
|
||||||
|
columns: ['doc'],
|
||||||
|
values: { doc: !isEmpty ? [jsonContent] : [] }
|
||||||
|
},
|
||||||
|
hasErrors: false,
|
||||||
|
messages: [],
|
||||||
|
rowCount: +(!isEmpty)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadToDb (file) {
|
||||||
if (!this.tableName) {
|
if (!this.tableName) {
|
||||||
this.tableNameError = "Table name can't be empty"
|
this.tableNameError = "Table name can't be empty"
|
||||||
return
|
return
|
||||||
@@ -229,17 +287,18 @@ export default {
|
|||||||
quoteChar: this.quoteChar || '"',
|
quoteChar: this.quoteChar || '"',
|
||||||
escapeChar: this.escapeChar,
|
escapeChar: this.escapeChar,
|
||||||
header: this.header,
|
header: this.header,
|
||||||
delimiter: this.delimiter
|
delimiter: this.delimiter,
|
||||||
|
columns: !this.isJson && !this.isNdJson ? null : ['doc']
|
||||||
}
|
}
|
||||||
const parseCsvMsg = {
|
const parsingMsg = {
|
||||||
message: 'Parsing CSV...',
|
message: `Parsing ${this.typeName}...`,
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}
|
}
|
||||||
this.importCsvMessages.push(parseCsvMsg)
|
this.importMessages.push(parsingMsg)
|
||||||
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
|
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
|
||||||
|
|
||||||
const importMsg = {
|
const importMsg = {
|
||||||
message: 'Importing CSV into a SQLite database...',
|
message: `Importing ${this.typeName} into a SQLite database...`,
|
||||||
type: 'info'
|
type: 'info'
|
||||||
}
|
}
|
||||||
let importLoadingIndicator = null
|
let importLoadingIndicator = null
|
||||||
@@ -251,27 +310,30 @@ export default {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
let start = new Date()
|
let start = new Date()
|
||||||
const parseResult = await csv.parse(this.file, config)
|
const parseResult = this.isJson
|
||||||
|
? await this.getJsonParseResult(file)
|
||||||
|
: await csv.parse(this.file, config)
|
||||||
|
|
||||||
let end = new Date()
|
let end = new Date()
|
||||||
|
|
||||||
if (!parseResult.hasErrors) {
|
if (!parseResult.hasErrors) {
|
||||||
const rowCount = parseResult.data.values.length
|
const rowCount = parseResult.rowCount
|
||||||
let period = time.getPeriod(start, end)
|
let period = time.getPeriod(start, end)
|
||||||
parseCsvMsg.type = 'success'
|
parsingMsg.type = 'success'
|
||||||
|
|
||||||
if (parseResult.messages.length > 0) {
|
if (parseResult.messages.length > 0) {
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
|
parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
|
||||||
} else {
|
} else {
|
||||||
// Inform about csv parsing success
|
// Inform about parsing success
|
||||||
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
parsingMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loading indicator for csv parsing is not needed anymore
|
// Loading indicator for parsing is not needed anymore
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
clearTimeout(parsingLoadingIndicator)
|
||||||
|
|
||||||
// Add info about import start
|
// Add info about import start
|
||||||
this.importCsvMessages.push(importMsg)
|
this.importMessages.push(importMsg)
|
||||||
|
|
||||||
// Show import progress after 1 second
|
// Show import progress after 1 second
|
||||||
importLoadingIndicator = setTimeout(() => {
|
importLoadingIndicator = setTimeout(() => {
|
||||||
@@ -286,51 +348,108 @@ export default {
|
|||||||
this.addedTable = this.tableName
|
this.addedTable = this.tableName
|
||||||
// Inform about import success
|
// Inform about import success
|
||||||
period = time.getPeriod(start, end)
|
period = time.getPeriod(start, end)
|
||||||
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
|
importMsg.message = `Importing ${this.typeName} ` +
|
||||||
|
`into a SQLite database is completed in ${period}.`
|
||||||
importMsg.type = 'success'
|
importMsg.type = 'success'
|
||||||
|
|
||||||
// Loading indicator for import is not needed anymore
|
// Loading indicator for import is not needed anymore
|
||||||
clearTimeout(importLoadingIndicator)
|
clearTimeout(importLoadingIndicator)
|
||||||
|
|
||||||
this.importCsvCompleted = true
|
this.importCompleted = true
|
||||||
} else {
|
} else {
|
||||||
parseCsvMsg.message = 'Parsing ended with errors.'
|
parsingMsg.message = 'Parsing ended with errors.'
|
||||||
parseCsvMsg.type = 'info'
|
parsingMsg.type = 'info'
|
||||||
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
|
this.importMessages = this.importMessages.concat(parseResult.messages)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (parseCsvMsg.type === 'loading') {
|
console.error(err)
|
||||||
parseCsvMsg.type = 'info'
|
if (parsingMsg.type === 'loading') {
|
||||||
|
parsingMsg.type = 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (importMsg.type === 'loading') {
|
if (importMsg.type === 'loading') {
|
||||||
importMsg.type = 'info'
|
importMsg.type = 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
this.importCsvMessages.push({
|
this.importMessages.push({
|
||||||
message: err,
|
message: err,
|
||||||
type: 'error'
|
type: 'error'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
clearTimeout(parseCsvLoadingIndicator)
|
clearTimeout(parsingLoadingIndicator)
|
||||||
clearTimeout(importLoadingIndicator)
|
clearTimeout(importLoadingIndicator)
|
||||||
this.db.deleteProgressCounter(progressCounterId)
|
this.db.deleteProgressCounter(progressCounterId)
|
||||||
this.disableDialog = false
|
this.disableDialog = false
|
||||||
},
|
},
|
||||||
async finish () {
|
async finish () {
|
||||||
this.$modal.hide(this.dialogName)
|
this.$modal.hide(this.dialogName)
|
||||||
const stmt = [
|
const stmt = this.getQueryExample()
|
||||||
'/*',
|
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
||||||
|
this.$store.commit('setCurrentTabId', tabId)
|
||||||
|
this.importCompleted = false
|
||||||
|
this.$emit('finish')
|
||||||
|
events.send('inquiry.create', null, { auto: true })
|
||||||
|
},
|
||||||
|
getQueryExample () {
|
||||||
|
return this.isNdJson ? this.getNdJsonQueryExample()
|
||||||
|
: this.isJson ? this.getJsonQueryExample()
|
||||||
|
: [
|
||||||
|
'/*',
|
||||||
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
` * Your CSV file has been imported into ${this.addedTable} table.`,
|
||||||
' * You can run this SQL query to make all CSV records available for charting.',
|
' * You can run this SQL query to make all CSV records available for charting.',
|
||||||
' */',
|
' */',
|
||||||
`SELECT * FROM "${this.addedTable}"`
|
`SELECT * FROM "${this.addedTable}"`
|
||||||
].join('\n')
|
].join('\n')
|
||||||
const tabId = await this.$store.dispatch('addTab', { query: stmt })
|
},
|
||||||
this.$store.commit('setCurrentTabId', tabId)
|
getNdJsonQueryExample () {
|
||||||
this.importCsvCompleted = false
|
try {
|
||||||
this.$emit('finish')
|
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||||
|
const firstKey = Object.keys(firstRowJson)[0]
|
||||||
|
return [
|
||||||
|
'/*',
|
||||||
|
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||||
|
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||||
|
'and make them available for charting.',
|
||||||
|
' */',
|
||||||
|
`SELECT doc->>'${firstKey}'`,
|
||||||
|
`FROM "${this.addedTable}"`
|
||||||
|
].join('\n')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return [
|
||||||
|
'/*',
|
||||||
|
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||||
|
' */',
|
||||||
|
'SELECT *',
|
||||||
|
`FROM "${this.addedTable}"`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getJsonQueryExample () {
|
||||||
|
try {
|
||||||
|
const firstRowJson = JSON.parse(this.previewData.values.doc[0])
|
||||||
|
const firstKey = Object.keys(firstRowJson)[0]
|
||||||
|
return [
|
||||||
|
'/*',
|
||||||
|
` * Your JSON file has been imported into ${this.addedTable} table.`,
|
||||||
|
` * Run this SQL query to get values of property ${firstKey} ` +
|
||||||
|
'and make them available for charting.',
|
||||||
|
' */',
|
||||||
|
'SELECT *',
|
||||||
|
`FROM "${this.addedTable}"`,
|
||||||
|
`JOIN json_each(doc, '$.${firstKey}')`
|
||||||
|
].join('\n')
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
return [
|
||||||
|
'/*',
|
||||||
|
` * Your NDJSON file has been imported into ${this.addedTable} table.`,
|
||||||
|
' */',
|
||||||
|
'SELECT *',
|
||||||
|
`FROM "${this.addedTable}"`
|
||||||
|
].join('\n')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -341,10 +460,14 @@ export default {
|
|||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#csv-json-table-name {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.chars {
|
.chars {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
margin: 24px 0 20px;
|
margin: 0 0 20px;
|
||||||
}
|
}
|
||||||
.char-input {
|
.char-input {
|
||||||
margin-right: 44px;
|
margin-right: 44px;
|
||||||
@@ -353,7 +476,7 @@ export default {
|
|||||||
margin-top: 18px;
|
margin-top: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.import-csv-errors {
|
.import-errors {
|
||||||
height: 136px;
|
height: 136px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="db-uploader-container" :style="{ width }">
|
<div class="db-uploader-container" :style="{ width }">
|
||||||
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
|
<change-db-icon v-if="type === 'small'" @click="browse"/>
|
||||||
<div v-if="type === 'illustrated'" class="drop-area-container">
|
<div v-if="type === 'illustrated'" class="drop-area-container">
|
||||||
<div
|
<div
|
||||||
class="drop-area"
|
class="drop-area"
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
@click="browse"
|
@click="browse"
|
||||||
>
|
>
|
||||||
<div class="text">
|
<div class="text">
|
||||||
Drop the database or CSV file here or click to choose a file from your computer.
|
Drop the database, CSV, JSON or NDJSON file here
|
||||||
|
or click to choose a file from your computer.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -41,13 +42,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="error" class="error"></div>
|
<div id="error" class="error"></div>
|
||||||
|
|
||||||
<!--Parse csv dialog -->
|
<!--Parse csv or json dialog -->
|
||||||
<csv-import
|
<csv-json-import
|
||||||
ref="addCsv"
|
ref="addCsvJson"
|
||||||
:file="file"
|
:file="file"
|
||||||
:db="newDb"
|
:db="newDb"
|
||||||
dialog-name="importFromCsv"
|
dialog-name="importFromCsvJson"
|
||||||
@cancel="cancelCsvImport"
|
@cancel="cancelImport"
|
||||||
@finish="finish"
|
@finish="finish"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +58,8 @@
|
|||||||
import fIo from '@/lib/utils/fileIo'
|
import fIo from '@/lib/utils/fileIo'
|
||||||
import ChangeDbIcon from '@/components/svg/changeDb'
|
import ChangeDbIcon from '@/components/svg/changeDb'
|
||||||
import database from '@/lib/database'
|
import database from '@/lib/database'
|
||||||
import CsvImport from '@/components/CsvImport'
|
import CsvJsonImport from '@/components/CsvJsonImport'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DbUploader',
|
name: 'DbUploader',
|
||||||
@@ -78,7 +80,7 @@ export default {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
ChangeDbIcon,
|
ChangeDbIcon,
|
||||||
CsvImport
|
CsvJsonImport
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
@@ -101,7 +103,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
cancelCsvImport () {
|
cancelImport () {
|
||||||
if (this.newDb) {
|
if (this.newDb) {
|
||||||
this.newDb.shutDown()
|
this.newDb.shutDown()
|
||||||
this.newDb = null
|
this.newDb = null
|
||||||
@@ -110,8 +112,8 @@ export default {
|
|||||||
|
|
||||||
async finish () {
|
async finish () {
|
||||||
this.$store.commit('setDb', this.newDb)
|
this.$store.commit('setDb', this.newDb)
|
||||||
if (this.$route.path !== '/editor') {
|
if (this.$route.path !== '/workspace') {
|
||||||
this.$router.push('/editor')
|
this.$router.push('/workspace')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -127,16 +129,22 @@ export default {
|
|||||||
if (fIo.isDatabase(file)) {
|
if (fIo.isDatabase(file)) {
|
||||||
this.loadDb(file)
|
this.loadDb(file)
|
||||||
} else {
|
} else {
|
||||||
|
const isJson = fIo.isJSON(file) || fIo.isNDJSON(file)
|
||||||
|
events.send('database.import', file.size, {
|
||||||
|
from: isJson ? 'json' : 'csv',
|
||||||
|
new_db: true
|
||||||
|
})
|
||||||
|
|
||||||
this.file = file
|
this.file = file
|
||||||
await this.$nextTick()
|
await this.$nextTick()
|
||||||
const csvImport = this.$refs.addCsv
|
const csvJsonImportModal = this.$refs.addCsvJson
|
||||||
csvImport.reset()
|
csvJsonImportModal.reset()
|
||||||
return Promise.all([csvImport.previewCsv(), this.animationPromise])
|
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
|
||||||
.then(csvImport.open)
|
.then(csvJsonImportModal.open)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
browse () {
|
browse () {
|
||||||
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
|
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
|
||||||
.then(this.checkFile)
|
.then(this.checkFile)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
112
src/components/IconButton.vue
Normal file
112
src/components/IconButton.vue
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
:class="['icon-btn', { active }]"
|
||||||
|
:disabled="disabled"
|
||||||
|
@click="onClick"
|
||||||
|
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||||
|
@mouseleave="hideTooltip"
|
||||||
|
>
|
||||||
|
<div class="icon"><slot /></div>
|
||||||
|
<div v-show="loading" class="icon-in-progress">
|
||||||
|
<loading-indicator />
|
||||||
|
</div>
|
||||||
|
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
|
{{ tooltip }}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import tooltipMixin from '@/tooltipMixin'
|
||||||
|
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SideBarButton',
|
||||||
|
props: ['active', 'disabled', 'tooltip', 'tooltipPosition', 'loading'],
|
||||||
|
components: { LoadingIndicator },
|
||||||
|
mixins: [tooltipMixin],
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.icon-btn {
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.icon-btn:hover {
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
border-radius: var(--border-radius-medium-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover .icon >>> path,
|
||||||
|
.icon-btn.active .icon >>> path,
|
||||||
|
.icon-btn:hover .icon >>> circle,
|
||||||
|
.icon-btn.active .icon >>> circle {
|
||||||
|
fill: var(--color-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:disabled .icon >>> path,
|
||||||
|
.icon-btn:disabled .icon >>> circle {
|
||||||
|
fill: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:disabled {
|
||||||
|
cursor: default;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled.icon-btn:hover .icon >>> path {
|
||||||
|
fill: var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-in-progress {
|
||||||
|
position: absolute;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--color-bg-light);
|
||||||
|
will-change: opacity;
|
||||||
|
/*
|
||||||
|
We need to show loader in 1 sec after starting query execution. We can't do that with
|
||||||
|
setTimeout because the main thread can be busy by getting a result set from the web worker.
|
||||||
|
But we can use CSS animation for opacity. Opacity triggers changes only in the Composite Layer
|
||||||
|
stage in rendering waterfall. Hence it can be processed only with Compositor Thread while
|
||||||
|
the Main Thread processes a result set.
|
||||||
|
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
|
||||||
|
*/
|
||||||
|
animation: show-loader 1s linear 0s 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes show-loader {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
99% {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
83
src/components/LoadingDialog.vue
Normal file
83
src/components/LoadingDialog.vue
Normal 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>
|
||||||
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@@ -49,6 +52,7 @@ export default {
|
|||||||
.paginator-continer {
|
.paginator-continer {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
line-height: 10px;
|
||||||
}
|
}
|
||||||
>>> .paginator-page-link {
|
>>> .paginator-page-link {
|
||||||
padding: 2px 3px;
|
padding: 2px 3px;
|
||||||
|
|||||||
@@ -17,20 +17,35 @@
|
|||||||
class="table-container"
|
class="table-container"
|
||||||
ref="table-container"
|
ref="table-container"
|
||||||
@scroll="onScrollTable"
|
@scroll="onScrollTable"
|
||||||
:style="{maxHeight: `${height}px`}"
|
|
||||||
>
|
>
|
||||||
<table ref="table">
|
<table
|
||||||
|
ref="table"
|
||||||
|
class="sqliteviz-table"
|
||||||
|
tabindex="0"
|
||||||
|
@keydown="onTableKeydown"
|
||||||
|
>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="(th,index) in dataSet.columns" :key="index" ref="th">
|
<th v-for="(th, index) in columns" :key="index" ref="th">
|
||||||
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
<div class="cell-data" :style="cellStyle">{{ th }}</div>
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="(row,index) in currentPageData" :key="index">
|
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||||
<td v-for="(value, valIndex) in row" :key="valIndex">
|
<td
|
||||||
<div class="cell-data" :style="cellStyle">{{ value }}</div>
|
v-for="(col, colIndex) in columns"
|
||||||
|
:data-col="colIndex"
|
||||||
|
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
|
||||||
|
:data-isNull="isNull(getCellValue(col, rowIndex))"
|
||||||
|
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
|
||||||
|
:key="colIndex"
|
||||||
|
:aria-selected="false"
|
||||||
|
@click="onCellClick"
|
||||||
|
>
|
||||||
|
<div class="cell-data" :style="cellStyle">
|
||||||
|
{{ getCellText(col, rowIndex) }}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -39,11 +54,15 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="table-footer">
|
<div class="table-footer">
|
||||||
<div class="table-footer-count">
|
<div class="table-footer-count">
|
||||||
{{ dataSet.values.length}} {{dataSet.values.length === 1 ? 'row' : 'rows'}} retrieved
|
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved
|
||||||
<span v-if="preview">for preview</span>
|
<span v-if="preview">for preview</span>
|
||||||
<span v-if="time">in {{ time }}</span>
|
<span v-if="time">in {{ time }}</span>
|
||||||
</div>
|
</div>
|
||||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
<pager
|
||||||
|
v-show="pageCount > 1"
|
||||||
|
:page-count="pageCount"
|
||||||
|
v-model="currentPage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -54,33 +73,90 @@ import Pager from './Pager'
|
|||||||
export default {
|
export default {
|
||||||
name: 'SqlTable',
|
name: 'SqlTable',
|
||||||
components: { Pager },
|
components: { Pager },
|
||||||
props: ['dataSet', 'time', 'height', 'preview'],
|
props: {
|
||||||
|
dataSet: Object,
|
||||||
|
time: [String, Number],
|
||||||
|
pageSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 20
|
||||||
|
},
|
||||||
|
page: {
|
||||||
|
type: Number,
|
||||||
|
default: 1
|
||||||
|
},
|
||||||
|
preview: Boolean,
|
||||||
|
selectedCellCoordinates: Object
|
||||||
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
header: null,
|
header: null,
|
||||||
tableWidth: null,
|
tableWidth: null,
|
||||||
currentPage: 1,
|
currentPage: this.page,
|
||||||
resizeObserver: null
|
resizeObserver: null,
|
||||||
|
selectedCellElement: null
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
columns () {
|
||||||
|
return this.dataSet.columns
|
||||||
|
},
|
||||||
|
rowCount () {
|
||||||
|
return this.dataSet.values[this.columns[0]].length
|
||||||
|
},
|
||||||
cellStyle () {
|
cellStyle () {
|
||||||
const eq = this.tableWidth / this.dataSet.columns.length
|
const eq = this.tableWidth / this.columns.length
|
||||||
|
|
||||||
return { maxWidth: `${Math.max(eq, 100)}px` }
|
return { maxWidth: `${Math.max(eq, 100)}px` }
|
||||||
},
|
},
|
||||||
pageSize () {
|
|
||||||
return Math.max(Math.floor(this.height / 40), 20)
|
|
||||||
},
|
|
||||||
pageCount () {
|
pageCount () {
|
||||||
return Math.ceil(this.dataSet.values.length / this.pageSize)
|
return Math.ceil(this.rowCount / this.pageSize)
|
||||||
},
|
},
|
||||||
currentPageData () {
|
currentPageData () {
|
||||||
const start = (this.currentPage - 1) * this.pageSize
|
const start = (this.currentPage - 1) * this.pageSize
|
||||||
return this.dataSet.values.slice(start, start + this.pageSize)
|
let end = start + this.pageSize
|
||||||
|
if (end > this.rowCount - 1) {
|
||||||
|
end = this.rowCount - 1
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
count: end - start + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted () {
|
||||||
|
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
||||||
|
this.resizeObserver.observe(this.$refs.table)
|
||||||
|
this.calculateHeadersWidth()
|
||||||
|
|
||||||
|
if (this.selectedCellCoordinates) {
|
||||||
|
const { row, col } = this.selectedCellCoordinates
|
||||||
|
const cell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
|
||||||
|
if (cell) {
|
||||||
|
this.selectCell(cell)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isBlob (value) {
|
||||||
|
return value && ArrayBuffer.isView(value)
|
||||||
|
},
|
||||||
|
isNull (value) {
|
||||||
|
return value === null
|
||||||
|
},
|
||||||
|
getCellValue (col, rowIndex) {
|
||||||
|
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
|
||||||
|
},
|
||||||
|
getCellText (col, rowIndex) {
|
||||||
|
const value = this.getCellValue(col, rowIndex)
|
||||||
|
if (this.isNull(value)) {
|
||||||
|
return 'NULL'
|
||||||
|
}
|
||||||
|
if (this.isBlob(value)) {
|
||||||
|
return 'BLOB'
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
},
|
||||||
calculateHeadersWidth () {
|
calculateHeadersWidth () {
|
||||||
this.tableWidth = this.$refs['table-container'].offsetWidth
|
this.tableWidth = this.$refs['table-container'].offsetWidth
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
@@ -91,18 +167,95 @@ export default {
|
|||||||
},
|
},
|
||||||
onScrollTable () {
|
onScrollTable () {
|
||||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||||
|
},
|
||||||
|
onTableKeydown (e) {
|
||||||
|
const keyCodeMap = {
|
||||||
|
37: 'left',
|
||||||
|
39: 'right',
|
||||||
|
38: 'up',
|
||||||
|
40: 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.selectedCellElement ||
|
||||||
|
!Object.keys(keyCodeMap).includes(e.keyCode.toString())
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
|
||||||
|
},
|
||||||
|
onCellClick (e) {
|
||||||
|
this.selectCell(e.target.closest('td'), false)
|
||||||
|
},
|
||||||
|
selectCell (cell, scrollTo = true) {
|
||||||
|
if (!cell) {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else if (!cell.ariaSelected || cell.ariaSelected === 'false') {
|
||||||
|
if (this.selectedCellElement) {
|
||||||
|
this.selectedCellElement.ariaSelected = 'false'
|
||||||
|
}
|
||||||
|
cell.ariaSelected = 'true'
|
||||||
|
this.selectedCellElement = cell
|
||||||
|
} else {
|
||||||
|
cell.ariaSelected = 'false'
|
||||||
|
this.selectedCellElement = null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.selectedCellElement && scrollTo) {
|
||||||
|
this.selectedCellElement.scrollIntoView()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('updateSelectedCell', this.selectedCellElement)
|
||||||
|
},
|
||||||
|
moveFocusInTable (initialCell, direction) {
|
||||||
|
const currentRowIndex = +initialCell.dataset.row
|
||||||
|
const currentColIndex = +initialCell.dataset.col
|
||||||
|
let newRowIndex, newColIndex
|
||||||
|
|
||||||
|
if (direction === 'right') {
|
||||||
|
if (currentColIndex === this.columns.length - 1) {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = 0
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex + 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'left') {
|
||||||
|
if (currentColIndex === 0) {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = this.columns.length - 1
|
||||||
|
} else {
|
||||||
|
newRowIndex = currentRowIndex
|
||||||
|
newColIndex = currentColIndex - 1
|
||||||
|
}
|
||||||
|
} else if (direction === 'up') {
|
||||||
|
newRowIndex = currentRowIndex - 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
} else if (direction === 'down') {
|
||||||
|
newRowIndex = currentRowIndex + 1
|
||||||
|
newColIndex = currentColIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCell = this.$refs.table
|
||||||
|
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
|
||||||
|
if (newCell) {
|
||||||
|
this.selectCell(newCell)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
|
||||||
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
|
|
||||||
this.resizeObserver.observe(this.$refs.table)
|
|
||||||
this.calculateHeadersWidth()
|
|
||||||
},
|
|
||||||
beforeDestroy () {
|
beforeDestroy () {
|
||||||
this.resizeObserver.unobserve(this.$refs.table)
|
this.resizeObserver.unobserve(this.$refs.table)
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
currentPageData: 'calculateHeadersWidth',
|
currentPageData () {
|
||||||
|
this.calculateHeadersWidth()
|
||||||
|
this.selectCell(null)
|
||||||
|
},
|
||||||
dataSet () {
|
dataSet () {
|
||||||
this.currentPage = 1
|
this.currentPage = 1
|
||||||
}
|
}
|
||||||
@@ -111,4 +264,13 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
table.sqliteviz-table:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td:hover {
|
||||||
|
background-color: var(--color-bg-light-3);
|
||||||
|
}
|
||||||
|
.sqliteviz-table tbody td[aria-selected="true"] {
|
||||||
|
box-shadow:inset 0 0 0 1px var(--color-accent);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -6,22 +6,23 @@
|
|||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 18 18"
|
viewBox="0 0 18 18"
|
||||||
fill="none"
|
fill="none"
|
||||||
@click.stop="$emit('click')"
|
@click.stop="onClick"
|
||||||
@mouseover="showTooltip"
|
@mouseenter="showTooltip"
|
||||||
@mouseout="hideTooltip"
|
@mouseleave="hideTooltip"
|
||||||
>
|
>
|
||||||
<g clip-path="url(#clip0)">
|
<g clip-path="url(#clip0)">
|
||||||
<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"
|
||||||
/>
|
/>
|
||||||
@@ -32,8 +33,8 @@
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
Add new table from CSV
|
Add new table from CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
@@ -44,7 +45,13 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'AddTableIcon',
|
name: 'AddTableIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
props: ['tooltip']
|
props: ['tooltip'],
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
20
src/components/svg/arrow.vue
Normal file
20
src/components/svg/arrow.vue
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="28"
|
||||||
|
height="27"
|
||||||
|
viewBox="0 0 28 27"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.9475 8.33625L12.7838 13.5L17.9475 18.6638L16.35 20.25L9.60001
|
||||||
|
13.5L16.35 6.75L17.9475 8.33625Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,17 +6,23 @@
|
|||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 18 18"
|
viewBox="0 0 18 18"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click.stop="onClick"
|
||||||
@mouseover="showTooltip"
|
@mouseenter="showTooltip"
|
||||||
@mouseout="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>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
Load another database or CSV
|
Load another database, CSV, JSON or NDJSON
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -26,7 +32,13 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'changeDbIcon',
|
name: 'changeDbIcon',
|
||||||
mixins: [tooltipMixin]
|
mixins: [tooltipMixin],
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
53
src/components/svg/chart.vue
Normal file
53
src/components/svg/chart.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M8.41943 16V10H10.4194V16H8.41943Z"
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ChartIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|||||||
33
src/components/svg/clipboard.vue
Normal file
33
src/components/svg/clipboard.vue
Normal 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>
|
||||||
@@ -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>
|
||||||
|
|||||||
38
src/components/svg/dataView.vue
Normal file
38
src/components/svg/dataView.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DataViewIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
26
src/components/svg/edgeArrow.vue
Normal file
26
src/components/svg/edgeArrow.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="27"
|
||||||
|
height="27"
|
||||||
|
viewBox="0 0 27 27"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M17.3474 8.33625L12.1837 13.5L17.3474 18.6638L15.7499 20.25L8.99991
|
||||||
|
13.5L15.7499 6.75L17.3474 8.33625Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M7.19995 19.8L7.19995 7.20001H9.19995V19.8H7.19995Z"
|
||||||
|
fill="#506784"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -6,17 +6,18 @@
|
|||||||
height="18"
|
height="18"
|
||||||
viewBox="0 0 18 18"
|
viewBox="0 0 18 18"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click.stop="onClick"
|
||||||
@click.stop="$emit('click')"
|
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||||
@mouseover="showTooltip"
|
@mouseleave="hideTooltip"
|
||||||
@mouseout="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>
|
||||||
<span class="icon-tooltip" :style="tooltipStyle">
|
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||||
{{ tooltip }}
|
{{ tooltip }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -28,7 +29,13 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'ExportIcon',
|
name: 'ExportIcon',
|
||||||
mixins: [tooltipMixin],
|
mixins: [tooltipMixin],
|
||||||
props: ['tooltip']
|
props: ['tooltip', 'tooltipPosition'],
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
62
src/components/svg/exportToCsv.vue
Normal file
62
src/components/svg/exportToCsv.vue
Normal 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>
|
||||||
61
src/components/svg/exportToSvg.vue
Normal file
61
src/components/svg/exportToSvg.vue
Normal 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>
|
||||||
@@ -6,14 +6,34 @@
|
|||||||
height="20"
|
height="20"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
@click.stop="onClick"
|
||||||
@mouseover="showTooltip"
|
@mouseenter="showTooltip"
|
||||||
@mouseout="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 }">
|
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip">
|
||||||
{{ hint }}
|
{{ hint }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +45,13 @@ import tooltipMixin from '@/tooltipMixin'
|
|||||||
export default {
|
export default {
|
||||||
name: 'HintIcon',
|
name: 'HintIcon',
|
||||||
props: ['hint', 'maxWidth'],
|
props: ['hint', 'maxWidth'],
|
||||||
mixins: [tooltipMixin]
|
mixins: [tooltipMixin],
|
||||||
|
methods: {
|
||||||
|
onClick () {
|
||||||
|
this.hideTooltip()
|
||||||
|
this.$emit('click')
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
49
src/components/svg/html.vue
Normal file
49
src/components/svg/html.vue
Normal 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>
|
||||||
28
src/components/svg/pivot.vue
Normal file
28
src/components/svg/pivot.vue
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
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 d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6"/>
|
||||||
|
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6"/>
|
||||||
|
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'PivotIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
41
src/components/svg/png.vue
Normal file
41
src/components/svg/png.vue
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'PngIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
src/components/svg/row.vue
Normal file
53
src/components/svg/row.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 19 19"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0_2130_5292)">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
|
||||||
|
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
|
||||||
|
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
|
||||||
|
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
|
||||||
|
6.30371L16.853 6.30371C17.6815 6.30371 18.353 6.97528 18.353
|
||||||
|
7.80371L18.353 11.3794C18.353 12.2078 17.6815 12.8794 16.853
|
||||||
|
12.8794L15.353 12.8794L15.353 14.3111C15.353 15.0153 14.7603 15.5916
|
||||||
|
14.0358 15.5916L4.67027 15.5916C3.94579 15.5916 3.35303 15.0153 3.35303
|
||||||
|
14.3111L3.35303 12.8794L1.85303 12.8794C1.0246 12.8794 0.353027 12.2078
|
||||||
|
0.353027 11.3794L0.353027 7.80371C0.353027 6.97528 1.0246 6.30371
|
||||||
|
1.85303 6.30371L3.35303 6.30371L3.35303 4.87201C3.35303 4.16349 3.94139
|
||||||
|
3.59155 4.67027 3.59155L14.0358 3.59155C14.7604 3.59155 15.353 4.16117
|
||||||
|
15.353 4.87201L15.353 6.30371ZM14.0315 6.30371L14.0315 4.87086L11.887
|
||||||
|
4.87086L11.887 6.30371L12.8428 6.30371L14.0315 6.30371ZM10.387
|
||||||
|
6.30371L10.387 4.87086L8.26685 4.87086L8.26685 6.30371L10.387
|
||||||
|
6.30371ZM6.76685 6.30371L6.76685 4.87086L4.67027 4.87086L4.67027
|
||||||
|
6.30371L6.76685 6.30371ZM4.67027 12.8794L4.67027 14.3121L6.76685
|
||||||
|
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
|
||||||
|
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
|
||||||
|
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2130_5292">
|
||||||
|
<rect
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="white"
|
||||||
|
transform="translate(0.353027 18.5916) rotate(-90)"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RowIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
17
src/components/svg/run.vue
Normal file
17
src/components/svg/run.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="12"
|
||||||
|
height="13"
|
||||||
|
viewBox="0 0 12 13"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z" fill="#A2B1C6"/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'RunIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
57
src/components/svg/sort.vue
Normal file
57
src/components/svg/sort.vue
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
:class="['sort-icon', { horizontal }, { asc }]"
|
||||||
|
width="6"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 6 12"
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SortIcon',
|
||||||
|
props: {
|
||||||
|
horizontal: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
asc: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
svg.asc {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.horizontal {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg.horizontal.asc {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/components/svg/sqlEditor.vue
Normal file
61
src/components/svg/sqlEditor.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 18 19"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<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
|
||||||
|
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>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0">
|
||||||
|
<rect width="18" height="18" fill="white" transform="translate(0 0.0134277)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'SqlEditorIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
48
src/components/svg/table.vue
Normal file
48
src/components/svg/table.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
viewBox="0 0 18 18"
|
||||||
|
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
|
||||||
|
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>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TableIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -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>
|
||||||
|
|||||||
50
src/components/svg/viewCellValue.vue
Normal file
50
src/components/svg/viewCellValue.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
width="19"
|
||||||
|
height="19"
|
||||||
|
viewBox="0 0 19 19"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<g clip-path="url(#clip0_2131_6054)">
|
||||||
|
<path
|
||||||
|
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
|
||||||
|
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
|
||||||
|
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
|
||||||
|
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
|
||||||
|
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
|
||||||
|
11.5846H3.53784Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
|
||||||
|
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
|
||||||
|
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
|
||||||
|
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
|
||||||
|
17.659 17.802C18.0027 17.5708 18.0939 17.1048 17.8628 16.7611L15.6884
|
||||||
|
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
|
||||||
|
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
|
||||||
|
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
|
||||||
|
fill="#A2B1C6"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_2131_6054">
|
||||||
|
<rect
|
||||||
|
width="18"
|
||||||
|
height="18"
|
||||||
|
fill="white"
|
||||||
|
transform="translate(0.5 18.5916) rotate(-90)"
|
||||||
|
/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'ViewCellValueIcon'
|
||||||
|
}
|
||||||
|
</script>
|
||||||
76
src/lib/chartHelper.js
Normal file
76
src/lib/chartHelper.js
Normal 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
|
||||||
|
}
|
||||||
128
src/lib/csv.js
Normal file
128
src/lib/csv.js
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import Papa from 'papaparse'
|
||||||
|
|
||||||
|
const hintsByCode = {
|
||||||
|
MissingQuotes: 'Edit your CSV so that the field has a closing quote char.',
|
||||||
|
TooFewFields: 'Add fields or try another delimiter.',
|
||||||
|
TooManyFields: 'Edit your CSV or try another delimiter.'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
getResult (source, columns) {
|
||||||
|
const result = {
|
||||||
|
columns: columns || []
|
||||||
|
}
|
||||||
|
const values = {}
|
||||||
|
if (source.meta.fields) {
|
||||||
|
source.meta.fields.forEach(col => {
|
||||||
|
const colName = col.trim()
|
||||||
|
result.columns.push(colName)
|
||||||
|
values[colName] = source.data.map(row => {
|
||||||
|
let value = row[col]
|
||||||
|
if (value instanceof Date) {
|
||||||
|
value = value.toISOString()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else if (columns) {
|
||||||
|
columns.forEach((col, i) => {
|
||||||
|
values[col] = source.data.map(row => {
|
||||||
|
let value = row[i]
|
||||||
|
if (value instanceof Date) {
|
||||||
|
value = value.toISOString()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
for (let i = 0; source.data[0] && i <= source.data[0].length - 1; i++) {
|
||||||
|
const colName = `col${i + 1}`
|
||||||
|
result.columns.push(colName)
|
||||||
|
values[colName] = source.data.map(row => {
|
||||||
|
let value = row[i]
|
||||||
|
if (value instanceof Date) {
|
||||||
|
value = value.toISOString()
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.values = values
|
||||||
|
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 = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const defaultConfig = {
|
||||||
|
delimiter: '', // auto-detect
|
||||||
|
newline: '', // auto-detect
|
||||||
|
quoteChar: '"',
|
||||||
|
escapeChar: '"',
|
||||||
|
header: false,
|
||||||
|
transformHeader: undefined,
|
||||||
|
dynamicTyping: true,
|
||||||
|
preview: 0,
|
||||||
|
encoding: 'UTF-8',
|
||||||
|
worker: true,
|
||||||
|
comments: false,
|
||||||
|
step: undefined,
|
||||||
|
complete: results => {
|
||||||
|
let res
|
||||||
|
try {
|
||||||
|
res = {
|
||||||
|
data: this.getResult(results, config.columns),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
resolve(res)
|
||||||
|
},
|
||||||
|
error: error => {
|
||||||
|
reject(error)
|
||||||
|
},
|
||||||
|
download: false,
|
||||||
|
downloadRequestHeaders: undefined,
|
||||||
|
downloadRequestBody: undefined,
|
||||||
|
skipEmptyLines: 'greedy',
|
||||||
|
chunk: undefined,
|
||||||
|
chunkSize: undefined,
|
||||||
|
fastMode: undefined,
|
||||||
|
beforeFirstChunk: undefined,
|
||||||
|
withCredentials: undefined,
|
||||||
|
transform: undefined,
|
||||||
|
delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
|
||||||
|
}
|
||||||
|
|
||||||
|
Papa.parse(file, { ...defaultConfig, ...config })
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
serialize (resultSet) {
|
||||||
|
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,17 @@ import dbUtils from './_statements'
|
|||||||
let SQL = null
|
let SQL = null
|
||||||
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
|
||||||
|
|
||||||
|
function _getDataSourcesFromSqlResult (sqlResult) {
|
||||||
|
if (!sqlResult) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
const dataSources = {}
|
||||||
|
sqlResult.columns.forEach((column, index) => {
|
||||||
|
dataSources[column] = sqlResult.values.map(row => row[index])
|
||||||
|
})
|
||||||
|
return dataSources
|
||||||
|
}
|
||||||
|
|
||||||
export default class Sql {
|
export default class Sql {
|
||||||
constructor () {
|
constructor () {
|
||||||
this.db = null
|
this.db = null
|
||||||
@@ -36,16 +47,24 @@ export default class Sql {
|
|||||||
if (!sql) {
|
if (!sql) {
|
||||||
throw new Error('exec: Missing query string')
|
throw new Error('exec: Missing query string')
|
||||||
}
|
}
|
||||||
return this.db.exec(sql, params)
|
const sqlResults = this.db.exec(sql, params)
|
||||||
|
return sqlResults.map(result => {
|
||||||
|
return {
|
||||||
|
columns: result.columns,
|
||||||
|
values: _getDataSourcesFromSqlResult(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
import (tabName, columns, values, progressCounterId, progressCallback, chunkSize = 1500) {
|
import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||||
if (this.db === null) {
|
if (this.db === null) {
|
||||||
this.createDb()
|
this.createDb()
|
||||||
}
|
}
|
||||||
this.db.exec(dbUtils.getCreateStatement(tabName, columns, values))
|
const columns = data.columns
|
||||||
const chunks = dbUtils.generateChunks(values, chunkSize)
|
const rowCount = data.values[columns[0]].length
|
||||||
const chunksAmount = Math.ceil(values.length / chunkSize)
|
this.db.exec(dbUtils.getCreateStatement(tabName, data.values))
|
||||||
|
const chunks = dbUtils.generateChunks(data.values, chunkSize)
|
||||||
|
const chunksAmount = Math.ceil(rowCount / chunkSize)
|
||||||
let count = 0
|
let count = 0
|
||||||
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
||||||
const insertStmt = this.db.prepare(insertStr)
|
const insertStmt = this.db.prepare(insertStr)
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import sqliteParser from 'sqlite-parser'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
* generateChunks (arr, size) {
|
* generateChunks (data, size) {
|
||||||
const count = Math.ceil(arr.length / size)
|
const matrix = Object.keys(data).map(col => data[col])
|
||||||
|
const [row] = matrix
|
||||||
|
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
||||||
|
|
||||||
|
const count = Math.ceil(transposedMatrix.length / size)
|
||||||
|
|
||||||
for (let i = 0; i <= count - 1; i++) {
|
for (let i = 0; i <= count - 1; i++) {
|
||||||
const start = size * i
|
const start = size * i
|
||||||
const end = start + size
|
const end = start + size
|
||||||
yield arr.slice(start, end)
|
yield transposedMatrix.slice(start, end)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -17,11 +19,11 @@ export default {
|
|||||||
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
||||||
},
|
},
|
||||||
|
|
||||||
getCreateStatement (tabName, columns, values) {
|
getCreateStatement (tabName, data) {
|
||||||
let result = `CREATE table "${tabName}"(`
|
let result = `CREATE table "${tabName}"(`
|
||||||
columns.forEach((col, index) => {
|
for (const col in data) {
|
||||||
// Get the first row of values to determine types
|
// Get the first row of values to determine types
|
||||||
const value = values[0][index]
|
const value = data[col][0]
|
||||||
let type = ''
|
let type = ''
|
||||||
switch (typeof value) {
|
switch (typeof value) {
|
||||||
case 'number': {
|
case 'number': {
|
||||||
@@ -39,52 +41,9 @@ export default {
|
|||||||
default: type = 'TEXT'
|
default: type = 'TEXT'
|
||||||
}
|
}
|
||||||
result += `"${col}" ${type}, `
|
result += `"${col}" ${type}, `
|
||||||
})
|
}
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ function processMsg (sql) {
|
|||||||
case 'import':
|
case 'import':
|
||||||
return sql.import(
|
return sql.import(
|
||||||
data.tabName,
|
data.tabName,
|
||||||
data.columns,
|
data.data,
|
||||||
data.values,
|
|
||||||
data.progressCounterId,
|
data.progressCounterId,
|
||||||
postMessage
|
postMessage
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -55,8 +56,7 @@ class Database {
|
|||||||
async addTableFromCsv (tabName, data, progressCounterId) {
|
async addTableFromCsv (tabName, data, progressCounterId) {
|
||||||
const result = await this.pw.postMessage({
|
const result = await this.pw.postMessage({
|
||||||
action: 'import',
|
action: 'import',
|
||||||
columns: data.columns,
|
data,
|
||||||
values: data.values,
|
|
||||||
progressCounterId,
|
progressCounterId,
|
||||||
tabName
|
tabName
|
||||||
})
|
})
|
||||||
@@ -77,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.forEach(item => {
|
|
||||||
parsedSchema.push({
|
|
||||||
name: item[0],
|
|
||||||
columns: stms.getColumns(item[1])
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh schema
|
|
||||||
this.schema = parsedSchema
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute (commands) {
|
async execute (commands) {
|
||||||
@@ -120,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) {
|
||||||
|
|||||||
12
src/lib/storedInquiries/_migrations.js
Normal file
12
src/lib/storedInquiries/_migrations.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
_migrate (installedVersion, inquiries) {
|
||||||
|
if (installedVersion === 1) {
|
||||||
|
inquiries.forEach(inquire => {
|
||||||
|
inquire.viewType = 'chart'
|
||||||
|
inquire.viewOptions = inquire.chart
|
||||||
|
delete inquire.chart
|
||||||
|
})
|
||||||
|
return inquiries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/lib/storedInquiries/index.js
Normal file
98
src/lib/storedInquiries/index.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
import migration from './_migrations'
|
||||||
|
|
||||||
|
const migrate = migration._migrate
|
||||||
|
|
||||||
|
export default {
|
||||||
|
version: 2,
|
||||||
|
getStoredInquiries () {
|
||||||
|
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
|
||||||
|
if (!myInquiries) {
|
||||||
|
const oldInquiries = localStorage.getItem('myQueries')
|
||||||
|
if (oldInquiries) {
|
||||||
|
myInquiries = migrate(1, JSON.parse(oldInquiries))
|
||||||
|
this.updateStorage(myInquiries)
|
||||||
|
return myInquiries
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
return (myInquiries && myInquiries.inquiries) || []
|
||||||
|
},
|
||||||
|
|
||||||
|
duplicateInquiry (baseInquiry) {
|
||||||
|
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
|
||||||
|
newInquiry.name = newInquiry.name + ' Copy'
|
||||||
|
newInquiry.id = nanoid()
|
||||||
|
newInquiry.createdAt = new Date()
|
||||||
|
delete newInquiry.isPredefined
|
||||||
|
|
||||||
|
return newInquiry
|
||||||
|
},
|
||||||
|
|
||||||
|
isTabNeedName (inquiryTab) {
|
||||||
|
return inquiryTab.isPredefined || !inquiryTab.name
|
||||||
|
},
|
||||||
|
|
||||||
|
updateStorage (inquiries) {
|
||||||
|
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
|
||||||
|
},
|
||||||
|
|
||||||
|
serialiseInquiries (inquiryList) {
|
||||||
|
const preparedData = JSON.parse(JSON.stringify(inquiryList))
|
||||||
|
preparedData.forEach(inquiry => delete inquiry.isPredefined)
|
||||||
|
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
|
||||||
|
},
|
||||||
|
|
||||||
|
deserialiseInquiries (str) {
|
||||||
|
const inquiries = JSON.parse(str)
|
||||||
|
let inquiryList = []
|
||||||
|
if (!inquiries.version) {
|
||||||
|
// Turn data into array if they are not
|
||||||
|
inquiryList = !Array.isArray(inquiries) ? [inquiries] : inquiries
|
||||||
|
inquiryList = migrate(1, inquiryList)
|
||||||
|
} else {
|
||||||
|
inquiryList = inquiries.inquiries || []
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate new ids if they are the same as existing inquiries
|
||||||
|
inquiryList.forEach(inquiry => {
|
||||||
|
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id)
|
||||||
|
if (allInquiriesIds.includes(inquiry.id)) {
|
||||||
|
inquiry.id = nanoid()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return inquiryList
|
||||||
|
},
|
||||||
|
|
||||||
|
importInquiries () {
|
||||||
|
return fu.importFile()
|
||||||
|
.then(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 () {
|
||||||
|
const res = await fu.readFile('./inquiries.json')
|
||||||
|
const data = await res.json()
|
||||||
|
|
||||||
|
if (!data.version) {
|
||||||
|
return data.length > 0 ? migrate(1, data) : []
|
||||||
|
} else {
|
||||||
|
return data.inquiries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
import { nanoid } from 'nanoid'
|
|
||||||
import fu from '@/lib/utils/fileIo'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
getStoredQueries () {
|
|
||||||
return JSON.parse(localStorage.getItem('myQueries')) || []
|
|
||||||
},
|
|
||||||
|
|
||||||
duplicateQuery (baseQuery) {
|
|
||||||
const newQuery = JSON.parse(JSON.stringify(baseQuery))
|
|
||||||
newQuery.name = newQuery.name + ' Copy'
|
|
||||||
newQuery.id = nanoid()
|
|
||||||
newQuery.createdAt = new Date()
|
|
||||||
delete newQuery.isPredefined
|
|
||||||
|
|
||||||
return newQuery
|
|
||||||
},
|
|
||||||
|
|
||||||
isTabNeedName (queryTab) {
|
|
||||||
const isFromScratch = !queryTab.initName
|
|
||||||
return queryTab.isPredefined || isFromScratch
|
|
||||||
},
|
|
||||||
|
|
||||||
save (queryTab, newName) {
|
|
||||||
const value = {
|
|
||||||
id: queryTab.isPredefined ? nanoid() : queryTab.id,
|
|
||||||
query: queryTab.query,
|
|
||||||
chart: queryTab.$refs.chart.getChartStateForSave(),
|
|
||||||
name: newName || queryTab.initName
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get queries from local storage
|
|
||||||
const myQueries = this.getStoredQueries()
|
|
||||||
|
|
||||||
// Set createdAt
|
|
||||||
if (newName) {
|
|
||||||
value.createdAt = new Date()
|
|
||||||
} else {
|
|
||||||
var queryIndex = myQueries.findIndex(oldQuery => oldQuery.id === queryTab.id)
|
|
||||||
value.createdAt = myQueries[queryIndex].createdAt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert in queries list
|
|
||||||
if (newName) {
|
|
||||||
myQueries.push(value)
|
|
||||||
} else {
|
|
||||||
myQueries[queryIndex] = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to local storage
|
|
||||||
this.updateStorage(myQueries)
|
|
||||||
return value
|
|
||||||
},
|
|
||||||
|
|
||||||
updateStorage (value) {
|
|
||||||
localStorage.setItem('myQueries', JSON.stringify(value))
|
|
||||||
},
|
|
||||||
|
|
||||||
serialiseQueries (queryList) {
|
|
||||||
const preparedData = JSON.parse(JSON.stringify(queryList))
|
|
||||||
preparedData.forEach(query => delete query.isPredefined)
|
|
||||||
return JSON.stringify(preparedData, null, 4)
|
|
||||||
},
|
|
||||||
|
|
||||||
deserialiseQueries (str) {
|
|
||||||
let queryList = JSON.parse(str)
|
|
||||||
// Turn data into array if they are not
|
|
||||||
if (!Array.isArray(queryList)) {
|
|
||||||
queryList = [queryList]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate new ids if they are the same as existing queries
|
|
||||||
queryList.forEach(query => {
|
|
||||||
const allQueriesIds = this.getStoredQueries().map(query => query.id)
|
|
||||||
if (allQueriesIds.includes(query.id)) {
|
|
||||||
query.id = nanoid()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return queryList
|
|
||||||
},
|
|
||||||
|
|
||||||
importQueries () {
|
|
||||||
return fu.importFile()
|
|
||||||
.then(data => {
|
|
||||||
return this.deserialiseQueries(data)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
|
|
||||||
readPredefinedQueries () {
|
|
||||||
return fu.readFile('./queries.json')
|
|
||||||
.then(resp => {
|
|
||||||
return resp.json()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
59
src/lib/tab.js
Normal file
59
src/lib/tab.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { nanoid } from 'nanoid'
|
||||||
|
import time from '@/lib/utils/time'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default class Tab {
|
||||||
|
constructor (state, inquiry = {}) {
|
||||||
|
this.id = inquiry.id || nanoid()
|
||||||
|
this.name = inquiry.id ? inquiry.name : null
|
||||||
|
this.tempName = inquiry.name || (state.untitledLastIndex
|
||||||
|
? `Untitled ${state.untitledLastIndex}`
|
||||||
|
: 'Untitled')
|
||||||
|
this.query = inquiry.query
|
||||||
|
this.viewOptions = inquiry.viewOptions || undefined
|
||||||
|
this.isPredefined = inquiry.isPredefined
|
||||||
|
this.viewType = inquiry.viewType || 'chart'
|
||||||
|
this.result = null
|
||||||
|
this.isGettingResults = false
|
||||||
|
this.error = null
|
||||||
|
this.time = 0
|
||||||
|
this.layout = inquiry.layout || {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
this.maximize = inquiry.maximize
|
||||||
|
|
||||||
|
this.isSaved = !!inquiry.id
|
||||||
|
this.state = state
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute () {
|
||||||
|
this.isGettingResults = true
|
||||||
|
this.result = null
|
||||||
|
this.error = null
|
||||||
|
const db = this.state.db
|
||||||
|
try {
|
||||||
|
const start = new Date()
|
||||||
|
this.result = await db.execute(this.query + ';')
|
||||||
|
this.time = time.getPeriod(start, new Date())
|
||||||
|
|
||||||
|
if (this.result && this.result.values) {
|
||||||
|
events.send('resultset.create',
|
||||||
|
this.result.values[this.result.columns[0]].length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', parseFloat(this.time), { status: 'success' })
|
||||||
|
} catch (err) {
|
||||||
|
this.error = {
|
||||||
|
type: 'error',
|
||||||
|
message: err
|
||||||
|
}
|
||||||
|
|
||||||
|
events.send('query.run', 0, { status: 'error' })
|
||||||
|
}
|
||||||
|
db.refreshSchema()
|
||||||
|
this.isGettingResults = false
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/lib/utils/clipboardIo.js
Normal file
40
src/lib/utils/clipboardIo.js
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import Lib from 'plotly.js/src/lib'
|
||||||
|
import dataUrlToBlob from 'dataurl-to-blob'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async copyText (str, notifyMessage) {
|
||||||
|
await navigator.clipboard.writeText(str)
|
||||||
|
if (notifyMessage) {
|
||||||
|
Lib.notifier(notifyMessage, '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
12
src/lib/utils/events.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
export default {
|
||||||
|
send (name, value, labels) {
|
||||||
|
const event = new CustomEvent('sqliteviz-app-event', {
|
||||||
|
detail: {
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
labels: labels || {}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
export default {
|
export default {
|
||||||
|
isJSON (file) {
|
||||||
|
return file && file.type === 'application/json'
|
||||||
|
},
|
||||||
|
isNDJSON (file) {
|
||||||
|
return file && file.name.endsWith('.ndjson')
|
||||||
|
},
|
||||||
isDatabase (file) {
|
isDatabase (file) {
|
||||||
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
|
||||||
return file.type
|
return file.type
|
||||||
@@ -10,11 +16,9 @@ export default {
|
|||||||
return file.name.replace(/\.[^.]+$/, '')
|
return file.name.replace(/\.[^.]+$/, '')
|
||||||
},
|
},
|
||||||
|
|
||||||
exportToFile (str, fileName, type = 'octet/stream') {
|
downloadFromUrl (url, fileName) {
|
||||||
// Create downloader
|
// Create downloader
|
||||||
const downloader = document.createElement('a')
|
const downloader = document.createElement('a')
|
||||||
const blob = new Blob([str], { type })
|
|
||||||
const url = URL.createObjectURL(blob)
|
|
||||||
downloader.href = url
|
downloader.href = url
|
||||||
downloader.download = fileName
|
downloader.download = fileName
|
||||||
|
|
||||||
@@ -25,6 +29,12 @@ export default {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async exportToFile (str, fileName, type = 'octet/stream') {
|
||||||
|
const blob = new Blob([str], { type })
|
||||||
|
const url = URL.createObjectURL(blob)
|
||||||
|
this.downloadFromUrl(url, fileName)
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Note: if user press Cancel in file choosing dialog
|
* Note: if user press Cancel in file choosing dialog
|
||||||
* it will be an unsettled promise. But it's grabbed by
|
* it will be an unsettled promise. But it's grabbed by
|
||||||
@@ -47,19 +57,20 @@ export default {
|
|||||||
},
|
},
|
||||||
|
|
||||||
importFile () {
|
importFile () {
|
||||||
const reader = new FileReader()
|
|
||||||
|
|
||||||
return this.getFileFromUser('.json')
|
return this.getFileFromUser('.json')
|
||||||
.then(file => {
|
.then(file => {
|
||||||
return new Promise((resolve, reject) => {
|
return this.getFileContent(file)
|
||||||
reader.onload = e => {
|
|
||||||
resolve(e.target.result)
|
|
||||||
}
|
|
||||||
reader.readAsText(file)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFileContent (file) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
return new Promise(resolve => {
|
||||||
|
reader.onload = e => resolve(e.target.result)
|
||||||
|
reader.readAsText(file)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
readFile (path) {
|
readFile (path) {
|
||||||
return fetch(path)
|
return fetch(path)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import '@/assets/styles/tables.css'
|
|||||||
import '@/assets/styles/dialogs.css'
|
import '@/assets/styles/dialogs.css'
|
||||||
import '@/assets/styles/tooltips.css'
|
import '@/assets/styles/tooltips.css'
|
||||||
import '@/assets/styles/messages.css'
|
import '@/assets/styles/messages.css'
|
||||||
|
import 'vue-multiselect/dist/vue-multiselect.min.css'
|
||||||
|
import '@/assets/styles/multiselect.css'
|
||||||
|
|
||||||
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
|
||||||
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||||
|
|||||||
@@ -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')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import Vue from 'vue'
|
import Vue from 'vue'
|
||||||
import VueRouter from 'vue-router'
|
import VueRouter from 'vue-router'
|
||||||
import Editor from '@/views/Main/Editor'
|
import Workspace from '@/views/Main/Workspace'
|
||||||
import MyQueries from '@/views/Main/MyQueries'
|
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'
|
||||||
|
|
||||||
@@ -21,16 +22,21 @@ const routes = [
|
|||||||
component: Main,
|
component: Main,
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
path: '/editor',
|
path: '/workspace',
|
||||||
name: 'Editor',
|
name: 'Workspace',
|
||||||
component: Editor
|
component: Workspace
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/my-queries',
|
path: '/inquiries',
|
||||||
name: 'MyQueries',
|
name: 'Inquiries',
|
||||||
component: MyQueries
|
component: Inquiries
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/load',
|
||||||
|
name: 'Load',
|
||||||
|
component: LoadView
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -39,7 +45,7 @@ const router = new VueRouter({
|
|||||||
})
|
})
|
||||||
|
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, from, next) => {
|
||||||
if (!store.state.db) {
|
if (!store.state.db && to.name !== 'Load') {
|
||||||
const newDb = database.getNewDatabase()
|
const newDb = database.getNewDatabase()
|
||||||
await newDb.loadDb()
|
await newDb.loadDb()
|
||||||
store.commit('setDb', newDb)
|
store.commit('setDb', newDb)
|
||||||
|
|||||||
@@ -1,30 +1,82 @@
|
|||||||
|
import Tab from '@/lib/tab'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
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' query after csv import
|
|
||||||
if (!data || !data.id) {
|
|
||||||
tab.id = nanoid()
|
|
||||||
tab.name = null
|
|
||||||
tab.tempName = state.untitledLastIndex
|
|
||||||
? `Untitled ${state.untitledLastIndex}`
|
|
||||||
: 'Untitled'
|
|
||||||
tab.isUnsaved = true
|
|
||||||
} else {
|
|
||||||
tab.isUnsaved = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
},
|
||||||
|
async saveInquiry ({ state }, { inquiryTab, newName }) {
|
||||||
|
const value = {
|
||||||
|
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
|
||||||
|
query: inquiryTab.query,
|
||||||
|
viewType: inquiryTab.dataView.mode,
|
||||||
|
viewOptions: inquiryTab.dataView.getOptionsForSave(),
|
||||||
|
name: newName || inquiryTab.name
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get inquiries from local storage
|
||||||
|
const myInquiries = state.inquiries
|
||||||
|
|
||||||
|
// Set createdAt
|
||||||
|
if (newName) {
|
||||||
|
value.createdAt = new Date()
|
||||||
|
} else {
|
||||||
|
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
|
||||||
|
value.createdAt = myInquiries[inquiryIndex].createdAt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert in inquiries list
|
||||||
|
if (newName) {
|
||||||
|
myInquiries.push(value)
|
||||||
|
} else {
|
||||||
|
myInquiries.splice(inquiryIndex, 1, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
},
|
||||||
|
addInquiry ({ state }, newInquiry) {
|
||||||
|
state.inquiries.push(newInquiry)
|
||||||
|
},
|
||||||
|
deleteInquiries ({ state, commit }, inquiryIdSet) {
|
||||||
|
state.inquiries = state.inquiries.filter(
|
||||||
|
inquiry => !inquiryIdSet.has(inquiry.id)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Close deleted inquiries if it was opened
|
||||||
|
const tabs = state.tabs
|
||||||
|
let i = tabs.length - 1
|
||||||
|
while (i > -1) {
|
||||||
|
if (inquiryIdSet.has(tabs[i].id)) {
|
||||||
|
commit('deleteTab', tabs[i])
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renameInquiry ({ state, commit }, { inquiryId, newName }) {
|
||||||
|
const renamingInquiry = state.inquiries
|
||||||
|
.find(inquiry => inquiry.id === inquiryId)
|
||||||
|
|
||||||
|
renamingInquiry.name = newName
|
||||||
|
|
||||||
|
// update tab, if renamed inquiry is opened
|
||||||
|
const tab = state.tabs.find(tab => tab.id === renamingInquiry.id)
|
||||||
|
if (tab) {
|
||||||
|
commit('updateTab', {
|
||||||
|
tab,
|
||||||
|
newValues: {
|
||||||
|
name: newName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default {
|
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, chart, isUnsaved }) {
|
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) {
|
||||||
@@ -19,41 +17,51 @@ export default {
|
|||||||
if (id) { tab.id = id }
|
if (id) { tab.id = id }
|
||||||
if (name) { tab.name = name }
|
if (name) { tab.name = name }
|
||||||
if (query) { tab.query = query }
|
if (query) { tab.query = query }
|
||||||
if (chart) { tab.chart = chart }
|
if (viewType) { tab.viewType = viewType }
|
||||||
if (isUnsaved !== undefined) { tab.isUnsaved = isUnsaved }
|
if (viewOptions) { tab.viewOptions = viewOptions }
|
||||||
if (!isUnsaved) {
|
if (isSaved !== undefined) { tab.isSaved = isSaved }
|
||||||
// Saved query is not predefined
|
if (isSaved) {
|
||||||
|
// 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)
|
||||||
updatePredefinedQueries (state, queries) {
|
|
||||||
if (Array.isArray(queries)) {
|
|
||||||
state.predefinedQueries = queries
|
|
||||||
} else {
|
|
||||||
state.predefinedQueries = [queries]
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updatePredefinedInquiries (state, inquiries) {
|
||||||
|
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
|
||||||
|
},
|
||||||
|
setLoadingPredefinedInquiries (state, value) {
|
||||||
|
state.loadingPredefinedInquiries = value
|
||||||
|
},
|
||||||
|
setPredefinedInquiriesLoaded (state, value) {
|
||||||
|
state.predefinedInquiriesLoaded = value
|
||||||
|
},
|
||||||
|
setInquiries (state, value) {
|
||||||
|
state.inquiries = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ export default {
|
|||||||
currentTab: null,
|
currentTab: null,
|
||||||
currentTabId: null,
|
currentTabId: null,
|
||||||
untitledLastIndex: 0,
|
untitledLastIndex: 0,
|
||||||
predefinedQueries: [],
|
inquiries: [],
|
||||||
|
predefinedInquiries: [],
|
||||||
|
loadingPredefinedInquiries: false,
|
||||||
|
predefinedInquiriesLoaded: false,
|
||||||
db: null
|
db: null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,28 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
tooltipElement () {
|
||||||
|
return this.$refs.tooltip
|
||||||
|
}
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
showTooltip (e) {
|
showTooltip (e, tooltipPosition) {
|
||||||
this.tooltipStyle.top = e.clientY - 12 + 'px'
|
const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right']
|
||||||
this.tooltipStyle.left = e.clientX + 12 + 'px'
|
const offset = 12
|
||||||
|
|
||||||
|
if (position[0] === 'top') {
|
||||||
|
this.tooltipStyle.top = e.clientY - offset + 'px'
|
||||||
|
} else {
|
||||||
|
this.tooltipStyle.top = e.clientY + offset + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position[1] === 'right') {
|
||||||
|
this.tooltipStyle.left = e.clientX + offset + 'px'
|
||||||
|
} else {
|
||||||
|
this.tooltipStyle.left = e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
|
||||||
|
}
|
||||||
|
|
||||||
this.tooltipStyle.visibility = 'visible'
|
this.tooltipStyle.visibility = 'visible'
|
||||||
},
|
},
|
||||||
hideTooltip () {
|
hideTooltip () {
|
||||||
|
|||||||
200
src/views/LoadView.vue
Normal file
200
src/views/LoadView.vue
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<logs
|
||||||
|
id="logs"
|
||||||
|
:messages="messages"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
v-if="hasErrors"
|
||||||
|
id="open-workspace-btn"
|
||||||
|
class="secondary"
|
||||||
|
@click="$router.push('/workspace?hide_schema=1')">
|
||||||
|
Open workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import fu from '@/lib/utils/fileIo'
|
||||||
|
import database from '@/lib/database'
|
||||||
|
import Logs from '@/components/Logs'
|
||||||
|
import events from '@/lib/utils/events'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'LoadView',
|
||||||
|
components: {
|
||||||
|
Logs
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
newDb: null,
|
||||||
|
messages: [],
|
||||||
|
dataMsg: {},
|
||||||
|
inquiryMsg: {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hasErrors () {
|
||||||
|
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async created () {
|
||||||
|
const {
|
||||||
|
data_url: dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
inquiry_url: inquiryUrl,
|
||||||
|
inquiry_id: inquiryIds,
|
||||||
|
maximize
|
||||||
|
} = this.$route.query
|
||||||
|
|
||||||
|
events.send('share.load', null, {
|
||||||
|
has_data_url: !!dataUrl,
|
||||||
|
data_format: dataFormat,
|
||||||
|
has_inquiry_url: !!inquiryUrl,
|
||||||
|
inquiry_id_count: (inquiryIds || []).length,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.loadData(dataUrl, dataFormat)
|
||||||
|
const inquiries = await this.loadInquiries(inquiryUrl, inquiryIds)
|
||||||
|
if (inquiries && inquiries.length > 0) {
|
||||||
|
await this.openInquiries(inquiries, maximize)
|
||||||
|
}
|
||||||
|
if (!this.hasErrors) {
|
||||||
|
this.$router.push('/workspace?hide_schema=1')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async loadData (dataUrl, dataFormat) {
|
||||||
|
this.newDb = database.getNewDatabase()
|
||||||
|
if (dataUrl) {
|
||||||
|
this.dataMsg = {
|
||||||
|
message: 'Preparing data...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.dataMsg)
|
||||||
|
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingDataIndicator = setTimeout(() => {
|
||||||
|
if (this.dataMsg.type === 'info') {
|
||||||
|
this.dataMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
if (dataFormat === 'sqlite') {
|
||||||
|
await this.getSqliteDb(dataUrl)
|
||||||
|
} else {
|
||||||
|
this.dataMsg.message = 'Unknown data format'
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingDataIndicator)
|
||||||
|
} else {
|
||||||
|
await this.newDb.loadDb()
|
||||||
|
}
|
||||||
|
this.$store.commit('setDb', this.newDb)
|
||||||
|
},
|
||||||
|
async getSqliteDb (dataUrl) {
|
||||||
|
try {
|
||||||
|
const filename = new URL(dataUrl).pathname.split('/').pop()
|
||||||
|
const res = await fu.readFile(dataUrl)
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('Fetching DB failed')
|
||||||
|
}
|
||||||
|
const file = await res.blob()
|
||||||
|
file.name = filename
|
||||||
|
|
||||||
|
await this.newDb.loadDb(file)
|
||||||
|
this.dataMsg.message = 'Data is ready'
|
||||||
|
this.dataMsg.type = 'success'
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.dataMsg.message = error
|
||||||
|
this.dataMsg.type = 'error'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadInquiries (inquiryUrl, inquiryIds = []) {
|
||||||
|
if (!inquiryUrl) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
// Show loading indicator after 1 second
|
||||||
|
const loadingInquiriesIndicator = setTimeout(() => {
|
||||||
|
if (this.inquiryMsg.type === 'info') {
|
||||||
|
this.inquiryMsg.type = 'loading'
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
try {
|
||||||
|
this.inquiryMsg = {
|
||||||
|
message: 'Preparing inquiries...',
|
||||||
|
type: 'info'
|
||||||
|
}
|
||||||
|
this.messages.push(this.inquiryMsg)
|
||||||
|
|
||||||
|
const res = await fu.readFile(inquiryUrl)
|
||||||
|
const file = await res.json()
|
||||||
|
|
||||||
|
this.inquiryMsg.message = 'Inquiries are ready'
|
||||||
|
this.inquiryMsg.type = 'success'
|
||||||
|
|
||||||
|
return inquiryIds.length > 0
|
||||||
|
? file.inquiries.filter(inquiry => inquiryIds.includes(inquiry.id))
|
||||||
|
: file.inquiries
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
this.inquiryMsg.message = error
|
||||||
|
this.inquiryMsg.type = 'error'
|
||||||
|
}
|
||||||
|
// Loading indicator is not needed anymore
|
||||||
|
clearTimeout(loadingInquiriesIndicator)
|
||||||
|
},
|
||||||
|
async openInquiries (inquiries, maximize) {
|
||||||
|
let tabToOpen = null
|
||||||
|
const layout = maximize ? this.getLayout(maximize) : undefined
|
||||||
|
for (const inquiry of inquiries) {
|
||||||
|
const tabId = await this.$store.dispatch('addTab', {
|
||||||
|
...inquiry,
|
||||||
|
id: undefined,
|
||||||
|
layout,
|
||||||
|
maximize
|
||||||
|
})
|
||||||
|
if (!tabToOpen) {
|
||||||
|
tabToOpen = tabId
|
||||||
|
this.$store.commit('setCurrentTabId', tabToOpen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.state.currentTab.execute()
|
||||||
|
},
|
||||||
|
|
||||||
|
getLayout (panelToMaximize) {
|
||||||
|
if (panelToMaximize === 'dataView') {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'hidden',
|
||||||
|
table: 'above',
|
||||||
|
dataView: 'bottom'
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
sqlEditor: 'above',
|
||||||
|
table: 'bottom',
|
||||||
|
dataView: 'hidden'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#logs {
|
||||||
|
margin: 8px auto;
|
||||||
|
max-width: 800px;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#open-workspace-btn {
|
||||||
|
margin: 16px auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -44,16 +44,16 @@ export default {
|
|||||||
|
|
||||||
async created () {
|
async created () {
|
||||||
const state = this.$store.state
|
const state = this.$store.state
|
||||||
let result = await state.db.execute('select sqlite_version()')
|
let result = (await state.db.execute('select sqlite_version()')).values
|
||||||
this.info.push({
|
this.info.push({
|
||||||
name: 'SQLite version',
|
name: 'SQLite version',
|
||||||
info: result.values[0]
|
info: result['sqlite_version()']
|
||||||
})
|
})
|
||||||
|
|
||||||
result = await state.db.execute('PRAGMA compile_options')
|
result = (await state.db.execute('PRAGMA compile_options')).values
|
||||||
this.info.push({
|
this.info.push({
|
||||||
name: 'SQLite compile options',
|
name: 'SQLite compile options',
|
||||||
info: result.values.map(row => row[0])
|
info: result.compile_options
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ export default {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
#app-info-icon {
|
#app-info-icon {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
width: 24px;
|
||||||
}
|
}
|
||||||
#app-info-container {
|
#app-info-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
|
||||||
|
|
||||||
export function getDataSourcesFromSqlResult (sqlResult) {
|
|
||||||
if (!sqlResult) {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const dataSorces = {}
|
|
||||||
const matrix = sqlResult.values
|
|
||||||
const [row] = matrix
|
|
||||||
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
|
||||||
sqlResult.columns.forEach((column, index) => {
|
|
||||||
dataSorces[column] = transposedMatrix[index]
|
|
||||||
})
|
|
||||||
return dataSorces
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOptionsFromDataSources (dataSources) {
|
|
||||||
return Object.keys(dataSources).map(name => ({
|
|
||||||
value: name,
|
|
||||||
label: name
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getChartStateForSave (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 {
|
|
||||||
getDataSourcesFromSqlResult,
|
|
||||||
getOptionsFromDataSources,
|
|
||||||
getChartStateForSave
|
|
||||||
}
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-show="visible" class="chart-container">
|
|
||||||
<div class="warning chart-warning" v-show="!sqlResult && visible">
|
|
||||||
There is no data to build a chart. Run your sql query and make sure the result is not empty.
|
|
||||||
</div>
|
|
||||||
<PlotlyEditor
|
|
||||||
:data="state.data"
|
|
||||||
:layout="state.layout"
|
|
||||||
:frames="state.frames"
|
|
||||||
:config="{ editable: true, displaylogo: false }"
|
|
||||||
:dataSources="dataSources"
|
|
||||||
:dataSourceOptions="dataSourceOptions"
|
|
||||||
:plotly="plotly"
|
|
||||||
@onUpdate="update"
|
|
||||||
@onRender="go"
|
|
||||||
:useResizeHandler="true"
|
|
||||||
:debug="true"
|
|
||||||
:advancedTraceTypeSelector="true"
|
|
||||||
class="chart"
|
|
||||||
ref="plotlyEditor"
|
|
||||||
:style="{ height: !sqlResult ? 'calc(100% - 40px)' : '100%' }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import plotly from 'plotly.js/dist/plotly'
|
|
||||||
import 'react-chart-editor/lib/react-chart-editor.min.css'
|
|
||||||
|
|
||||||
import PlotlyEditor from 'react-chart-editor'
|
|
||||||
import chartHelper from './chartHelper'
|
|
||||||
import dereference from 'react-chart-editor/lib/lib/dereference'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'Chart',
|
|
||||||
props: ['sqlResult', 'initChart', 'visible'],
|
|
||||||
components: {
|
|
||||||
PlotlyEditor
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
plotly: plotly,
|
|
||||||
state: this.initChart || {
|
|
||||||
data: [],
|
|
||||||
layout: {},
|
|
||||||
frames: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
dataSources () {
|
|
||||||
return chartHelper.getDataSourcesFromSqlResult(this.sqlResult)
|
|
||||||
},
|
|
||||||
dataSourceOptions () {
|
|
||||||
return chartHelper.getOptionsFromDataSources(this.dataSources)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
dataSources () {
|
|
||||||
// we need to update state.data in order to update the graph
|
|
||||||
// https://github.com/plotly/react-chart-editor/issues/948
|
|
||||||
dereference(this.state.data, this.dataSources)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
go (data, layout, frames) {
|
|
||||||
// TODO: check changes and enable Save button if needed
|
|
||||||
},
|
|
||||||
update (data, layout, frames) {
|
|
||||||
this.state = { data, layout, frames }
|
|
||||||
this.$emit('update')
|
|
||||||
},
|
|
||||||
getChartStateForSave () {
|
|
||||||
return chartHelper.getChartStateForSave(this.state, this.dataSources)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.chart-container {
|
|
||||||
height: calc(100% - 89px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-warning {
|
|
||||||
height: 40px;
|
|
||||||
line-height: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart {
|
|
||||||
border-top: 1px solid var(--color-border);
|
|
||||||
min-height: 242px;
|
|
||||||
}
|
|
||||||
|
|
||||||
>>> .editor_controls .sidebar__item:before {
|
|
||||||
width: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user