1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-07 02:28:54 +08:00

99 Commits

Author SHA1 Message Date
lana-k
310a939109 #89 add tests 2021-12-24 16:13:42 +01:00
lana-k
bb9ba08902 #89 remove head and body 2021-12-22 20:42:53 +01:00
lana-k
c7c727ff78 fix lint 2021-12-21 22:15:21 +01:00
lana-k
8669a6a9e5 #89 export to html plolty charts and pivots 2021-12-21 22:13:02 +01:00
lana-k
c1cc5bb95e getHtml for chart #89 2021-12-20 22:31:08 +01:00
lana-k
9c55e76a41 update version 2021-12-19 16:00:11 +01:00
lana-k
70a9edf57e fix lint 2021-12-19 15:57:30 +01:00
lana-k
b2c2344951 update version 2021-12-19 15:38:17 +01:00
lana-k
cbec91e78a Merge branch 'master' of github.com:lana-k/sqliteviz 2021-12-19 15:37:01 +01:00
lana-k
816b0e6218 show plotly warnings and errors #55 2021-12-19 15:36:46 +01:00
saaj
4ed93bbea7 Two more extensions and improved extension documentation (#86) 2021-09-10 20:11:37 +02:00
lana-k
3e3a70430f fix lint 2021-09-03 16:03:06 +02:00
lana-k
bce3854792 fix test 2021-09-03 15:58:37 +02:00
lana-k
307cac06e6 remove html2canvas logging 2021-09-03 15:58:20 +02:00
lana-k
c4b0bdc870 fix lint 2021-09-03 15:25:30 +02:00
lana-k
69e0b2129b add tests for Pivot and Chart 2021-09-03 15:25:23 +02:00
lana-k
4ebb3715d6 add tests for RinResult 2021-09-03 15:22:48 +02:00
lana-k
858e32c9fd fix errors in tests 2021-09-01 22:42:34 +02:00
lana-k
42cce95ed0 fix errors in tests 2021-09-01 22:27:51 +02:00
lana-k
61ffcc82d6 add test for clipboard copying in DataView 2021-08-30 23:10:57 +02:00
lana-k
4716bcf258 DataView tests 2021-08-29 22:27:25 +02:00
lana-k
23d2421584 add tests for clipboard lib 2021-08-29 21:32:13 +02:00
lana-k
4730afc68a add test for tab 2021-08-29 21:31:50 +02:00
lana-k
6b902d5f00 fix line size 2021-08-29 21:31:30 +02:00
lana-k
a484c1e022 add test for pivot helper 2021-08-29 21:30:53 +02:00
lana-k
1ed5bf2fae add test for sleep 2021-08-29 21:29:36 +02:00
lana-k
da8dc71f23 add test to csv (serialize) 2021-08-29 21:29:21 +02:00
lana-k
ebac3d8f6c add test for chart helper 2021-08-29 21:28:55 +02:00
lana-k
0336168bdc line size lint checking 2021-08-29 21:27:02 +02:00
lana-k
e38f482933 saveAsPng refactoring 2021-08-28 19:07:10 +02:00
lana-k
bcd98fe1ad add sleep function 2021-08-23 22:20:03 +02:00
lana-k
ec3ec6f57a add test for selection and filter #84 2021-08-22 16:50:17 +02:00
lana-k
ae9e2bc03c fix lint errors 2021-08-22 15:01:22 +02:00
lana-k
fe11d446f3 add NO found msg #83; fix checkbox behaviour #84 2021-08-22 15:00:34 +02:00
lana-k
e6bef139f1 Support vector graphic chart download #39 2021-08-21 22:51:17 +02:00
lana-k
d7e0e52da5 add copy dialog for csv #50 2021-08-21 21:41:06 +02:00
lana-k
09cec13a9c show dialog for keeping user activity #50 2021-08-21 18:44:29 +02:00
lana-k
0cae368350 fix lint errors #50 2021-08-20 22:33:15 +02:00
lana-k
88c62a1dfb show message about ClipboardItem support #50
for Firefox
2021-08-20 22:30:48 +02:00
lana-k
ead861b610 copy png to clipboard #50 2021-08-20 21:45:05 +02:00
lana-k
949e6b626e fix lint errors #81 2021-08-20 18:07:30 +02:00
lana-k
6dc81f7be6 copy result set as tsv #50 2021-08-20 18:06:58 +02:00
lana-k
bb39dcbc2f use tab for export to csv #81
(support paste to google sheets)
2021-08-20 18:05:51 +02:00
lana-k
131efb55df update readme #81 2021-08-20 15:38:18 +02:00
lana-k
b33974c26c Export result set to CSV file #81 2021-08-20 15:36:19 +02:00
lana-k
e9c97beb5a fix lint error 2021-08-18 22:09:32 +02:00
lana-k
cabc4f8acd fix JS error during running queries #80 2021-08-18 21:50:42 +02:00
lana-k
679a785d70 add test for view #78 2021-08-18 21:24:12 +02:00
lana-k
a91add40cc uninstall sqlite parser #78 2021-08-18 20:58:53 +02:00
twoxfh
53b2d8372f Updating how database object information is getting retrieved (#79)
* Updating how database object information is getting retrieved

I updated the SQLite query for gathering database objects to make use of the JSON1 extension so you can grab tables and views name, their associated columns with types and set it to the schema. This removes the need to work with DDL's. Hints for Tables and Views works since my approach is they are both database objects.
2021-08-18 20:22:30 +02:00
lana-k
f3e8448851 update version 2021-08-11 23:16:11 +02:00
lana-k
4213e9df5c fix lint errors 2021-08-10 23:36:16 +02:00
lana-k
9f32323a80 fix column order in result set #74 2021-08-10 20:31:12 +02:00
lana-k
2ed5160f65 remove maps from root 2021-08-05 18:47:24 +02:00
lana-k
fe8ab3f3e8 update version 2021-08-05 18:44:10 +02:00
saaj
848112979b SQLite WebAssembly micro-benchmark and new build with a memory leak fix (#70) 2021-08-05 18:35:31 +02:00
lana-k
2830df2adc Update README.md 2021-08-04 22:25:23 +02:00
lana-k
5017b55944 Pivot implementation and redesign (#69)
- Pivot support implementation 
- Rename queries into inquiries
- Rename editor into workspace
- Change result set format
- New JSON format for inquiries
- Redesign panels
2021-08-04 22:20:51 +02:00
lana-k
8d0bc6affe Update package.json 2021-07-15 22:48:46 +02:00
lana-k
d07506266c fix lint error 2021-07-15 22:41:36 +02:00
lana-k
cea1d40797 Merge branch 'master' of github.com:lana-k/sqliteviz 2021-07-15 22:36:33 +02:00
lana-k
0f2dc9f11e fix ISO time columns when no column names #66 2021-07-15 22:36:07 +02:00
lana-k
23250259eb Update package.json 2021-07-04 13:57:13 +02:00
lana-k
fb930028de update chokidar 2021-07-04 13:05:06 +02:00
lana-k
1ff4adf95c Merge branch 'master' of github.com:lana-k/sqliteviz 2021-07-04 12:23:35 +02:00
lana-k
78cdb3809c update npm in CI 2021-07-03 19:55:47 +02:00
saaj
3a6628cab9 Pre-built custom sql.js for sqliteviz (#62)
* Proof-of-concept pre-built custom sql.js

* Rewrite Makefile as a couple of comprehensible Python scripts

* Add link to a blog post about transitive closure in SQLite

* Remove eval extension -- no much point as it only returns a string

* Consistently use existing Path objects

* Add basic tests scalar functions from extension-functions.c

* Test presence of functions from the rest of built extensions

* Use the same sqlite.com domain

* Add a couple SQLite compile flags that may make it a bit faster

* Add regexpi function test

* Add node about regexpi and REGEXP operator

* Workaround first build failure, rebuild lock file and minor fixes
2021-07-03 16:43:43 +02:00
lana-k
418809d27d lint no fix in CI 2021-07-03 15:48:56 +02:00
lana-k
f9edeafd40 fix csv import with ISO dates #64 2021-07-01 19:07:59 +02:00
lana-k
a37ed93306 update version 2021-06-17 12:24:41 +02:00
lana-k
cf4b83f7d4 fix csv result when column names have spaces #59 2021-06-17 12:23:44 +02:00
lana-k
2abd42c9c3 run tests on pull request 2021-06-07 13:20:42 +02:00
lana-k
1251c542cb update react-chart-editor 2021-05-25 11:52:37 +02:00
lana-k
ac89259924 Always create empty db on start #46 2021-05-24 21:45:51 +02:00
lana-k
179ff8b1e1 fix table layout on My queries #32 2021-05-24 20:50:52 +02:00
lana-k
99a10225a3 CSV import as a table and db connection rework
- Add csv to existing db #32
- [RFE] Simplify working with temporary tables #53
2021-05-24 19:40:47 +02:00
lana-k
c96deb5766 Fix overflow for Firefox #46 2021-05-22 22:26:23 +02:00
lana-k
700970e1cc Add addTable icon #32 2021-05-22 22:25:19 +02:00
lana-k
e2be61e2cf Make error in text field start with uppercase #32 2021-05-22 22:24:20 +02:00
lana-k
9c2c8f3692 Make Logs smaller #32 2021-05-22 22:23:12 +02:00
lana-k
414a116f94 Lost focus in SQL query editor #54 2021-05-19 22:50:56 +02:00
lana-k
3e503f85a9 Stub app-diagnostic-info in tests 2021-05-19 21:52:42 +02:00
lana-k
88257bfcf6 fix CSV dialog height typo 2021-05-19 16:53:30 +02:00
lana-k
bdcc494138 Add scrolling to App info , CSV import dialogs #46 2021-05-19 16:43:42 +02:00
lana-k
d750541c80 App diagnostic dialog #46 2021-05-19 15:46:18 +02:00
lana-k
75f743ff9e remove title in release notes 2021-05-18 15:38:44 +02:00
lana-k
8a9f4b3c0a add Help link 2021-05-18 15:04:35 +02:00
lana-k
77468d34ae add release notes 2021-05-18 15:04:19 +02:00
lana-k
a0577ec0ce Fix gradient for Safari 2021-05-17 21:34:42 +02:00
lana-k
e7d1398546 Rewrite reg exp: make them work in Safari #52 2021-05-17 21:34:24 +02:00
lana-k
aa52048d51 Fix file type detection #48
file.type is empty on some Windows machines (Registry settings affects)
2021-05-17 21:32:09 +02:00
lana-k
33913f8f5c fix lint 2021-05-14 16:47:01 +02:00
lana-k
51eb7a543c Merge branch 'master' of github.com:lana-k/sqliteviz 2021-05-14 16:43:13 +02:00
lana-k
a3fb38b23c SQL query execution state in UI #3
- use LoadingIndicator
- use Logs
2021-05-14 16:42:58 +02:00
lana-k
3bb40b4eb7 Improve LoadingIndicator
- size parameters
- smooth animation (use ony transform)
2021-05-14 16:40:55 +02:00
lana-k
6864bf84f8 Update README.md 2021-05-06 21:36:20 +02:00
lana-k
9f1b3823f6 Update README.md 2021-05-06 20:46:36 +02:00
lana-k
7574f529c3 add hidden state for file in animation 2021-05-06 15:17:15 +02:00
lana-k
653f8eff7b minor changes in animation 2021-05-06 15:05:58 +02:00
159 changed files with 11482 additions and 5030 deletions

View File

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

17
.github/workflows/config.grenrc.js vendored Normal file
View File

@@ -0,0 +1,17 @@
module.exports = {
dataSource: 'milestones',
ignoreIssuesWith: [
'wontfix',
'duplicate'
],
milestoneMatch: 'v{{tag_name}}',
template: {
issue: '- {{name}} [{{text}}]({{url}})',
changelogTitle: "",
release: "{{body}}",
},
groupBy: {
'Enhancements': ["enhancement", "internal"],
'Bug fixes': ["bug"]
}
}

View File

@@ -15,6 +15,10 @@ jobs:
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10.x node-version: 10.x
- name: Update npm
run: npm install -g npm@7
- name: npm install and build - name: npm install and build
run: | run: |
npm install npm install
@@ -23,13 +27,21 @@ jobs:
- name: Create archive - name: Create archive
run: | run: |
cd dist cd dist
zip -9 -r dist.zip . -x "js/*.map" zip -9 -r dist.zip . -x "js/*.map" -x "/*.map"
- name: Create Release Notes
run: |
npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.js"
env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release - name: Create release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "dist/dist.zip" artifacts: "dist/dist.zip"
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
- name: Deploy 🚀 - name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@4.1.1 uses: JamesIves/github-pages-deploy-action@4.1.1

View File

@@ -4,6 +4,9 @@ on:
push: push:
branches: branches:
- 'master' - 'master'
pull_request:
branches:
- 'master'
jobs: jobs:
test: test:
@@ -21,11 +24,14 @@ jobs:
sudo apt-get update sudo apt-get update
sudo apt-get install -y chromium-browser firefox sudo apt-get install -y chromium-browser firefox
- name: Update npm
run: npm install -g npm@7
- name: Install the project - name: Install the project
run: npm install run: npm install
- name: Run lint - name: Run lint
run: npm run lint run: npm run lint -- --no-fix
- name: Run karma tests - name: Run karma tests
run: npm run test run: npm run test

View File

@@ -7,13 +7,16 @@
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 or CSV 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 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/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 GitHub Pages at [lana-k.github.io/sqliteviz][6].
@@ -24,7 +27,7 @@ For user documentation, check out sqliteviz [Wiki][7].
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
@@ -37,3 +40,4 @@ It is built on top of [react-chart-editor][3], [sql.js][4] and [Vue-Codemirror][
[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

View File

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

2
lib/sql-js/.dockerignore Normal file
View File

@@ -0,0 +1,2 @@
benchmark
dist

9
lib/sql-js/Dockerfile Normal file
View File

@@ -0,0 +1,9 @@
FROM emscripten/emsdk:2.0.24
WORKDIR /tmp/build
COPY configure.py .
RUN python3.8 configure.py
COPY build.py .
RUN python3.8 build.py

101
lib/sql-js/README.md Normal file
View File

@@ -0,0 +1,101 @@
# SQLite WebAssembly build
This directory contains Docker-based build script, `make.sh`, that builds
a custom version of [sql.js][1]. It allows sqliteviz to have more recent
version of SQLite build with a number of useful extensions.
`Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py`
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
## Extension
SQLite [amalgamation][2] extensions included:
1. [FTS5][4] -- virtual table module that provides full-text search
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:
1. `generate_series` table-valued [series function][6] ([series.c][7])
2. `transitive_closure` virtual table for
[Querying Tree Structures in SQLite][11] ([closure.c][8])
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])
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:
1. [pivot_vtab][5] -- a pivot virtual table
To ease the step to have working clone locally, the build is committed into
the repository.
Examples of queries involving these extensions can be found in the test suite in
[sqliteExtensions.spec.js][19].
## Build method
Basically it's extended amalgamation and `SQLITE_EXTRA_INIT` concisely
described in [this message from SQLite Forum][12]:
> Simply append it to the end of the amalgamation file. The real problem is
> how you get the init function called. The easiest way (to me at any rate) is
> to append a function (after the extensions you want to add are all appended)
> that adds the init function for each extension to the auto extension list
> for new connections, and set the pre-processor symbol SQLITE_EXTRA_INIT to
> the name of this function. [...]
>
> An example `SQLITE_EXTRA_INIT` function looks like this:
>
> ```
> int core_init(const char* dummy)
> {
> int nErr = 0;
>
> nErr += sqlite3_auto_extension((void*)sqlite3_autobusy_init);
> nErr += sqlite3_auto_extension((void*)sqlite3_ipaddress_init);
>
> return nErr ? SQLITE_ERROR : SQLITE_OK;
> }
> ```
>
> so you would then define `SQLITE_EXTRA_INIT=core_init` when compiling the
> amalgamation code and the extensions would thereafter be automatically
> initialized on each connection.
[1]: https://github.com/sql-js/sql.js
[2]: https://sqlite.org/amalgamation.html
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
[4]: https://sqlite.org/fts5.html
[5]: https://github.com/jakethaw/pivot_vtab
[6]: https://sqlite.org/series.html
[7]: https://sqlite.org/src/file/ext/misc/series.c
[8]: https://sqlite.org/src/file/ext/misc/closure.c
[9]: https://sqlite.org/src/file/ext/misc/uuid.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/
[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

4
lib/sql-js/benchmark/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
/lib/build-*
/lib/dist
/build-*-result.json
/sample.csv

View File

@@ -0,0 +1,19 @@
FROM node:12-buster
RUN set -ex; \
echo 'deb http://deb.debian.org/debian unstable main' \
> /etc/apt/sources.list.d/unstable.list; \
apt-get update; \
apt-get install -y -t unstable firefox; \
apt-get install -y 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

View File

@@ -0,0 +1,14 @@
# SQLite WebAssembly build micro-benchmark
This directory contains a micro-benchmark for evaluating SQLite
WebAssembly builds performance on typical SQL queries, run from
`make.sh` script. It can also serve as a smoke test.
The benchmark operates on a set of SQLite WebAssembly builds expected
in `lib/build-$NAME` directories each containing `sql-wasm.js` and
`sql-wasm.wasm`. Then it creates a Docker image for each, and runs
the benchmark in Firefox and Chromium using Karma in the container.
After successful run, the benchmark result of each build is contained
in `build-$NAME-result.json`. The JSON result files can be analysed
using `result-analysis.ipynb` Jupyter notebook.

View 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' }
})
}

View File

@@ -0,0 +1,5 @@
{
"name": "sql.js",
"main": "./dist/sql-wasm.js",
"private": true
}

21
lib/sql-js/benchmark/make.sh Executable file
View File

@@ -0,0 +1,21 @@
#!/bin/bash -e
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
for d in lib/build-* ; do
rm -r lib/dist || true
cp -r $d lib/dist
name=$(basename $d)
docker build -t sqliteviz/sqljs-benchmark:$name .
docker rm sqljs-benchmark-$name 2> /dev/null || true
docker run -it --name sqljs-benchmark-$name sqliteviz/sqljs-benchmark:$name
docker cp sqljs-benchmark-$name:/tmp/build/suite-result.json ${name}-result.json
docker rm sqljs-benchmark-$name
done
rm -r lib/dist

View 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"
}
}

File diff suppressed because one or more lines are too long

View 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
}

86
lib/sql-js/build.py Normal file
View File

@@ -0,0 +1,86 @@
import logging
import subprocess
from pathlib import Path
cflags = (
'-O2',
'-DSQLITE_DEFAULT_CACHE_SIZE=-65536', # 64 MiB
'-DSQLITE_DEFAULT_MEMSTATUS=0',
'-DSQLITE_DEFAULT_SYNCHRONOUS=0',
'-DSQLITE_DISABLE_LFS',
'-DSQLITE_DQS=0',
'-DSQLITE_ENABLE_FTS3',
'-DSQLITE_ENABLE_FTS3_PARENTHESIS',
'-DSQLITE_ENABLE_FTS5',
'-DSQLITE_ENABLE_JSON1',
'-DSQLITE_ENABLE_NORMALIZE',
'-DSQLITE_EXTRA_INIT=extra_init',
'-DSQLITE_OMIT_DEPRECATED',
'-DSQLITE_OMIT_LOAD_EXTENSION',
'-DSQLITE_OMIT_SHARED_CACHE',
'-DSQLITE_THREADSAFE=0',
)
emflags = (
# Base
'--memory-init-file', '0',
'-s', 'RESERVED_FUNCTION_POINTERS=64',
'-s', 'ALLOW_TABLE_GROWTH=1',
'-s', 'SINGLE_FILE=0',
# WASM
'-s', 'WASM=1',
'-s', 'ALLOW_MEMORY_GROWTH=1',
# Optimisation
'-s', 'INLINING_LIMIT=50',
'-O3',
'-flto',
# sql.js
'-s', 'EXPORTED_FUNCTIONS=@src/sqljs/exported_functions.json',
'-s', 'EXPORTED_RUNTIME_METHODS=@src/sqljs/exported_runtime_methods.json',
'--pre-js', 'src/sqljs/api.js',
)
def build(src: Path, dst: Path):
out = Path('out')
out.mkdir()
logging.info('Building LLVM bitcode for sqlite3.c')
subprocess.check_call([
'emcc',
*cflags,
'-c', src / 'sqlite3.c',
'-o', out / 'sqlite3.bc',
])
logging.info('Building LLVM bitcode for extension-functions.c')
subprocess.check_call([
'emcc',
*cflags,
'-c', src / 'extension-functions.c',
'-o', out / 'extension-functions.bc',
])
logging.info('Building WASM from bitcode')
subprocess.check_call([
'emcc',
*emflags,
out / 'sqlite3.bc',
out / 'extension-functions.bc',
'-o', out / 'sql-wasm.js',
])
logging.info('Post-processing build and copying to dist')
(out / 'sql-wasm.wasm').rename(dst / 'sql-wasm.wasm')
with (dst / 'sql-wasm.js').open('w') as f:
f.write((src / 'sqljs' / 'shell-pre.js').read_text())
f.write((out / 'sql-wasm.js').read_text())
f.write((src / 'sqljs' / 'shell-post.js').read_text())
if __name__ == '__main__':
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
src = Path('src')
dst = Path('dist')
dst.mkdir()
build(src, dst)

107
lib/sql-js/configure.py Normal file
View File

@@ -0,0 +1,107 @@
import logging
import shutil
import subprocess
import sys
import zipfile
from io import BytesIO
from pathlib import Path
from urllib import request
amalgamation_url = 'https://sqlite.org/2021/sqlite-amalgamation-3360000.zip'
# Extension-functions
# ===================
# It breaks amalgamation if appended as other extension because it redefines
# several functions, so build it separately. Note that sql.js registers these
# extension functions by calling ``registerExtensionFunctions`` itself.
contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions.c?get=25'
extension_urls = (
# Miscellaneous extensions
# ========================
('https://sqlite.org/src/raw/c6bd5d24?at=series.c', 'sqlite3_series_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/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
# =====================
('https://github.com/jakethaw/pivot_vtab/raw/08ab0797/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
)
sqljs_url = 'https://github.com/sql-js/sql.js/archive/refs/tags/v1.5.0.zip'
def _generate_extra_init_c_function(init_function_names):
auto_ext_calls = '\n'.join([
'nErr += sqlite3_auto_extension((void*){});'.format(init_fn)
for init_fn in init_function_names
])
return '''
int extra_init(const char* dummy)
{
int nErr = 0;
%s
return nErr ? SQLITE_ERROR : SQLITE_OK;
}
''' % auto_ext_calls
def _get_amalgamation(tgt: Path):
logging.info('Downloading and extracting SQLite amalgamation %s', amalgamation_url)
archive = zipfile.ZipFile(BytesIO(request.urlopen(amalgamation_url).read()))
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
for zpath in archive_root_dir.iterdir():
with zpath.open() as fr, (tgt / zpath.name).open('wb') as fw:
shutil.copyfileobj(fr, fw)
def _get_contrib_functions(tgt: Path):
request.urlretrieve(contrib_functions_url, tgt / 'extension-functions.c')
def _get_extensions(tgt: Path):
init_functions = []
sqlite3_c = tgt / 'sqlite3.c'
with sqlite3_c.open('ab') as f:
for url, init_fn in extension_urls:
logging.info('Downloading and appending to amalgamation %s', url)
with request.urlopen(url) as resp:
shutil.copyfileobj(resp, f)
init_functions.append(init_fn)
logging.info('Appending SQLITE_EXTRA_INIT to amalgamation')
f.write(_generate_extra_init_c_function(init_functions).encode())
def _get_sqljs(tgt: Path):
logging.info('Downloading and extracting sql.js %s', sqljs_url)
archive = zipfile.ZipFile(BytesIO(request.urlopen(sqljs_url).read()))
archive_root_dir = zipfile.Path(archive, archive.namelist()[0])
(tgt / 'sqljs').mkdir()
for zpath in (archive_root_dir / 'src').iterdir():
with zpath.open() as fr, (tgt / 'sqljs' / zpath.name).open('wb') as fw:
shutil.copyfileobj(fr, fw)
def configure(tgt: Path):
_get_amalgamation(tgt)
_get_contrib_functions(tgt)
_get_extensions(tgt)
_get_sqljs(tgt)
subprocess.check_call(['emcc', '--version'])
if __name__ == '__main__':
if sys.version_info < (3, 8):
print('Python 3.8 or higher is expected', file=sys.stderr)
sys.exit(1)
logging.basicConfig(level='INFO', format='%(asctime)s %(levelname)s %(name)s %(message)s')
src = Path('src')
src.mkdir()
configure(src)

94
lib/sql-js/dist/sql-wasm.js vendored Normal file

File diff suppressed because one or more lines are too long

BIN
lib/sql-js/dist/sql-wasm.wasm vendored Executable file

Binary file not shown.

9
lib/sql-js/make.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash -e
docker build -t sqliteviz/sqljs .
rm -r dist || true
CONTAINER=$(docker create sqliteviz/sqljs)
docker cp $CONTAINER:/tmp/build/dist .
docker rm $CONTAINER

5
lib/sql-js/package.json Normal file
View File

@@ -0,0 +1,5 @@
{
"name": "sql.js",
"main": "./dist/sql-wasm.js",
"private": true
}

1908
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.11.0", "version": "0.18.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -12,20 +12,24 @@
"dependencies": { "dependencies": {
"codemirror": "^5.57.0", "codemirror": "^5.57.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"debounce": "^1.2.0", "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.0", "papaparse": "^5.3.1",
"pivottable": "^2.23.0",
"plotly.js": "^1.58.4", "plotly.js": "^1.58.4",
"promise-worker": "^2.0.1", "promise-worker": "^2.0.1",
"react": "^16.13.1", "react": "^16.13.1",
"react-chart-editor": "^0.42.0", "react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"sql.js": "^1.5.0", "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"

View 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

View 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

View 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

View 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

View 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

View 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;
}

View File

@@ -1,11 +1,17 @@
.rounded-bg { .rounded-bg {
padding: 40px 5px 5px; padding: 35px 5px 5px;
background-color: white; background-color: white;
border-radius: 5px; border-radius: 5px;
position: relative; position: relative;
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: -40px; 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,
padding: 12px 24px; .sqliteviz-table th,
.fixed-header {
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;

View File

@@ -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);

View File

@@ -0,0 +1,385 @@
<template>
<modal
:name="dialogName"
classes="dialog"
height="auto"
width="80%"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
CSV import
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<text-field
label="Table name"
v-model="tableName"
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-table-name"
/>
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCsv"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
label="Escape char"
hint='
The character used to escape the quote character within a field
(e.g. "column with ""quotes"" in text").
'
max-hint-width="242px"
v-model="escapeChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData
&& (previewData.rowCount > 0 || Object.keys(previewData).length > 0)
"
:data-set="previewData"
class="preview-table"
:preview="true"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
</template>
<script>
import csv from '@/lib/csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import time from '@/lib/utils/time'
import fIo from '@/lib/utils/fileIo'
export default {
name: 'CsvImport',
components: {
CloseIcon,
TextField,
DelimiterSelector,
CheckBox,
SqlTable,
Logs
},
props: ['file', 'db', 'dialogName'],
data () {
return {
disableDialog: false,
tableName: '',
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
importCsvCompleted: false,
importCsvMessages: [],
previewData: null,
addedTable: null,
tableNameError: ''
}
},
watch: {
quoteChar () {
this.previewCsv()
},
escapeChar () {
this.previewCsv()
},
header () {
this.previewCsv()
},
tableName: time.debounce(function () {
this.tableNameError = ''
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`)
this.db.refreshSchema()
}
this.$modal.hide(this.dialogName)
this.$emit('cancel')
}
},
reset () {
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
this.tableName = ''
this.disableDialog = false
this.importCsvCompleted = false
this.importCsvMessages = []
this.previewData = null
this.addedTable = null
this.tableNameError = ''
},
open () {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName)
},
async previewCsv () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
async loadFromCsv (file) {
if (!this.tableName) {
this.tableNameError = "Table name can't be empty"
return
}
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.rowCount
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Add table
start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
end = new Date()
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async finish () {
this.$modal.hide(this.dialogName)
const stmt = [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.',
' */',
`SELECT * FROM "${this.addedTable}"`
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
this.$emit('finish')
}
}
}
</script>
<style scoped>
.dialog-body {
padding-bottom: 0;
}
.chars {
display: flex;
align-items: flex-end;
margin: 24px 0 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 18px;
}
.import-csv-errors {
height: 136px;
margin-top: 8px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 147px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
}
/* https://github.com/euvl/vue-js-modal/issues/623 */
>>> .vm--modal {
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>

View File

@@ -0,0 +1,259 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click="browse"/>
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@drop.prevent="drop"
@click="browse"
>
<div class="text">
Drop the database or CSV file here or click to choose a file from your computer.
</div>
</div>
</div>
<div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'dropping',
'hidden': state === 'dropped'
}"
:src="require('@/assets/images/file.png')"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
<img id="body-img" :src="require('@/assets/images/body.svg')" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
/>
</div>
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<csv-import
ref="addCsv"
:file="file"
:db="newDb"
dialog-name="importFromCsv"
@cancel="cancelCsvImport"
@finish="finish"
/>
</div>
</template>
<script>
import fIo from '@/lib/utils/fileIo'
import ChangeDbIcon from '@/components/svg/changeDb'
import database from '@/lib/database'
import CsvImport from '@/components/CsvImport'
export default {
name: 'DbUploader',
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
return ['illustrated', 'small'].includes(value)
}
},
width: {
type: String,
required: false,
default: 'unset'
}
},
components: {
ChangeDbIcon,
CsvImport
},
data () {
return {
state: '',
animationPromise: Promise.resolve(),
file: null,
newDb: null
}
},
mounted () {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
this.state = 'dropped'
resolve()
}
})
})
}
},
methods: {
cancelCsvImport () {
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
},
async finish () {
this.$store.commit('setDb', this.newDb)
if (this.$route.path !== '/workspace') {
this.$router.push('/workspace')
}
},
loadDb (file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(this.finish)
},
async checkFile (file) {
this.state = 'dropping'
this.newDb = database.getNewDatabase()
if (fIo.isDatabase(file)) {
this.loadDb(file)
} else {
this.file = file
await this.$nextTick()
const csvImport = this.$refs.addCsv
csvImport.reset()
return Promise.all([csvImport.previewCsv(), this.animationPromise])
.then(csvImport.open)
}
},
browse () {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
.then(this.checkFile)
},
drop (event) {
this.checkFile(event.dataTransfer.files[0])
}
}
}
</script>
<style scoped>
.db-uploader-container {
position: relative;
}
.drop-area-container {
display: inline-block;
border: 1px dashed var(--color-border);
padding: 8px;
border-radius: var(--border-radius-big);
height: 100%;
width: 100%;
box-sizing: border-box;
}
.drop-area {
background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
position: absolute;
top: 54px;
left: 50%;
transform: translate(-50%, 0);
width: 450px;
height: 338px;
pointer-events: none;
}
#drop-file-top-img {
width: 450px;
height: 175px;
position: absolute;
top: 0;
left: 0;
}
#drop-file-bottom-img {
width: 450px;
height: 167px;
position: absolute;
bottom: 0;
left: 0;
}
#body-img {
width: 74px;
position: absolute;
top: 94.05px;
left: 46px;
}
#right-arm-img {
width: 106px;
position: absolute;
top: 110.05px;
left: 78px;
}
#left-arm-img {
width: 114px;
position: absolute;
top: 69.05px;
left: 69px;
}
#file-img {
width: 125px;
position: absolute;
top: 15.66px;
left: 152px;
}
.swing {
animation: swing ease-in-out 0.6s infinite alternate;
}
#left-arm-img.swing {
transform-origin: 9px 83px;
}
#right-arm-img.swing {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
}
#file-img.fly {
animation: fly ease-in-out 1s 1 normal;
transform-origin: center center;
}
@keyframes fly {
100% {
transform: rotate(360deg) scale(0.5);
top: 183px;
left: 225px;
}
}
#file-img.hidden {
display: none;
}
</style>

View File

@@ -1,552 +0,0 @@
<template>
<div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click.native="browse"/>
<div v-if="type === 'illustrated'" class="drop-area-container">
<div
class="drop-area"
@dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''"
@drop.prevent="drop"
@click="browse"
>
<div class="text">
Drop the database or CSV file here or click to choose a file from your computer.
</div>
</div>
</div>
<div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" />
<img
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'drop'
}"
:src="require('@/assets/images/file.png')"
/>
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" />
<img id="body-img" :src="require('@/assets/images/body.svg')" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
/>
</div>
<div id="error" class="error"></div>
<!--Parse csv dialog -->
<modal name="parse" classes="dialog" height="auto" width="60%" :clickToClose="false">
<div class="dialog-header">
Import CSV
<close-icon @click="cancelCsvImport" :disabled="disableDialog"/>
</div>
<div class="dialog-body">
<div class="chars">
<delimiter-selector
v-model="delimiter"
width="210px"
class="char-input"
@input="previewCSV"
:disabled="disableDialog"
/>
<text-field
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
/>
<text-field
label="Escape char"
hint='The character used to escape the quote character within a field (e.g. "column with ""quotes"" in text").'
max-hint-width="242px"
v-model="escapeChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
/>
</div>
<check-box
@click="header = $event"
:init="true"
label="Use first row as column headers"
:disabled="disableDialog"
/>
<sql-table
v-if="previewData"
:data-set="previewData"
height="160"
class="preview-table"
:preview="true"
/>
<div v-if="!previewData" class="no-data">No data</div>
<logs
class="import-csv-errors"
:messages="importCsvMessages"
/>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="disableDialog"
@click="cancelCsvImport"
id="csv-cancel"
>
Cancel
</button>
<button
v-show="!importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="loadFromCsv(file)"
id="csv-import"
>
Import
</button>
<button
v-show="importCsvCompleted"
class="primary"
:disabled="disableDialog"
@click="finish"
id="csv-finish"
>
Finish
</button>
</div>
</modal>
</div>
</template>
<script>
import fu from '@/lib/utils/fileIo'
import csv from './csv'
import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField'
import DelimiterSelector from './DelimiterSelector'
import CheckBox from '@/components/CheckBox'
import SqlTable from '@/components/SqlTable'
import Logs from '@/components/Logs'
import ChangeDbIcon from '@/components/svg/changeDb'
import time from '@/lib/utils/time'
import database from '@/lib/database'
const csvMimeTypes = [
'text/csv',
'text/x-csv',
'application/x-csv',
'application/csv',
'text/x-comma-separated-values',
'text/comma-separated-values'
]
export default {
name: 'DbUploader',
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
return ['illustrated', 'small'].includes(value)
}
},
width: {
type: String,
required: false,
default: 'unset'
}
},
components: {
ChangeDbIcon,
TextField,
DelimiterSelector,
CloseIcon,
CheckBox,
SqlTable,
Logs
},
data () {
return {
state: '',
animationPromise: Promise.resolve(),
file: null,
schema: null,
delimiter: '',
quoteChar: '"',
escapeChar: '"',
header: true,
previewData: null,
importCsvMessages: [],
disableDialog: false,
importCsvCompleted: false,
newDb: null
}
},
mounted () {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
resolve()
}
})
})
}
},
watch: {
quoteChar () {
this.previewCSV()
},
escapeChar () {
this.previewCSV()
},
header () {
this.previewCSV()
}
},
methods: {
cancelCsvImport () {
if (!this.disableDialog) {
this.$modal.hide('parse')
if (this.newDb) {
this.newDb.shutDown()
this.newDb = null
}
}
},
async finish () {
this.$store.commit('setDb', this.newDb)
this.$store.commit('saveSchema', this.schema)
if (this.importCsvCompleted) {
this.$modal.hide('parse')
const stmt = [
'/*',
' * Your CSV file has been imported into csv_import table.',
' * You can run this SQL query to make all CSV records available for charting.',
' */',
'SELECT * FROM csv_import'
].join('\n')
const tabId = await this.$store.dispatch('addTab', { query: stmt })
this.$store.commit('setCurrentTabId', tabId)
this.importCsvCompleted = false
}
if (this.$route.path !== '/editor') {
this.$router.push('/editor')
}
},
async previewCSV () {
this.importCsvCompleted = false
const config = {
preview: 3,
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
try {
const start = new Date()
const parseResult = await csv.parse(this.file, config)
const end = new Date()
this.previewData = parseResult.data
this.delimiter = parseResult.delimiter
// In parseResult.messages we can get parse errors
this.importCsvMessages = parseResult.messages || []
if (!parseResult.hasErrors) {
this.importCsvMessages.push({
message: `Preview parsing is completed in ${time.getPeriod(start, end)}.`,
type: 'success'
})
}
} catch (err) {
this.importCsvMessages = [{
message: err,
type: 'error'
}]
}
},
loadDb (file) {
this.newDb = database.getNewDatabase()
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(([schema]) => {
this.schema = schema
this.finish()
})
},
async loadFromCsv (file) {
this.disableDialog = true
const config = {
quoteChar: this.quoteChar || '"',
escapeChar: this.escapeChar,
header: this.header,
delimiter: this.delimiter
}
const parseCsvMsg = {
message: 'Parsing CSV...',
type: 'info'
}
this.importCsvMessages.push(parseCsvMsg)
const parseCsvLoadingIndicator = setTimeout(() => { parseCsvMsg.type = 'loading' }, 1000)
const importMsg = {
message: 'Importing CSV into a SQLite database...',
type: 'info'
}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
}
this.newDb = database.getNewDatabase()
const progressCounterId = this.newDb.createProgressCounter(updateProgress)
try {
let start = new Date()
const parseResult = await csv.parse(this.file, config)
let end = new Date()
if (!parseResult.hasErrors) {
const rowCount = parseResult.data.values.length
let period = time.getPeriod(start, end)
parseCsvMsg.type = 'success'
if (parseResult.messages.length > 0) {
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
parseCsvMsg.message = `${rowCount} rows are parsed in ${period}.`
} else {
// Inform about csv parsing success
parseCsvMsg.message = `${rowCount} rows are parsed successfully in ${period}.`
}
// Loading indicator for csv parsing is not needed anymore
clearTimeout(parseCsvLoadingIndicator)
// Add info about import start
this.importCsvMessages.push(importMsg)
// Show import progress after 1 second
importLoadingIndicator = setTimeout(() => {
importMsg.type = 'loading'
}, 1000)
// Create db with csv table and get schema
const name = file.name.replace(/\.[^.]+$/, '')
start = new Date()
this.schema = await this.newDb.importDb(name, parseResult.data, progressCounterId)
end = new Date()
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing CSV into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
// Loading indicator for import is not needed anymore
clearTimeout(importLoadingIndicator)
this.importCsvCompleted = true
} else {
parseCsvMsg.message = 'Parsing ended with errors.'
parseCsvMsg.type = 'info'
this.importCsvMessages = this.importCsvMessages.concat(parseResult.messages)
}
} catch (err) {
if (parseCsvMsg.type === 'loading') {
parseCsvMsg.type = 'info'
}
if (importMsg.type === 'loading') {
importMsg.type = 'info'
}
this.importCsvMessages.push({
message: err,
type: 'error'
})
}
clearTimeout(parseCsvLoadingIndicator)
clearTimeout(importLoadingIndicator)
this.newDb.deleteProgressCounter(progressCounterId)
this.disableDialog = false
},
async checkFile (file) {
this.state = 'drop'
if (csvMimeTypes.includes(file.type)) {
this.file = file
this.header = true
this.quoteChar = '"'
this.escapeChar = '"'
this.delimiter = ''
return Promise.all([this.previewCSV(), this.animationPromise])
.then(() => {
this.$modal.show('parse')
})
} else {
this.loadDb(file)
}
},
browse () {
fu.getFileFromUser('.db,.sqlite,.sqlite3,.csv')
.then(this.checkFile)
},
drop (event) {
this.checkFile(event.dataTransfer.files[0])
}
}
}
</script>
<style scoped>
.db-uploader-container {
position: relative;
}
.drop-area-container {
display: inline-block;
border: 1px dashed var(--color-border);
padding: 8px;
border-radius: var(--border-radius-big);
height: 100%;
width: 100%;
box-sizing: border-box;
}
.drop-area {
background-color: var(--color-bg-light-3);
border-radius: var(--border-radius-big);
color: var(--color-text-base);
font-size: 13px;
text-align: center;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
cursor: pointer;
}
#img-container {
position: absolute;
top: 54px;
left: 50%;
transform: translate(-50%, 0);
width: 450px;
height: 338px;
pointer-events: none;
}
#drop-file-top-img {
width: 450px;
height: 175px;
position: absolute;
top: 0;
left: 0;
}
#drop-file-bottom-img {
width: 450px;
height: 167px;
position: absolute;
bottom: 0;
left: 0;
}
#body-img {
width: 74px;
position: absolute;
top: 94.05px;
left: 46px;
}
#right-arm-img {
width: 106px;
position: absolute;
top: 110.05px;
left: 78px;
}
#left-arm-img {
width: 114px;
position: absolute;
top: 69.05px;
left: 69px;
}
#file-img {
width: 125px;
position: absolute;
top: 15.66px;
left: 152px;
}
.swing {
animation: swing ease-in-out 0.6s infinite alternate;
}
#left-arm-img.swing {
transform-origin: 9px 83px;
}
#right-arm-img.swing {
transform-origin: 0 56px;
}
#file-img.swing {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
}
#file-img.fly {
animation: fly ease-in-out 1s 1 normal;
transform-origin: center center;
top: 183px;
left: 225px;
transition: top 1s ease-in-out, left 1s ease-in-out;
}
@keyframes fly {
100% { transform: rotate(360deg) scale(0.5); }
}
/* Parse CSV dialog */
.chars {
display: flex;
align-items: flex-end;
margin-bottom: 20px;
}
.char-input {
margin-right: 44px;
}
.preview-table {
margin-top: 32px;
}
.import-csv-errors {
height: 160px;
margin-top: 32px;
}
.no-data {
margin-top: 32px;
background-color: white;
border-radius: 5px;
position: relative;
border: 1px solid var(--color-border-light);
box-sizing: border-box;
height: 160px;
font-size: 13px;
color: var(--color-text-base);
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@@ -0,0 +1,110 @@
<template>
<div
:class="['icon-btn', { active }, { 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>
</div>
</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;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.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);
}
.disabled.icon-btn .icon >>> path,
.disabled.icon-btn .icon >>> circle {
fill: var(--color-border);
}
.disabled.icon-btn {
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>

View File

@@ -0,0 +1,83 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
:clickToClose="false"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon"/>
{{ loadingMsg }}
</div>
<div v-else class="loading-dialog-body">
<img :src="require('@/assets/images/success.svg')" class="success-icon state-icon" />
{{ successMsg }}
</div>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
:disabled="loading"
@click="$emit('cancel')"
>
Cancel
</button>
<button
class="primary"
:disabled="loading"
@click="$emit('action')"
>
{{ actionBtnName }}
</button>
</div>
</modal>
</template>
<script>
import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
props: {
loadingMsg: String,
successMsg: String,
actionBtnName: String,
name: String,
title: String,
loading: Boolean
},
watch: {
loading () {
if (this.loading) {
this.$modal.show(this.name)
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel () {
this.$emit('cancel')
}
}
}
</script>
<style scoped>
.loading-dialog-body {
display: flex;
align-items: center;
}
.success-icon {
width: 30px;
}
.state-icon {
margin-right: 8px;
}
</style>

View File

@@ -1,17 +1,18 @@
<template> <template>
<svg :class="animationClass" height="20" width="20" viewBox="0 0 20 20"> <svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
<circle <circle
class="loader-svg bg" class="loader-svg bg"
cx="10" :style="{ strokeWidth }"
cy="10" :cx="size / 2"
r="8" :cy="size / 2"
:r="radius"
/> />
<circle <circle
class="loader-svg front" class="loader-svg front"
:style="{ strokeDasharray: circleProgress }" :style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
cx="10" :cx="size / 2"
cy="10" :cy="size / 2"
r="8" :r="radius"
/> />
</svg> </svg>
</template> </template>
@@ -19,15 +20,35 @@
<script> <script>
export default { export default {
name: 'LoadingIndicator', name: 'LoadingIndicator',
props: ['progress'], props: {
progress: {
type: Number,
required: false
},
size: {
type: Number,
required: false,
default: 20
}
},
computed: { computed: {
circleProgress () { circleProgress () {
const dash = (50.24 * this.progress) / 100 const circle = this.radius * 3.14 * 2
const space = 50.24 - dash const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
const space = circle - dash
return `${dash}px, ${space}px` return `${dash}px, ${space}px`
}, },
animationClass () { animationClass () {
return this.progress === undefined ? 'loading' : 'progress' return this.progress === undefined ? 'loading' : 'progress'
},
radius () {
return this.size / 2 - this.strokeWidth
},
offset () {
return this.radius * 3.14 / 2
},
strokeWidth () {
return this.size / 10
} }
} }
} }
@@ -38,7 +59,6 @@ export default {
position: absolute; position: absolute;
left: 0; right: 0; top: 0; bottom: 0; left: 0; right: 0; top: 0; bottom: 0;
fill: none; fill: none;
stroke-width: 2px;
stroke-linecap: round; stroke-linecap: round;
stroke: var(--color-accent); stroke: var(--color-accent);
} }
@@ -48,27 +68,30 @@ export default {
} }
.loading .loader-svg.front { .loading .loader-svg.front {
stroke-dasharray: 40.24px; will-change: transform;
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite; animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite;
transform-origin: center;
} }
/*
We can't change anything in loading animation except transform, opacity and filter. Because in
our case the Main Thread can be busy and animation will be frozen (e. g. getting a result set
from the web-worker after query execution).
But transform, opacity and filter trigger changes only in the Composite Layer stage in rendering
waterfall. Hence they can be processed only with Compositor Thread while the Main Thread
processes something else.
https://www.viget.com/articles/animation-performance-101-browser-under-the-hood/
*/
@keyframes fill-animation-loading { @keyframes fill-animation-loading {
0% { 0% {
stroke-dasharray: 10px 40.24px; transform: rotate(0deg);
stroke-dashoffset: 0;
}
50% {
stroke-dasharray: 25.12px;
stroke-dashoffset: 25.12px;
} }
100% { 100% {
stroke-dasharray: 10px 40.24px; transform: rotate(360deg);
stroke-dashoffset: 50.24px;
} }
} }
.progress .loader-svg.front { .progress .loader-svg.front {
stroke-dashoffset: 12.56;
transition: stroke-dasharray 0.2s; transition: stroke-dasharray 0.2s;
} }

View File

@@ -63,14 +63,16 @@ export default {
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--color-border-light); border: 1px solid var(--color-border-light);
box-sizing: border-box; box-sizing: border-box;
overflow-y: scroll; overflow-y: auto;
color: var(--color-text-base);
} }
.msg { .msg {
padding: 16px 7px; padding: 12px 7px;
border-bottom: 1px solid var(--color-border-light); border-bottom: 1px solid var(--color-border-light);
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
font-family: monospace; font-family: monospace;
font-size: 13px;
} }
.msg:last-child { .msg:last-child {

View File

@@ -26,7 +26,10 @@
<div <div
:class="[ :class="[
'toggle-btns', 'toggle-btns',
{'both': after.max === 100 && before.max === 100 && paneAfter.size > 0 && paneBefore.size > 0} {
'both': after.max === 100 && before.max === 100 &&
paneAfter.size > 0 && paneBefore.size > 0
}
]" ]"
> >
<div <div

View File

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

View File

@@ -17,20 +17,21 @@
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">
<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 v-for="(col, colIndex) in columns" :key="colIndex">
<div class="cell-data" :style="cellStyle">{{ value }}</div> <div class="cell-data" :style="cellStyle">
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -39,8 +40,9 @@
</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>
</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>
@@ -53,7 +55,15 @@ import Pager from './Pager'
export default { export default {
name: 'SqlTable', name: 'SqlTable',
components: { Pager }, components: { Pager },
props: ['dataSet', 'height', 'preview'], props: {
dataSet: Object,
time: String,
pageSize: {
type: Number,
default: 20
},
preview: Boolean
},
data () { data () {
return { return {
header: null, header: null,
@@ -63,20 +73,30 @@ export default {
} }
}, },
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
}
} }
}, },
methods: { methods: {

View File

@@ -87,4 +87,7 @@ input.error {
margin-top: 2px; margin-top: 2px;
position: absolute; position: absolute;
} }
.text-field-error:first-letter {
text-transform: uppercase;
}
</style> </style>

View File

@@ -0,0 +1,68 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<g clip-path="url(#clip0)">
<path
fill-rule="evenodd"
clip-rule="evenodd"
:d="
`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
15.1649H9.84V13.6649H8.87866V9.08244H13.6573V9.83777H15.1573V3C15.1573
2.17 14.4873 1.5 13.6573 1.5ZM13.6573
7.58244V3H8.87866V7.58244H13.6573ZM7.37866 3H2.59985V7.58244H7.37866V3ZM2.59985
9.08244V13.6649H7.37866V9.08244H2.59985ZM13.1702
10.8434H15.6702V13.1717H18.0001V15.6717H15.6702V18H13.1702V15.6717H10.8401V13.` +
'1717H13.1702V10.8434Z'
"
fill="#A2B1C6"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="18" height="18" fill="white"/>
</clipPath>
</defs>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Add new table from CSV
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'AddTableIcon',
mixins: [tooltipMixin],
props: ['tooltip'],
methods: {
onClick () {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0;
cursor: pointer;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -6,16 +6,22 @@
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 or CSV
</span> </span>
</div> </div>
@@ -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>

View 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>

View File

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

View File

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

View File

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

View File

@@ -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>

View File

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

View File

@@ -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>

View File

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

View File

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

View File

@@ -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>

View File

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

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

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

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

@@ -0,0 +1,76 @@
import dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js'
import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export function getOptionsForSave (state, dataSources) {
// we don't need to save the data, only settings
// so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state))
const emptySources = {}
for (const key in dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
return stateCopy
}
export async function getImageDataUrl (element, type) {
const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, {
format: type,
width: null,
height: null
})
}
export function getChartData (element) {
const chartElement = element.querySelector('.js-plotly-plot')
return {
data: chartElement.data,
layout: chartElement.layout
}
}
export function getHtml (options) {
const chartId = nanoid()
return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>
<div id="${chartId}"></div>
<script>
const el = document.getElementById("${chartId}")
let timeout
function debounceResize() {
clearTimeout(timeout)
timeout = setTimeout(() => {
var r = el.getBoundingClientRect()
Plotly.relayout(el, {width: r.width, height: r.height})
}, 200)
}
const resizeObserver = new ResizeObserver(debounceResize)
resizeObserver.observe(el)
Plotly.newPlot(el, ${JSON.stringify(options.data)}, ${JSON.stringify(options.layout)})
</script>
`
}
export default {
getOptionsFromDataSources,
getOptionsForSave,
getImageDataUrl,
getHtml,
getChartData
}

View File

@@ -8,20 +8,50 @@ const hintsByCode = {
export default { export default {
getResult (source) { getResult (source) {
const result = {} const result = {
columns: []
}
const values = {}
if (source.meta.fields) { if (source.meta.fields) {
result.columns = source.meta.fields.map(col => col.trim()) source.meta.fields.forEach(col => {
result.values = source.data.map(row => { const colName = col.trim()
const resultRow = [] result.columns.push(colName)
result.columns.forEach(col => { resultRow.push(row[col]) }) values[colName] = source.data.map(row => {
return resultRow let value = row[col]
if (value instanceof Date) {
value = value.toISOString()
}
return value
})
}) })
} else { } else {
result.values = source.data for (let i = 0; i <= source.data[0].length - 1; i++) {
result.columns = [] const colName = `col${i + 1}`
for (let i = 1; i <= source.data[0].length; i++) { result.columns.push(colName)
result.columns.push(`col${i}`) 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 return result
@@ -46,7 +76,8 @@ export default {
const res = { const res = {
data: this.getResult(results), data: this.getResult(results),
delimiter: results.meta.delimiter, delimiter: results.meta.delimiter,
hasErrors: false hasErrors: false,
rowCount: results.data.length
} }
res.messages = results.errors.map(msg => { res.messages = results.errors.map(msg => {
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error' msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
@@ -74,5 +105,9 @@ export default {
Papa.parse(file, { ...defaultConfig, ...config }) Papa.parse(file, { ...defaultConfig, ...config })
}) })
},
serialize (resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
} }
} }

View File

@@ -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 dataSorces = {}
sqlResult.columns.forEach((column, index) => {
dataSorces[column] = sqlResult.values.map(row => row[index])
})
return dataSorces
}
export default class Sql { export default class Sql {
constructor () { constructor () {
this.db = null this.db = null
@@ -36,16 +47,26 @@ 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 (columns, values, progressCounterId, progressCallback, chunkSize = 1500) { import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
if (this.db === null) {
this.createDb() this.createDb()
this.db.exec(dbUtils.getCreateStatement(columns, values)) }
const chunks = dbUtils.generateChunks(values, chunkSize) const columns = data.columns
const chunksAmount = Math.ceil(values.length / chunkSize) const rowCount = data.values[columns[0]].length
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(columns) const insertStr = dbUtils.getInsertStmt(tabName, columns)
const insertStmt = this.db.prepare(insertStr) const insertStmt = this.db.prepare(insertStr)
progressCallback({ progress: 0, id: progressCounterId }) progressCallback({ progress: 0, id: progressCounterId })

View File

@@ -1,25 +1,29 @@
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)
} }
}, },
getInsertStmt (columns) { getInsertStmt (tabName, columns) {
const colList = `"${columns.join('", "')}"` const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ') const params = columns.map(() => '?').join(', ')
return `INSERT INTO csv_import (${colList}) VALUES (${params});` return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
}, },
getCreateStatement (columns, values) { getCreateStatement (tabName, data) {
let result = 'CREATE table csv_import(' 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': {
@@ -37,7 +41,8 @@ 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
} }

View File

@@ -8,10 +8,17 @@ function processMsg (sql) {
switch (data && data.action) { switch (data && data.action) {
case 'open': case 'open':
return sql.open(data.buffer) return sql.open(data.buffer)
case 'reopen':
return sql.open(sql.export())
case 'exec': case 'exec':
return sql.exec(data.sql, data.params) return sql.exec(data.sql, data.params)
case 'import': case 'import':
return sql.import(data.columns, data.values, data.progressCounterId, postMessage) return sql.import(
data.tabName,
data.data,
data.progressCounterId,
postMessage
)
case 'export': case 'export':
return sql.export() return sql.export()
case 'close': case 'close':

View File

@@ -1,4 +1,3 @@
import sqliteParser from 'sqlite-parser'
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/
@@ -20,6 +19,8 @@ export default {
let progressCounterIds = 0 let progressCounterIds = 0
class Database { class Database {
constructor (worker) { constructor (worker) {
this.dbName = null
this.schema = null
this.worker = worker this.worker = worker
this.pw = new PromiseWorker(worker) this.pw = new PromiseWorker(worker)
@@ -50,19 +51,19 @@ class Database {
delete this.importProgresses[id] delete this.importProgresses[id]
} }
async importDb (name, 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
}) })
if (result.error) { if (result.error) {
throw new Error(result.error) throw new Error(result.error)
} }
this.dbName = this.dbName || 'database'
return await this.getSchema(name) this.refreshSchema()
} }
async loadDb (file) { async loadDb (file) {
@@ -73,36 +74,30 @@ class Database {
throw new Error(res.error) throw new Error(res.error)
} }
const dbName = file ? file.name.replace(/\.[^.]+$/, '') : 'database' this.dbName = file ? fu.getFileName(file) : 'database'
return this.getSchema(dbName) this.refreshSchema()
} }
async getSchema (name) { 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: getColumns(item[1])
})
})
}
// Return db name and schema
return {
dbName: name,
schema: parsedSchema
}
} }
async execute (commands) { async execute (commands) {
await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands }) const results = await this.pw.postMessage({ action: 'exec', sql: commands })
if (results.error) { if (results.error) {
@@ -120,48 +115,27 @@ class Database {
} }
fu.exportToFile(data, fileName) fu.exportToFile(data, fileName)
} }
}
function getAst (sql) { async validateTableName (name) {
// There is a bug is sqlite-parser if (name.startsWith('sqlite_')) {
// It throws an error if tokenizer has an arguments: throw new Error("Table name can't start with sqlite_")
// https://github.com/codeschool/sqlite-parser/issues/59
const fixedSql = sql
.replace(/(?<=tokenize=.+)"tokenchars=.+"/, '')
.replace(/(?<=tokenize=.+)"remove_diacritics=.+"/, '')
.replace(/(?<=tokenize=.+)"separators=.+"/, '')
.replace(/tokenize=.+(?=(,|\)))/, 'tokenize=unicode61')
return sqliteParser(fixedSql)
}
/*
* Return an array of columns with name and type. E.g.:
* [
* { name: 'id', type: 'INTEGER' },
* { name: 'title', type: 'NVARCHAR(30)' },
* ]
*/
function getColumns (sql) {
const columns = []
const ast = 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 + ')'
if (/[^\w]/.test(name)) {
throw new Error('Table name can contain only letters, digits and underscores')
} }
columns.push({ name: item.name, type: type })
if (/^(\d)/.test(name)) {
throw new Error("Table name can't start with a digit")
}
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
}
sanitizeTableName (tabName) {
return tabName
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit
.replace(/_{2,}/g, '_') // replace multiple _ with one _
} }
})
return columns
} }

View 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
}
}
}

View File

@@ -0,0 +1,120 @@
import { nanoid } from 'nanoid'
import fu from '@/lib/utils/fileIo'
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) {
const isFromScratch = !inquiryTab.initName
return inquiryTab.isPredefined || isFromScratch
},
save (inquiryTab, newName) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
viewType: inquiryTab.$refs.dataView.mode,
viewOptions: inquiryTab.$refs.dataView.getOptionsForSave(),
name: newName || inquiryTab.initName
}
// Get inquiries from local storage
const myInquiries = this.getStoredInquiries()
// Set createdAt
if (newName) {
value.createdAt = new Date()
} else {
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
value.createdAt = myInquiries[inquiryIndex].createdAt
}
// Insert in inquiries list
if (newName) {
myInquiries.push(value)
} else {
myInquiries[inquiryIndex] = value
}
// Save to local storage
this.updateStorage(myInquiries)
return value
},
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 => {
return this.deserialiseInquiries(str)
})
},
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
}
}
}

View File

@@ -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()
})
}
}

View File

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

View File

@@ -1,9 +1,18 @@
export default { export default {
exportToFile (str, fileName, type = 'octet/stream') { isDatabase (file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type
? dbTypes.includes(file.type)
: /\.(db|sqlite(3)?)+$/.test(file.name)
},
getFileName (file) {
return file.name.replace(/\.[^.]+$/, '')
},
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
@@ -14,6 +23,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

View File

@@ -3,5 +3,19 @@ export default {
const diff = end.getTime() - start.getTime() const diff = end.getTime() - start.getTime()
const seconds = diff / 1000 const seconds = diff / 1000
return seconds.toFixed(3) + 's' return seconds.toFixed(3) + 's'
},
debounce (func, ms) {
let timeout
return function () {
clearTimeout(timeout)
timeout = setTimeout(() => func.apply(this, arguments), ms)
}
},
sleep (ms) {
return new Promise(resolve => {
setTimeout(() => { resolve() }, ms)
})
} }
} }

View File

@@ -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

View File

@@ -1,9 +1,11 @@
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 store from '@/store'
import database from '@/lib/database'
Vue.use(VueRouter) Vue.use(VueRouter)
@@ -19,14 +21,14 @@ 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
} }
] ]
} }
@@ -36,4 +38,13 @@ const router = new VueRouter({
routes routes
}) })
router.beforeEach(async (to, from, next) => {
if (!store.state.db) {
const newDb = database.getNewDatabase()
await newDb.loadDb()
store.commit('setDb', newDb)
}
next()
})
export default router export default router

View File

@@ -5,16 +5,18 @@ export default {
const tab = data ? JSON.parse(JSON.stringify(data)) : {} const tab = data ? JSON.parse(JSON.stringify(data)) : {}
// If no data then create a new blank one... // If no data then create a new blank one...
// No data.id means to create new tab, but not blank, // No data.id means to create new tab, but not blank,
// e.g. with 'select * from csv_import' query after csv import // e.g. with 'select * from csv_import' inquiry after csv import
if (!data || !data.id) { if (!data || !data.id) {
tab.id = nanoid() tab.id = nanoid()
tab.name = null tab.name = null
tab.tempName = state.untitledLastIndex tab.tempName = state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}` ? `Untitled ${state.untitledLastIndex}`
: 'Untitled' : 'Untitled'
tab.isUnsaved = true tab.viewType = 'chart'
tab.viewOptions = undefined
tab.isSaved = false
} else { } else {
tab.isUnsaved = false tab.isSaved = true
} }
// add new tab only if was not already opened // add new tab only if was not already opened

View File

@@ -7,12 +7,8 @@ export default {
} }
state.db = db state.db = db
}, },
saveSchema (state, { dbName, schema }) {
state.dbName = dbName
state.schema = schema
},
updateTab (state, { index, name, id, query, chart, isUnsaved }) { updateTab (state, { index, name, id, query, viewType, viewOptions, isSaved }) {
const tab = state.tabs[index] const tab = state.tabs[index]
const oldId = tab.id const oldId = tab.id
@@ -23,15 +19,17 @@ 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) Vue.set(state.tabs, index, tab)
}, },
deleteTab (state, index) { deleteTab (state, index) {
// If closing tab is the current opened // If closing tab is the current opened
if (state.tabs[index].id === state.currentTabId) { if (state.tabs[index].id === state.currentTabId) {
@@ -53,11 +51,7 @@ export default {
setCurrentTab (state, tab) { setCurrentTab (state, tab) {
state.currentTab = tab state.currentTab = tab
}, },
updatePredefinedQueries (state, queries) { updatePredefinedInquiries (state, inquiries) {
if (Array.isArray(queries)) { state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
state.predefinedQueries = queries
} else {
state.predefinedQueries = [queries]
}
} }
} }

View File

@@ -1,11 +1,8 @@
export default { export default {
schema: null,
dbFile: null,
dbName: null,
tabs: [], tabs: [],
currentTab: null, currentTab: null,
currentTabId: null, currentTabId: null,
untitledLastIndex: 0, untitledLastIndex: 0,
predefinedQueries: [], predefinedInquiries: [],
db: null db: null
} }

View File

@@ -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 () {

View File

@@ -0,0 +1,90 @@
<template>
<div id="app-info-container">
<img
id="app-info-icon"
:src="require('@/assets/images/info.svg')"
@click="$modal.show('app-info')"
/>
<modal name="app-info" classes="dialog" height="auto" width="400px">
<div class="dialog-header">
App info
<close-icon @click="$modal.hide('app-info')"/>
</div>
<div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item">
{{item.name}}
<div class="divider"/>
<div class="options">
<div v-for="(opt, index) in item.info" :key="index">
{{opt}}
</div>
</div>
</div>
</div>
</modal>
</div>
</template>
<script>
import CloseIcon from '@/components/svg/close'
export default {
name: 'AppDiagnosticInfo',
components: { CloseIcon },
data () {
return {
info: [
{
name: 'sqliteviz version',
info: [require('../../../package.json').version]
}
]
}
},
async created () {
const state = this.$store.state
let result = (await state.db.execute('select sqlite_version()')).values
this.info.push({
name: 'SQLite version',
info: result['sqlite_version()']
})
result = (await state.db.execute('PRAGMA compile_options')).values
this.info.push({
name: 'SQLite compile options',
info: result.compile_options
})
}
}
</script>
<style scoped>
#app-info-icon {
cursor: pointer;
}
#app-info-container {
display: flex;
justify-content: center;
margin-left: 32px;
}
.divider {
height: 1px;
background-color: var(--color-border);
margin: 4px 0;
}
.options {
font-family: monospace;
font-size: 13px;
margin-left: 8px;
overflow: auto;
max-height: 170px;
}
.info-item {
margin-bottom: 32px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
</style>

View File

@@ -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
}

View File

@@ -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>

View File

@@ -1,66 +0,0 @@
<template>
<div class="codemirror-container">
<codemirror v-model="query" :options="cmOptions" @changes="onChange" />
</div>
</template>
<script>
import showHint, { showHintOnDemand } from './hint'
import { debounce } from 'debounce'
import { codemirror } from 'vue-codemirror'
import 'codemirror/lib/codemirror.css'
import 'codemirror/mode/sql/sql.js'
import 'codemirror/theme/neo.css'
import 'codemirror/addon/hint/show-hint.css'
import 'codemirror/addon/display/autorefresh.js'
export default {
name: 'SqlEditor',
props: ['value'],
components: { codemirror },
data () {
return {
query: this.value,
cmOptions: {
// codemirror options
tabSize: 4,
mode: 'text/x-mysql',
theme: 'neo',
lineNumbers: true,
line: true,
autofocus: true,
autoRefresh: true,
extraKeys: { 'Ctrl-Space': showHintOnDemand }
}
}
},
watch: {
query () {
this.$emit('input', this.query)
}
},
methods: {
onChange: debounce(showHint, 400)
}
}
</script>
<style scoped>
.codemirror-container {
flex-grow: 1;
min-height: 0;
}
>>> .vue-codemirror {
height: 100%;
max-height: 100%;
}
>>> .CodeMirror {
height: 100%;
max-height: 100%;
}
>>> .CodeMirror-cursor {
width: 1px;
background: var(--color-text-base);
}
</style>

View File

@@ -1,67 +0,0 @@
<template>
<div class="view-switcher">
<div
:class="['table-mode', {'active-mode': view === 'table'}]"
@click="$emit('update:view','table')"
>
Table
</div>
<div
:class="['chart-mode', {'active-mode': view === 'chart'}]"
@click="$emit('update:view','chart')"
>
Chart
</div>
</div>
</template>
<script>
export default {
name: 'ViewSwitcher',
props: ['view']
}
</script>
<style scoped>
.view-switcher {
height: 28px;
display: flex;
padding: 30px;
justify-content: center;
}
.view-switcher div {
height: 100%;
width: 136px;
box-sizing: border-box;
line-height: 28px;
font-size: 12px;
cursor: pointer;
background: var(--color-white);
border: 1px solid var(--color-border);
color: var(--color-text-base);
text-align: center;
font-weight: 400;
}
.view-switcher div:hover {
background-color: var(--color-bg-light);
color: var(--color-text-active);
}
.view-switcher div.active-mode {
background: var(--color-accent);
border: 1px solid var(--color-accent-shade);
color: var(--color-text-light);
text-shadow: var(--shadow);
z-index: 1;
font-weight: 600;
}
.view-switcher div.active-mode:hover {
background: var(--color-accent-shade);
}
.table-mode {
border-radius: var(--border-radius-medium) 0 0 var(--border-radius-medium);
}
.chart-mode {
margin-left: -1px;
border-radius: 0 var(--border-radius-medium) var(--border-radius-medium) 0;
}
</style>

View File

@@ -1,193 +0,0 @@
<template>
<div class="tab-content-container" v-show="isActive">
<splitpanes
class="query-results-splitter"
horizontal
:before="{ size: 50, max: 100 }"
:after="{ size: 50, max: 100 }"
>
<template #left-pane>
<div class="query-editor">
<sql-editor v-model="query" />
</div>
</template>
<template #right-pane>
<div id="bottomPane" ref="bottomPane">
<view-switcher :view.sync="view" />
<div v-show="view === 'table'" class="table-view">
<div
v-show="result === null && !isGettingResults && !error"
class="table-preview result-before"
>
Run your query and get results here
</div>
<div v-show="isGettingResults" class="table-preview result-in-progress">
Fetching results...
</div>
<div
v-show="result === undefined && !isGettingResults && !error"
class="table-preview result-empty"
>
No rows retrieved according to your query
</div>
<div v-show="error" class="table-preview error">
{{ error }}
</div>
<sql-table v-if="result" :data-set="result" :height="tableViewHeight" />
</div>
<chart
:visible="view === 'chart'"
:sql-result="result"
:init-chart="initChart"
ref="chart"
@update="$store.commit('updateTab', { index: tabIndex, isUnsaved: true })"
/>
</div>
</template>
</splitpanes>
</div>
</template>
<script>
import SqlTable from '@/components/SqlTable'
import Splitpanes from '@/components/Splitpanes'
import SqlEditor from './SqlEditor'
import ViewSwitcher from './ViewSwitcher'
import Chart from './Chart'
export default {
name: 'Tab',
props: ['id', 'initName', 'initQuery', 'initChart', 'tabIndex', 'isPredefined'],
components: {
SqlEditor,
SqlTable,
Splitpanes,
ViewSwitcher,
Chart
},
data () {
return {
query: this.initQuery,
result: null,
view: 'table',
tableViewHeight: 0,
isGettingResults: false,
error: null,
resizeObserver: null
}
},
computed: {
isActive () {
return this.id === this.$store.state.currentTabId
}
},
created () {
this.$store.commit('setCurrentTab', this)
},
mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.bottomPane)
this.calculateTableHeight()
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.bottomPane)
},
watch: {
isActive () {
if (this.isActive) {
this.$store.commit('setCurrentTab', this)
}
},
query () {
this.$store.commit('updateTab', { index: this.tabIndex, isUnsaved: true })
}
},
methods: {
// Run a command in the database
async execute () {
this.isGettingResults = true
this.result = null
this.error = null
const state = this.$store.state
try {
this.result = await state.db.execute(this.query + ';')
const schema = await state.db.getSchema(state.dbName)
this.$store.commit('saveSchema', schema)
} catch (err) {
this.error = err
}
this.isGettingResults = false
},
handleResize () {
if (this.view === 'chart') {
// hack react-chart editor: hidden and show in order to make the graph resize
this.view = 'not chart'
this.$nextTick(() => {
this.view = 'chart'
})
}
this.calculateTableHeight()
},
calculateTableHeight () {
const bottomPane = this.$refs.bottomPane
// 88 - view swittcher height
// 42 - table footer width
// 30 - desirable space after the table
// 5 - padding-bottom of rounded table container
// 40 - height of table header
const freeSpace = bottomPane.offsetHeight - 88 - 42 - 30 - 5 - 40
this.tableViewHeight = freeSpace - (freeSpace % 40)
}
}
}
</script>
<style scoped>
.tab-content-container {
background-color: var(--color-white);
border-top: 1px solid var(--color-border-light);
margin-top: -1px;
}
#bottomPane {
height: 100%;
background-color: var(--color-bg-light);
}
.query-results-splitter {
height: calc(100vh - 104px);
background-color: var(--color-bg-light);
}
.query-editor {
display: flex;
flex-direction: column;
height: 100%;
max-height: 100%;
box-sizing: border-box;
min-height: 190px;
}
.table-view {
margin: 0 52px;
height: calc(100% - 88px);
position: relative;
}
.table-preview {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
color: var(--color-text-base);
font-size: 13px;
}
.table-preview.error {
color: var(--color-text-error);
}
.table-preview.error::first-letter {
text-transform: capitalize;
}
</style>

View File

@@ -1,22 +1,22 @@
<template> <template>
<div> <div>
<div id="start-guide" v-if="showedQueries.length === 0"> <div id="start-guide" v-if="allInquiries.length === 0">
You don't have saved queries so far. You don't have saved inquiries so far.
<span class="link" @click="$root.$emit('createNewQuery')">Create</span> <span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
the one from scratch or the one from scratch or
<span @click="importQueries" class="link">import</span> from a file. <span @click="importInquiries" class="link">import</span> from a file.
</div> </div>
<div id="my-queries-content" ref="my-queries-content" v-show="showedQueries.length > 0"> <div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0">
<div id="my-queries-toolbar"> <div id="my-inquiries-toolbar">
<div id="toolbar-buttons"> <div id="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importQueries"> <button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
Import Import
</button> </button>
<button <button
id="toolbar-btns-export" id="toolbar-btns-export"
class="toolbar" class="toolbar"
v-show="selectedQueriesCount > 0" v-show="selectedInquiriesCount > 0"
@click="exportSelectedQueries()" @click="exportSelectedInquiries()"
> >
Export Export
</button> </button>
@@ -24,16 +24,21 @@
id="toolbar-btns-delete" id="toolbar-btns-delete"
class="toolbar" class="toolbar"
v-show="selectedNotPredefinedCount > 0" v-show="selectedNotPredefinedCount > 0"
@click="showDeleteDialog(selectedQueriesIds)" @click="showDeleteDialog(selectedInquiriesIds)"
> >
Delete Delete
</button> </button>
</div> </div>
<div id="toolbar-search"> <div id="toolbar-search">
<text-field placeholder="Search query by name" width="300px" v-model="filter"/> <text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/>
</div> </div>
</div> </div>
<div class="rounded-bg">
<div v-show="showedInquiries.length === 0" id="inquiries-not-found">
No inquiries found
</div>
<div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container"> <div class="header-container">
<div> <div>
<div class="fixed-header" ref="name-th"> <div class="fixed-header" ref="name-th">
@@ -46,48 +51,52 @@
</div> </div>
</div> </div>
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }"> <div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
<table ref="table"> <table ref="table" class="sqliteviz-table">
<tbody> <tbody>
<tr <tr
v-for="(query, index) in showedQueries" v-for="(inquiry, index) in showedInquiries"
:key="query.id" :key="inquiry.id"
@click="openQuery(index)" @click="openInquiry(index)"
> >
<td ref="name-td"> <td ref="name-td">
<div class="cell-data"> <div class="cell-data">
<check-box <check-box
ref="rowCheckBox" ref="rowCheckBox"
:init="selectAll || selectedQueriesIds.has(query.id)" :init="selectAll || selectedInquiriesIds.has(inquiry.id)"
@click="toggleRow($event, query.id)" @click="toggleRow($event, inquiry.id)"
/> />
<div class="name">{{ query.name }}</div> <div class="name">{{ inquiry.name }}</div>
<div <div
v-if="query.isPredefined" v-if="inquiry.isPredefined"
class="badge" class="badge"
@mouseover="showTooltip" @mouseenter="showTooltip"
@mouseout="hideTooltip" @mouseleave="hideTooltip"
> >
Predefined Predefined
<span class="icon-tooltip" :style="tooltipStyle"> <span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Predefined queries come from the server. Predefined inquiries come from the server.
These queries cant be deleted or renamed. These inquiries cant be deleted or renamed.
</span> </span>
</div> </div>
</div> </div>
</td> </td>
<td> <td>
<div class="second-column"> <div class="second-column">
<div class="date-container">{{ query.createdAt | date }}</div> <div class="date-container">{{ inquiry.createdAt | date }}</div>
<div class="icons-container"> <div class="icons-container">
<rename-icon v-if="!query.isPredefined" @click="showRenameDialog(query.id)" /> <rename-icon
<copy-icon @click="duplicateQuery(index)"/> v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)"/>
<export-icon <export-icon
@click="exportToFile([query], `${query.name}.json`)" @click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export query to file" tooltip="Export inquiry to file"
tooltip-position="top-left"
/> />
<delete-icon <delete-icon
v-if="!query.isPredefined" v-if="!inquiry.isPredefined"
@click="showDeleteDialog((new Set()).add(query.id))" @click="showDeleteDialog((new Set()).add(inquiry.id))"
/> />
</div> </div>
</div> </div>
@@ -99,15 +108,15 @@
</div> </div>
</div> </div>
<!--Rename Query dialog --> <!--Rename Inquiry dialog -->
<modal name="rename" classes="dialog" height="auto"> <modal name="rename" classes="dialog" height="auto">
<div class="dialog-header"> <div class="dialog-header">
Rename query Rename inquiry
<close-icon @click="$modal.hide('rename')"/> <close-icon @click="$modal.hide('rename')"/>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<text-field <text-field
label="New query name" label="New inquiry name"
:error-msg="errorMsg" :error-msg="errorMsg"
v-model="newName" v-model="newName"
width="100%" width="100%"
@@ -115,26 +124,26 @@
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button> <button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameQuery">Rename</button> <button class="primary" @click="renameInquiry">Rename</button>
</div> </div>
</modal> </modal>
<!--Delete Query dialog --> <!--Delete Inquiry dialog -->
<modal name="delete" classes="dialog" height="auto"> <modal name="delete" classes="dialog" height="auto">
<div class="dialog-header"> <div class="dialog-header">
Delete {{ deleteGroup ? 'queries' : 'query' }} Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')"/> <close-icon @click="$modal.hide('delete')"/>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
{{ deleteDialogMsg }} {{ deleteDialogMsg }}
<div v-show="selectedQueriesCount > selectedNotPredefinedCount" id="note"> <div v-show="selectedInquiriesCount > selectedNotPredefinedCount" id="note">
<img :src="require('@/assets/images/info.svg')"> <img :src="require('@/assets/images/info.svg')">
Note: Predefined queries you've selected won't be deleted Note: Predefined inquiries you've selected won't be deleted
</div> </div>
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button> <button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteQuery">Delete</button> <button class="primary" @click="deleteInquiry">Delete</button>
</div> </div>
</modal> </modal>
</div> </div>
@@ -149,11 +158,11 @@ import CloseIcon from '@/components/svg/close'
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import CheckBox from '@/components/CheckBox' import CheckBox from '@/components/CheckBox'
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import storedQueries from '@/lib/storedQueries' import storedInquiries from '@/lib/storedInquiries'
import fu from '@/lib/utils/fileIo' import fu from '@/lib/utils/fileIo'
export default { export default {
name: 'MyQueries', name: 'Inquiries',
components: { components: {
RenameIcon, RenameIcon,
CopyIcon, CopyIcon,
@@ -166,13 +175,13 @@ export default {
mixins: [tooltipMixin], mixins: [tooltipMixin],
data () { data () {
return { return {
queries: [], inquiries: [],
filter: null, filter: null,
newName: null, newName: null,
processedQueryId: null, processedInquiryId: null,
errorMsg: null, errorMsg: null,
selectedQueriesIds: new Set(), selectedInquiriesIds: new Set(),
selectedQueriesCount: 0, selectedInquiriesCount: 0,
selectedNotPredefinedCount: 0, selectedNotPredefinedCount: 0,
selectAll: false, selectAll: false,
deleteGroup: false, deleteGroup: false,
@@ -181,61 +190,78 @@ export default {
} }
}, },
computed: { computed: {
predefinedQueries () { predefinedInquiries () {
return this.$store.state.predefinedQueries.map(query => { return this.$store.state.predefinedInquiries.map(inquiry => {
query.isPredefined = true inquiry.isPredefined = true
return query return inquiry
}) })
}, },
predefinedQueriesIds () { predefinedInquiriesIds () {
return new Set(this.predefinedQueries.map(query => query.id)) return new Set(this.predefinedInquiries.map(inquiry => inquiry.id))
}, },
showedQueries () { showedInquiries () {
let showedQueries = this.allQueries let showedInquiries = this.allInquiries
if (this.filter) { if (this.filter) {
showedQueries = showedQueries.filter( showedInquiries = showedInquiries.filter(
query => query.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0 inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
) )
} }
return showedQueries return showedInquiries
}, },
allQueries () {
return this.predefinedQueries.concat(this.queries) allInquiries () {
return this.predefinedInquiries.concat(this.inquiries)
}, },
processedQueryIndex () { processedInquiryIndex () {
return this.queries.findIndex(query => query.id === this.processedQueryId) return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId)
}, },
deleteDialogMsg () { deleteDialogMsg () {
if (!this.deleteGroup && ( if (!this.deleteGroup && (
this.processedQueryIndex === null || this.processedInquiryIndex === null ||
this.processedQueryIndex < 0 || this.processedInquiryIndex < 0 ||
this.processedQueryIndex > this.queries.length this.processedInquiryIndex > this.inquiries.length
)) { )) {
return '' return ''
} }
const deleteItem = this.deleteGroup const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1 ? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
? 'queries' ? 'inquiries'
: 'query'}` : 'inquiry'}`
: `"${this.queries[this.processedQueryIndex].name}"` : `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?` return `Are you sure you want to delete ${deleteItem}?`
} }
}, },
watch: {
showedInquiries () {
this.selectedInquiriesIds = new Set(this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
.filter(id => !this.predefinedInquiriesIds.has(id))).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false
this.selectAll = false
}
}
},
created () { created () {
storedQueries.readPredefinedQueries() storedInquiries.readPredefinedInquiries()
.then(queries => { .then(inquiries => {
this.$store.commit('updatePredefinedQueries', queries) this.$store.commit('updatePredefinedInquiries', inquiries)
}) })
.catch(console.error) .catch(console.error)
.finally(() => { .finally(() => {
this.queries = storedQueries.getStoredQueries() this.inquiries = storedInquiries.getStoredInquiries()
}) })
}, },
mounted () { mounted () {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight) this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
this.resizeObserver.observe(this.$refs['my-queries-content']) this.resizeObserver.observe(this.$refs['my-inquiries-content'])
this.tableResizeObserver = new ResizeObserver(this.calcNameWidth) this.tableResizeObserver = new ResizeObserver(this.calcNameWidth)
this.tableResizeObserver.observe(this.$refs.table) this.tableResizeObserver.observe(this.$refs.table)
@@ -243,7 +269,7 @@ export default {
this.calcMaxTableHeight() this.calcMaxTableHeight()
}, },
beforeDestroy () { beforeDestroy () {
this.resizeObserver.unobserve(this.$refs['my-queries-content']) this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table) this.tableResizeObserver.unobserve(this.$refs.table)
}, },
filters: { filters: {
@@ -263,159 +289,154 @@ export default {
}, },
methods: { methods: {
calcNameWidth () { calcNameWidth () {
const nameWidth = this.$refs['name-td'] const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width ? this.$refs['name-td'][0].getBoundingClientRect().width
: 0 : 0
this.$refs['name-th'].style = `width: ${nameWidth}px` this.$refs['name-th'].style = `width: ${nameWidth}px`
}, },
calcMaxTableHeight () { calcMaxTableHeight () {
const freeSpace = this.$refs['my-queries-content'].offsetHeight - 200 const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1 this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
}, },
openQuery (index) { openInquiry (index) {
const tab = this.showedQueries[index] const tab = this.showedInquiries[index]
this.$store.dispatch('addTab', tab).then(id => { this.$store.dispatch('addTab', tab).then(id => {
this.$store.commit('setCurrentTabId', id) this.$store.commit('setCurrentTabId', id)
this.$router.push('/editor') this.$router.push('/workspace')
}) })
}, },
showRenameDialog (id) { showRenameDialog (id) {
this.errorMsg = null this.errorMsg = null
this.processedQueryId = id this.processedInquiryId = id
this.newName = this.queries[this.processedQueryIndex].name this.newName = this.inquiries[this.processedInquiryIndex].name
this.$modal.show('rename') this.$modal.show('rename')
}, },
renameQuery () { renameInquiry () {
if (!this.newName) { if (!this.newName) {
this.errorMsg = 'Query name can\'t be empty' this.errorMsg = "Inquiry name can't be empty"
return return
} }
const processedQuery = this.queries[this.processedQueryIndex] const processedInquiry = this.inquiries[this.processedInquiryIndex]
processedQuery.name = this.newName processedInquiry.name = this.newName
this.$set(this.queries, this.processedQueryIndex, processedQuery) this.$set(this.inquiries, this.processedInquiryIndex, processedInquiry)
// update queries in local storage // update inquiries in local storage
storedQueries.updateStorage(this.queries) storedInquiries.updateStorage(this.inquiries)
// update tab, if renamed query is opened // update tab, if renamed inquiry is opened
const tabIndex = this.findTabIndex(processedQuery.id) const tabIndex = this.findTabIndex(processedInquiry.id)
if (tabIndex >= 0) { if (tabIndex >= 0) {
this.$store.commit('updateTab', { this.$store.commit('updateTab', {
index: tabIndex, index: tabIndex,
name: this.newName, name: this.newName,
id: processedQuery.id id: processedInquiry.id
}) })
} }
// hide dialog // hide dialog
this.$modal.hide('rename') this.$modal.hide('rename')
}, },
duplicateQuery (index) { duplicateInquiry (index) {
const newQuery = storedQueries.duplicateQuery(this.showedQueries[index]) const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
if (this.selectAll) { this.inquiries.push(newInquiry)
this.selectedQueriesIds.add(newQuery.id) storedInquiries.updateStorage(this.inquiries)
this.selectedQueriesCount = this.selectedQueriesIds.size
}
this.queries.push(newQuery)
storedQueries.updateStorage(this.queries)
}, },
showDeleteDialog (idsSet) { showDeleteDialog (idsSet) {
this.deleteGroup = idsSet.size > 1 this.deleteGroup = idsSet.size > 1
if (!this.deleteGroup) { if (!this.deleteGroup) {
this.processedQueryId = idsSet.values().next().value this.processedInquiryId = idsSet.values().next().value
} }
this.$modal.show('delete') this.$modal.show('delete')
}, },
deleteQuery () { deleteInquiry () {
this.$modal.hide('delete') this.$modal.hide('delete')
if (!this.deleteGroup) { if (!this.deleteGroup) {
this.queries.splice(this.processedQueryIndex, 1) this.inquiries.splice(this.processedInquiryIndex, 1)
// Close deleted query tab if it was opened // Close deleted inquiry tab if it was opened
const tabIndex = this.findTabIndex(this.processedQueryId) const tabIndex = this.findTabIndex(this.processedInquiryId)
if (tabIndex >= 0) { if (tabIndex >= 0) {
this.$store.commit('deleteTab', tabIndex) this.$store.commit('deleteTab', tabIndex)
} }
// Clear checkboxes // Clear checkbox
if (this.selectedQueriesIds.has(this.processedQueryId)) { if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
this.selectedQueriesIds.delete(this.processedQueryId) this.selectedInquiriesIds.delete(this.processedInquiryId)
} }
} else { } else {
this.queries = this.selectAll this.inquiries = this.inquiries.filter(
? [] inquiry => !this.selectedInquiriesIds.has(inquiry.id)
: this.queries.filter(query => !this.selectedQueriesIds.has(query.id)) )
// Close deleted queries if it was opened // Close deleted inquiries if it was opened
const tabs = this.$store.state.tabs const tabs = this.$store.state.tabs
for (let i = tabs.length - 1; i >= 0; i--) { for (let i = tabs.length - 1; i >= 0; i--) {
if (this.selectedQueriesIds.has(tabs[i].id)) { if (this.selectedInquiriesIds.has(tabs[i].id)) {
this.$store.commit('deleteTab', i) this.$store.commit('deleteTab', i)
} }
} }
// Clear checkboxes // Clear checkboxes
this.selectedQueriesIds.clear() this.selectedInquiriesIds.clear()
} }
this.selectedQueriesCount = this.selectedQueriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
storedQueries.updateStorage(this.queries) storedInquiries.updateStorage(this.inquiries)
}, },
findTabIndex (id) { findTabIndex (id) {
return this.$store.state.tabs.findIndex(tab => tab.id === id) return this.$store.state.tabs.findIndex(tab => tab.id === id)
}, },
exportToFile (queryList, fileName) { exportToFile (inquiryList, fileName) {
const jsonStr = storedQueries.serialiseQueries(queryList) const jsonStr = storedInquiries.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName) fu.exportToFile(jsonStr, fileName)
}, },
exportSelectedQueries () { exportSelectedInquiries () {
const queryList = this.selectAll const inquiryList = this.allInquiries.filter(
? this.allQueries inquiry => this.selectedInquiriesIds.has(inquiry.id)
: this.allQueries.filter(query => this.selectedQueriesIds.has(query.id)) )
this.exportToFile(queryList, 'My sqliteviz queries.json') this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
}, },
importQueries () {
storedQueries.importQueries()
.then(importedQueries => {
if (this.selectAll) {
importedQueries.forEach(query => {
this.selectedQueriesIds.add(query.id)
})
this.selectedQueriesCount = this.selectedQueriesIds.size
}
this.queries = this.queries.concat(importedQueries) importInquiries () {
storedQueries.updateStorage(this.queries) storedInquiries.importInquiries()
.then(importedInquiries => {
this.inquiries = this.inquiries.concat(importedInquiries)
storedInquiries.updateStorage(this.inquiries)
}) })
}, },
toggleSelectAll (checked) { toggleSelectAll (checked) {
this.selectAll = checked this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked }) this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.selectedQueriesIds = checked this.selectedInquiriesIds = checked
? new Set(this.allQueries.map(query => query.id)) ? new Set(this.showedInquiries.map(inquiry => inquiry.id))
: new Set() : new Set()
this.selectedQueriesCount = this.selectedQueriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked ? this.queries.length : 0 this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
.length
: 0
}, },
toggleRow (checked, id) { toggleRow (checked, id) {
const isPredefined = this.predefinedQueriesIds.has(id) const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) { if (checked) {
this.selectedQueriesIds.add(id) this.selectedInquiriesIds.add(id)
if (!isPredefined) { if (!isPredefined) {
this.selectedNotPredefinedCount += 1 this.selectedNotPredefinedCount += 1
} }
} else { } else {
if (this.selectedQueriesIds.size === this.allQueries.length) { if (this.selectedInquiriesIds.size === this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false this.$refs.mainCheckBox.checked = false
this.selectAll = false this.selectAll = false
} }
this.selectedQueriesIds.delete(id) this.selectedInquiriesIds.delete(id)
if (!isPredefined) { if (!isPredefined) {
this.selectedNotPredefinedCount -= 1 this.selectedNotPredefinedCount -= 1
} }
} }
this.selectedQueriesCount = this.selectedQueriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
} }
} }
} }
@@ -432,13 +453,22 @@ export default {
text-align: center; text-align: center;
} }
#my-queries-content { #inquiries-not-found {
padding: 35px 5px;
border-radius: 5px;
border: 1px solid var(--color-border-light);
color: var(--color-text-base);
font-size: 14px;
text-align: center;
}
#my-inquiries-content {
padding: 52px; padding: 52px;
height: 100%; height: 100%;
box-sizing: border-box; box-sizing: border-box;
} }
#my-queries-toolbar { #my-inquiries-toolbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-bottom: 18px; margin-bottom: 18px;
@@ -448,6 +478,7 @@ export default {
} }
.rounded-bg { .rounded-bg {
padding-top: 40px;
margin: 0 auto; margin: 0 auto;
max-width: 1500px; max-width: 1500px;
width: 100%; width: 100%;
@@ -464,47 +495,47 @@ export default {
margin-left: 24px; margin-left: 24px;
} }
table { table.sqliteviz-table {
margin-top: 0; margin-top: 0;
} }
tbody tr td { .sqliteviz-table tbody tr td {
min-width: 0; min-width: 0;
height: 40px; height: 40px;
} }
tbody tr td:first-child { .sqliteviz-table tbody tr td:first-child {
width: 70%; width: 70%;
max-width: 0; max-width: 0;
padding: 0 12px; padding: 0 12px;
} }
tbody tr td:last-child { .sqliteviz-table tbody tr td:last-child {
width: 30%; width: 30%;
max-width: 0; max-width: 0;
padding: 0 24px; padding: 0 24px;
} }
tbody .cell-data { .sqliteviz-table tbody .cell-data {
display: flex; display: flex;
align-items: center; align-items: center;
max-width: 100%; max-width: 100%;
width: 100%; width: 100%;
} }
tbody .cell-data div.name { .sqliteviz-table tbody .cell-data div.name {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-left: 24px; margin-left: 24px;
} }
tbody tr:hover td { .sqliteviz-table tbody tr:hover td {
cursor: pointer; cursor: pointer;
} }
tbody tr:hover td { .sqliteviz-table tbody tr:hover td {
color: var(--color-text-active); color: var(--color-text-active);
} }
.second-column { .sqliteviz-table .second-column {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
@@ -522,7 +553,7 @@ tbody tr:hover td {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
tbody tr:hover .icons-container { .sqliteviz-table tbody tr:hover .icons-container {
display: flex; display: flex;
} }
.dialog input { .dialog input {
@@ -544,7 +575,7 @@ button.toolbar {
margin-left: 12px; margin-left: 12px;
} }
tbody tr:hover .badge { .sqliteviz-table tbody tr:hover .badge {
display: block; display: block;
} }
#note { #note {

View File

@@ -0,0 +1,52 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934
4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934
16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107
16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064
15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064
0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Duplicate inquiry
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'CopyIcon',
mixins: [tooltipMixin],
methods: {
onClick () {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip($event, 'top-left')"
@mouseleave="hideTooltip"
>
<path
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064
15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107
15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25
4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Delete inquiry
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'DeleteIcon',
mixins: [tooltipMixin],
methods: {
onClick () {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -0,0 +1,51 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
@click.stop="onClick"
@mouseenter="showTooltip"
@mouseleave="hideTooltip"
>
<path
d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25
12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825
4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545
4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Rename inquiry
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'RenameIcon',
mixins: [tooltipMixin],
methods: {
onClick () {
this.hideTooltip()
this.$emit('click')
}
}
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,51 +1,44 @@
<template> <template>
<nav> <nav>
<div> <div>
<router-link to="/editor">Editor</router-link> <router-link to="/workspace">Workspace</router-link>
<router-link to="/my-queries">My queries</router-link> <router-link to="/inquiries">Inquiries</router-link>
<a href="https://github.com/lana-k/sqliteviz/wiki" target="_blank">Help</a>
</div> </div>
<div> <div id="nav-buttons">
<button
id="run-btn"
v-if="currentQuery && $route.path === '/editor'"
class="primary"
:disabled="runDisabled"
@click="currentQuery.execute"
>
Run
</button>
<button <button
id="save-btn" id="save-btn"
v-show="currentQuery && $route.path === '/editor'" v-show="currentInquiry && $route.path === '/workspace'"
class="primary" class="primary"
:disabled="!isUnsaved" :disabled="isSaved"
@click="checkQueryBeforeSave" @click="checkInquiryBeforeSave"
> >
Save Save
</button> </button>
<button <button
id="create-btn" id="create-btn"
class="primary" class="primary"
@click="createNewQuery" @click="createNewInquiry"
> >
Create Create
</button> </button>
<app-diagnostic-info />
</div> </div>
<!--Save Query dialog --> <!--Save Inquiry dialog -->
<modal name="save" classes="dialog" height="auto"> <modal name="save" classes="dialog" height="auto">
<div class="dialog-header"> <div class="dialog-header">
Save query Save inquiry
<close-icon @click="cancelSave"/> <close-icon @click="cancelSave"/>
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<div v-show="isPredefined" id="save-note"> <div v-show="isPredefined" id="save-note">
<img :src="require('@/assets/images/info.svg')"> <img :src="require('@/assets/images/info.svg')">
Note: Predefined queries can't be edited. Note: Predefined inquiries can't be edited.
That's why your modifications will be saved as a new query. Enter the name for it. That's why your modifications will be saved as a new inquiry. Enter the name for it.
</div> </div>
<text-field <text-field
label="Query name" label="Inquiry name"
:error-msg="errorMsg" :error-msg="errorMsg"
v-model="name" v-model="name"
width="100%" width="100%"
@@ -53,7 +46,7 @@
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button> <button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="saveQuery">Save</button> <button class="primary" @click="saveInquiry">Save</button>
</div> </div>
</modal> </modal>
</nav> </nav>
@@ -62,13 +55,15 @@
<script> <script>
import TextField from '@/components/TextField' import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import storedQueries from '@/lib/storedQueries' import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
export default { export default {
name: 'MainMenu', name: 'MainMenu',
components: { components: {
TextField, TextField,
CloseIcon CloseIcon,
AppDiagnosticInfo
}, },
data () { data () {
return { return {
@@ -77,121 +72,122 @@ export default {
} }
}, },
computed: { computed: {
currentQuery () { currentInquiry () {
return this.$store.state.currentTab return this.$store.state.currentTab
}, },
isUnsaved () { isSaved () {
if (!this.currentQuery) { if (!this.currentInquiry) {
return false return false
} }
const tabIndex = this.currentQuery.tabIndex const tabIndex = this.currentInquiry.tabIndex
const tab = this.$store.state.tabs[tabIndex] const tab = this.$store.state.tabs[tabIndex]
return tab && tab.isUnsaved return tab && tab.isSaved
}, },
isPredefined () { isPredefined () {
if (this.currentQuery) { if (this.currentInquiry) {
return this.currentQuery.isPredefined return this.currentInquiry.isPredefined
} else { } else {
return false return false
} }
}, },
runDisabled () { runDisabled () {
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query) return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
} }
}, },
created () { created () {
this.$root.$on('createNewQuery', this.createNewQuery) this.$root.$on('createNewInquiry', this.createNewInquiry)
this.$root.$on('saveQuery', this.checkQueryBeforeSave) this.$root.$on('saveInquiry', this.checkInquiryBeforeSave)
document.addEventListener('keydown', this._keyListener) document.addEventListener('keydown', this._keyListener)
}, },
beforeDestroy () { beforeDestroy () {
document.removeEventListener('keydown', this._keyListener) document.removeEventListener('keydown', this._keyListener)
}, },
methods: { methods: {
createNewQuery () { createNewInquiry () {
this.$store.dispatch('addTab').then(id => { this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id) this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/editor') { if (this.$route.path !== '/workspace') {
this.$router.push('/editor') this.$router.push('/workspace')
} }
}) })
}, },
cancelSave () { cancelSave () {
this.$modal.hide('save') this.$modal.hide('save')
this.$root.$off('querySaved') this.$root.$off('inquirySaved')
}, },
checkQueryBeforeSave () { checkInquiryBeforeSave () {
this.errorMsg = null this.errorMsg = null
this.name = '' this.name = ''
if (storedQueries.isTabNeedName(this.currentQuery)) { if (storedInquiries.isTabNeedName(this.currentInquiry)) {
this.$modal.show('save') this.$modal.show('save')
} else { } else {
this.saveQuery() this.saveInquiry()
} }
}, },
saveQuery () { saveInquiry () {
const isNeedName = storedQueries.isTabNeedName(this.currentQuery) const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) { if (isNeedName && !this.name) {
this.errorMsg = 'Query name can\'t be empty' this.errorMsg = 'Inquiry name can\'t be empty'
return return
} }
const dataSet = this.currentQuery.result const dataSet = this.currentInquiry.result
const tabView = this.currentQuery.view const tabView = this.currentInquiry.view
// Save query // Save inquiry
const value = storedQueries.save(this.currentQuery, this.name) const value = storedInquiries.save(this.currentInquiry, this.name)
// Update tab in store // Update tab in store
this.$store.commit('updateTab', { this.$store.commit('updateTab', {
index: this.currentQuery.tabIndex, index: this.currentInquiry.tabIndex,
name: value.name, name: value.name,
id: value.id, id: value.id,
query: value.query, query: value.query,
chart: value.chart, viewType: value.viewType,
isUnsaved: false viewOptions: value.viewOptions,
isSaved: true
}) })
// Restore data: // Restore data:
// e.g. if we save predefined query the tab will be created again // e.g. if we save predefined inquiry the tab will be created again
// (because of new id) and // (because of new id) and
// it will be without sql result and has default view - table. // it will be without sql result and has default view - table.
// That's why we need to restore data and view // That's why we need to restore data and view
this.$nextTick(() => { this.$nextTick(() => {
this.currentQuery.result = dataSet this.currentInquiry.result = dataSet
this.currentQuery.view = tabView this.currentInquiry.view = tabView
}) })
// Hide dialog // Hide dialog
this.$modal.hide('save') this.$modal.hide('save')
// Signal about saving // Signal about saving
this.$root.$emit('querySaved') this.$root.$emit('inquirySaved')
}, },
_keyListener (e) { _keyListener (e) {
if (this.$route.path === '/editor') { if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter // Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) { if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()
if (!this.runDisabled) { if (!this.runDisabled) {
this.currentQuery.execute() this.currentInquiry.execute()
} }
return return
} }
// Save query Ctrl+S // Save inquiry Ctrl+S
if (e.key === 's' && (e.ctrlKey || e.metaKey)) { if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()
if (this.isUnsaved) { if (!this.isSaved) {
this.checkQueryBeforeSave() this.checkInquiryBeforeSave()
} }
return return
} }
} }
// New (blank) query Ctrl+B // New (blank) inquiry Ctrl+B
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) { if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
e.preventDefault() e.preventDefault()
this.createNewQuery() this.createNewInquiry()
} }
} }
} }
@@ -212,7 +208,7 @@ nav {
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
padding: 0 52px; padding: 0 16px 0 52px;
z-index: 999; z-index: 999;
} }
a { a {
@@ -237,4 +233,8 @@ button {
#save-note img { #save-note img {
margin: -3px 6px 0 0; margin: -3px 6px 0 0;
} }
#nav-buttons {
display: flex;
}
</style> </style>

View File

@@ -1,42 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="hideTooltip"
>
<path
d="M14.25 15.75H6V5.25H14.25V15.75ZM14.25 3.75H6C5.60218 3.75 5.22064 3.90804 4.93934 4.18934C4.65804 4.47064 4.5 4.85218 4.5 5.25V15.75C4.5 16.1478 4.65804 16.5294 4.93934 16.8107C5.22064 17.092 5.60218 17.25 6 17.25H14.25C14.6478 17.25 15.0294 17.092 15.3107 16.8107C15.592 16.5294 15.75 16.1478 15.75 15.75V5.25C15.75 4.85218 15.592 4.47064 15.3107 4.18934C15.0294 3.90804 14.6478 3.75 14.25 3.75ZM12 0.75H3C2.60218 0.75 2.22064 0.908035 1.93934 1.18934C1.65804 1.47064 1.5 1.85218 1.5 2.25V12.75H3V2.25H12V0.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Duplicate query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'CopyIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,43 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="hideTooltip"
>
<path
d="M6.75 2.25V3H3V4.5H3.75V14.25C3.75 14.6478 3.90804 15.0294 4.18934 15.3107C4.47064 15.592 4.85218 15.75 5.25 15.75H12.75C13.1478 15.75 13.5294 15.592 13.8107 15.3107C14.092 15.0294 14.25 14.6478 14.25 14.25V4.5H15V3H11.25V2.25H6.75ZM5.25 4.5H12.75V14.25H5.25V4.5ZM6.75 6V12.75H8.25V6H6.75ZM9.75 6V12.75H11.25V6H9.75Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Delete query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'DeleteIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

View File

@@ -1,43 +0,0 @@
<template>
<span>
<svg
class="icon"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
@mouseover="showTooltip"
@mouseout="hideTooltip"
>
<path
d="M10.545 6.75L11.25 7.455L4.44 14.25H3.75V13.56L10.545 6.75ZM13.245 2.25C13.0575 2.25 12.8625 2.325 12.72 2.4675L11.3475 3.84L14.16 6.6525L15.5325 5.28C15.825 4.9875 15.825 4.5 15.5325 4.2225L13.7775 2.4675C13.6275 2.3175 13.44 2.25 13.245 2.25ZM10.545 4.6425L2.25 12.9375V15.75H5.0625L13.3575 7.455L10.545 4.6425Z"
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle">
Rename query
</span>
</span>
</template>
<script>
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'RenameIcon',
mixins: [tooltipMixin]
}
</script>
<style scoped>
.icon {
display: block;
margin: 0 12px;
}
.icon:hover path {
fill: var(--color-accent);
}
</style>

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