Compare commits
200 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4213e9df5c | ||
|
|
9f32323a80 | ||
|
|
2ed5160f65 | ||
|
|
fe8ab3f3e8 | ||
|
|
848112979b | ||
|
|
2830df2adc | ||
|
|
5017b55944 | ||
|
|
8d0bc6affe | ||
|
|
d07506266c | ||
|
|
cea1d40797 | ||
|
|
0f2dc9f11e | ||
|
|
23250259eb | ||
|
|
fb930028de | ||
|
|
1ff4adf95c | ||
|
|
78cdb3809c | ||
|
|
3a6628cab9 | ||
|
|
418809d27d | ||
|
|
f9edeafd40 | ||
|
|
a37ed93306 | ||
|
|
cf4b83f7d4 | ||
|
|
2abd42c9c3 | ||
|
|
1251c542cb | ||
|
|
ac89259924 | ||
|
|
179ff8b1e1 | ||
|
|
99a10225a3 | ||
|
|
c96deb5766 | ||
|
|
700970e1cc | ||
|
|
e2be61e2cf | ||
|
|
9c2c8f3692 | ||
|
|
414a116f94 | ||
|
|
3e503f85a9 | ||
|
|
88257bfcf6 | ||
|
|
bdcc494138 | ||
|
|
d750541c80 | ||
|
|
75f743ff9e | ||
|
|
8a9f4b3c0a | ||
|
|
77468d34ae | ||
|
|
a0577ec0ce | ||
|
|
e7d1398546 | ||
|
|
aa52048d51 | ||
|
|
33913f8f5c | ||
|
|
51eb7a543c | ||
|
|
a3fb38b23c | ||
|
|
3bb40b4eb7 | ||
|
|
6864bf84f8 | ||
|
|
9f1b3823f6 | ||
|
|
7574f529c3 | ||
|
|
653f8eff7b | ||
|
|
9b3dda6cff | ||
|
|
d94604ebfb | ||
|
|
16868ef430 | ||
|
|
b162c7043e | ||
|
|
8e856063b8 | ||
|
|
8684b4cef9 | ||
|
|
bcaebd4840 | ||
|
|
4619461af8 | ||
|
|
9fff1d699a | ||
|
|
5ab19c3fae | ||
|
|
cc483f4720 | ||
|
|
a07f2d3d99 | ||
|
|
b9844b8696 | ||
|
|
464bff3db8 | ||
|
|
00e434e142 | ||
|
|
5d6280abec | ||
|
|
7a39e905b9 | ||
|
|
297ea2c18a | ||
|
|
1f2327a724 | ||
|
|
6d512422cf | ||
|
|
8ce9a01372 | ||
|
|
acd56a85cb | ||
|
|
a45e218e3f | ||
|
|
a2bc495259 | ||
|
|
7f4b167dc2 | ||
|
|
5ded99e89f | ||
|
|
a991b02a20 | ||
|
|
9b6aa3d6c7 | ||
|
|
92022f9083 | ||
|
|
15636fed5f | ||
|
|
9ed53e0d25 | ||
|
|
35baaf2722 | ||
|
|
453098b410 | ||
|
|
a469de3674 | ||
|
|
24411ac18f | ||
|
|
a7ef152140 | ||
|
|
97c0c6191b | ||
|
|
9a91dd19bf | ||
|
|
472794e203 | ||
|
|
58d4a7bacd | ||
|
|
d0c624a3cc | ||
|
|
9e29a12ebb | ||
|
|
fdbac941d5 | ||
|
|
628e9cee62 | ||
|
|
803622f18f | ||
|
|
edd45b317a | ||
|
|
6ce5d2b201 | ||
|
|
5c9a5560b7 | ||
|
|
13a8b33815 | ||
|
|
5ba371e339 | ||
|
|
5db8376f95 | ||
|
|
be56079e4e | ||
|
|
a7c8c29624 | ||
|
|
9ce1c27cf5 | ||
|
|
b2d21cc859 | ||
|
|
1945467dc7 | ||
|
|
73c0936927 | ||
|
|
8867092601 | ||
|
|
c7039e144a | ||
|
|
b30eeb6788 | ||
|
|
c2864b4308 | ||
|
|
ba75685a23 | ||
|
|
9314e27d0e | ||
|
|
d4d68bb682 | ||
|
|
05b642bc86 | ||
|
|
62386d3fc0 | ||
|
|
2acfc75da2 | ||
|
|
98cc09741c | ||
|
|
abc852624a | ||
|
|
7791f477cb | ||
|
|
eb950c1b9c | ||
|
|
e60a28ca23 | ||
|
|
4b67fb268b | ||
|
|
8f445ba570 | ||
|
|
0bf5300c9d | ||
|
|
a8e12d64a0 | ||
|
|
aa63aa94ee | ||
|
|
8decda51a4 | ||
|
|
d52f822c15 | ||
|
|
507e978386 | ||
|
|
a457f967c0 | ||
|
|
854d2adfca | ||
|
|
6118a12672 | ||
|
|
7bd8388fd6 | ||
|
|
664c43bce1 | ||
|
|
95d6d6131e | ||
|
|
46757bd5ef | ||
|
|
43cfc8d815 | ||
|
|
6cb0d2d95d | ||
|
|
8e5e2787c7 | ||
|
|
4014aa5730 | ||
|
|
869da1da74 | ||
|
|
3b34f81d56 | ||
|
|
720f23745f | ||
|
|
797430e9f8 | ||
|
|
f6559c1015 | ||
|
|
2974bc6d34 | ||
|
|
f3beeb9f1c | ||
|
|
36f0809546 | ||
|
|
896b8af1a2 | ||
|
|
7e26446573 | ||
|
|
9a7dca3482 | ||
|
|
f6c7c9283e | ||
|
|
840f4fa450 | ||
|
|
d68616b543 | ||
|
|
9e41dab43e | ||
|
|
5006f6935a | ||
|
|
9d38f2a047 | ||
|
|
a787d9afb2 | ||
|
|
82592ed69d | ||
|
|
3bcf0b9ea6 | ||
|
|
cd866fcf50 | ||
|
|
8eb33633e2 | ||
|
|
5c17638a90 | ||
|
|
7550c1e8bd | ||
|
|
82b80e4760 | ||
|
|
6cf22cef09 | ||
|
|
a8f49d9187 | ||
|
|
d45212733d | ||
|
|
f640727d96 | ||
|
|
242b65c446 | ||
|
|
e041e58e7f | ||
|
|
083386383e | ||
|
|
3aa9c144e3 | ||
|
|
ba44a6dfc6 | ||
|
|
8652edc139 | ||
|
|
6886b7903f | ||
|
|
1be139d327 | ||
|
|
86825475f0 | ||
|
|
eb1f71608d | ||
|
|
58401edc35 | ||
|
|
6dede6ee4d | ||
|
|
42199f6ebc | ||
|
|
badcefc4f7 | ||
|
|
b55dec55ee | ||
|
|
8e73e526ec | ||
|
|
fd6d7b8b3a | ||
|
|
6612f4444b | ||
|
|
3168c77c17 | ||
|
|
33a9b261bf | ||
|
|
11721590c1 | ||
|
|
78e803b95d | ||
|
|
8d083a93c4 | ||
|
|
9035cae19f | ||
|
|
e98fa8793b | ||
|
|
ef461be3e5 | ||
|
|
4c2da33d01 | ||
|
|
c3ae115546 | ||
|
|
bbcb553fdc | ||
|
|
08e4f5333d | ||
|
|
dc3979584c | ||
|
|
34764a6e33 |
@@ -12,13 +12,14 @@ module.exports = {
|
||||
},
|
||||
rules: {
|
||||
'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'
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: [
|
||||
'**/__tests__/*.{j,t}s?(x)',
|
||||
'**/tests/unit/**/*.spec.{j,t}s?(x)'
|
||||
'**/tests/**/*.spec.{j,t}s?(x)'
|
||||
],
|
||||
env: {
|
||||
mocha: true
|
||||
|
||||
17
.github/workflows/config.grenrc.js
vendored
Normal 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"]
|
||||
}
|
||||
}
|
||||
26
.github/workflows/main.yml
vendored
@@ -15,6 +15,10 @@ jobs:
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
|
||||
- name: npm install and build
|
||||
run: |
|
||||
npm install
|
||||
@@ -23,18 +27,28 @@ jobs:
|
||||
- name: Create archive
|
||||
run: |
|
||||
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
|
||||
uses: ncipollo/release-action@v1
|
||||
with:
|
||||
artifacts: "dist/dist.zip"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
bodyFile: "CHANGELOG.md"
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: JamesIves/github-pages-deploy-action@3.6.2
|
||||
uses: JamesIves/github-pages-deploy-action@4.1.1
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BRANCH: build # The branch the action should deploy to.
|
||||
FOLDER: dist/ # The folder the action should deploy.
|
||||
CLEAN: false # Automatically remove deleted files from the deploy branch
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
branch: build # The branch the action should deploy to.
|
||||
folder: dist/ # The folder the action should deploy.
|
||||
clean: true # Automatically remove deleted files from the deploy branch
|
||||
clean-exclude: .nojekyll
|
||||
|
||||
|
||||
37
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
name: Run tests
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10.x
|
||||
- name: Install browsers
|
||||
run: |
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y chromium-browser firefox
|
||||
|
||||
- name: Update npm
|
||||
run: npm install -g npm@7
|
||||
|
||||
- name: Install the project
|
||||
run: npm install
|
||||
|
||||
- name: Run lint
|
||||
run: npm run lint -- --no-fix
|
||||
|
||||
- name: Run karma tests
|
||||
run: npm run test
|
||||
37
README.md
@@ -4,22 +4,39 @@
|
||||
|
||||
# sqliteviz
|
||||
|
||||
**Note: This is a work-in-progress project.**
|
||||
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation of SQLite databases or CSV files.
|
||||
|
||||
Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases.
|
||||
With sqliteviz you can:
|
||||
- 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
|
||||
- manage inquiries and run them against different databases
|
||||
- import/export inquiries to/from a JSON file
|
||||
- export a modified SQLite database
|
||||
- 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
|
||||
The latest release of sqliteviz is deployed on GitHub Pages at [lana-k.github.io/sqliteviz][6].
|
||||
|
||||
## Wiki
|
||||
For user documentation, check out sqliteviz [Wiki][7].
|
||||
|
||||
## Motivation
|
||||
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
|
||||
It is built on top of [react-chart-editor][3] and [sql.js][4] in [Vue.js][5].
|
||||
|
||||
## Features
|
||||
|
||||
1. Ad-hoc and prepared visualisation of a SQLite database using flexibility
|
||||
of SQL and richness of Plotly's `react-chart-editor`
|
||||
2. Visualisation pallete, SQL and chart definitions, kept in local
|
||||
storage with import/export
|
||||
3. Integration-side provided pallete
|
||||
## Components
|
||||
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
|
||||
[2]: https://github.com/getredash/redash
|
||||
[3]: https://github.com/plotly/react-chart-editor
|
||||
[4]: https://github.com/sql-js/sql.js
|
||||
[5]: https://github.com/vuejs/vue
|
||||
[6]: https://lana-k.github.io/sqliteviz/
|
||||
[7]: https://github.com/lana-k/sqliteviz/wiki
|
||||
[8]: https://github.com/surmon-china/vue-codemirror#readme
|
||||
[9]: https://www.papaparse.com/
|
||||
[10]: https://github.com/lana-k/sqliteviz/wiki/Predefined-queries
|
||||
[11]: https://github.com/plotly/plotly.js
|
||||
[12]: https://github.com/nicolaskruchten/pivottable
|
||||
|
||||
119
karma.conf.js
@@ -1,31 +1,26 @@
|
||||
// Karma configuration
|
||||
"use strict";
|
||||
const path = require("path");
|
||||
const VueLoaderPlugin = require("vue-loader/lib/plugin");
|
||||
'use strict'
|
||||
const path = require('path')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
|
||||
function resolve(dir) {
|
||||
return path.join(__dirname, dir);
|
||||
function resolve (dir) {
|
||||
return path.join(__dirname, dir)
|
||||
}
|
||||
|
||||
module.exports = function(config) {
|
||||
module.exports = function (config) {
|
||||
config.set({
|
||||
// base path that will be used to resolve all patterns (eg. files, exclude)
|
||||
basePath: "",
|
||||
basePath: '',
|
||||
|
||||
// frameworks to use
|
||||
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
frameworks: ["mocha", "sinon-chai"],
|
||||
frameworks: ['mocha', 'sinon-chai'],
|
||||
|
||||
// list of files / patterns to load in the browser
|
||||
files: [
|
||||
"tests/unit/*.spec.js",
|
||||
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
nocache: false
|
||||
},
|
||||
{ pattern: 'node_modules/sql.js/dist/worker.sql-wasm.js',
|
||||
'./karma.files.js',
|
||||
{
|
||||
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
watched: false,
|
||||
included: false,
|
||||
served: true,
|
||||
@@ -39,29 +34,29 @@ module.exports = function(config) {
|
||||
// preprocess matching files before serving them to the browser
|
||||
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
preprocessors: {
|
||||
"tests/unit/*.spec.js": ["webpack"]
|
||||
'./karma.files.js': ['webpack']
|
||||
},
|
||||
|
||||
// test results reporter to use
|
||||
// possible values: 'dots', 'progress'
|
||||
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
reporters: ["spec", "coverage"],
|
||||
reporters: ['spec', 'coverage'],
|
||||
|
||||
coverageReporter: {
|
||||
dir: "coverage",
|
||||
reporters: [{ type: "lcov", subdir: "." }, { type: "text-summary" }]
|
||||
dir: 'coverage',
|
||||
reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
|
||||
},
|
||||
|
||||
// !!DONOT delete this reporter, or vue-cli-addon-ui-karma doesnot work
|
||||
jsonResultReporter: {
|
||||
outputFile: "report/karma-result.json",
|
||||
outputFile: 'report/karma-result.json',
|
||||
isSynchronous: true
|
||||
},
|
||||
|
||||
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
|
||||
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
|
||||
nameFormatter: undefined, // 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
|
||||
@@ -79,11 +74,19 @@ module.exports = function(config) {
|
||||
logLevel: config.LOG_INFO,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: true,
|
||||
autoWatch: false,
|
||||
|
||||
customLaunchers: {
|
||||
FirefoxHeadlessTouch: {
|
||||
base: 'FirefoxHeadless',
|
||||
prefs: {
|
||||
'dom.w3c_touch_events.enabled': 1
|
||||
}
|
||||
}
|
||||
},
|
||||
// start these browsers
|
||||
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
browsers: ["ChromiumHeadless"],
|
||||
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
|
||||
|
||||
// Continuous Integration mode
|
||||
// if true, Karma captures browsers, runs the tests and exits
|
||||
@@ -91,23 +94,26 @@ module.exports = function(config) {
|
||||
|
||||
// Concurrency level
|
||||
// how many browser should be started simultaneous
|
||||
concurrency: Infinity,
|
||||
concurrency: 2,
|
||||
|
||||
client: {
|
||||
captureConsole: true
|
||||
captureConsole: true,
|
||||
mocha: {
|
||||
timeout: 7000
|
||||
}
|
||||
},
|
||||
browserConsoleLogOptions: {
|
||||
terminal: true,
|
||||
level: ""
|
||||
level: ''
|
||||
},
|
||||
webpack: {
|
||||
mode: "development",
|
||||
entry: "./src/main.js",
|
||||
mode: 'development',
|
||||
entry: './src/main.js',
|
||||
resolve: {
|
||||
extensions: [".js", ".vue", ".json"],
|
||||
extensions: ['.js', '.vue', '.json'],
|
||||
alias: {
|
||||
vue$: "vue/dist/vue.esm.js",
|
||||
"@": resolve("src")
|
||||
vue$: 'vue/dist/vue.esm.js',
|
||||
'@': resolve('src')
|
||||
}
|
||||
},
|
||||
module: {
|
||||
@@ -117,46 +123,57 @@ module.exports = function(config) {
|
||||
exclude: /(node_modules|bower_components)/,
|
||||
use: [
|
||||
{
|
||||
loader: "babel-loader"
|
||||
},
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
include: /src/,
|
||||
exclude: /(node_modules|bower_components|\.spec\.js$)/,
|
||||
use: [
|
||||
{
|
||||
loader: "istanbul-instrumenter-loader",
|
||||
loader: 'istanbul-instrumenter-loader',
|
||||
options: {
|
||||
esModules: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /worker\.js$/,
|
||||
loader: 'worker-loader'
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
|
||||
loader: "url-loader"
|
||||
loader: 'url-loader'
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: "vue-loader",
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
loaders: {
|
||||
js: "babel-loader"
|
||||
js: 'babel-loader'
|
||||
},
|
||||
postLoaders: {
|
||||
js: "istanbul-instrumenter-loader?esModules=true"
|
||||
js: 'istanbul-instrumenter-loader?esModules=true'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["vue-style-loader", "css-loader"]
|
||||
use: ['vue-style-loader', 'css-loader']
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
use: ["vue-style-loader", "css-loader", "sass-loader"]
|
||||
use: ['vue-style-loader', 'css-loader', 'sass-loader']
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
loader: "url-loader",
|
||||
loader: 'url-loader',
|
||||
options: {
|
||||
limit: 10000,
|
||||
name: resolve("fonts/[name].[hash:7].[ext]")
|
||||
name: resolve('fonts/[name].[hash:7].[ext]')
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -166,8 +183,16 @@ module.exports = function(config) {
|
||||
fs: 'empty'
|
||||
}
|
||||
},
|
||||
webpackMiddleware: {
|
||||
watchOptions: {
|
||||
ignored: /node_modules/
|
||||
}
|
||||
},
|
||||
proxies: {
|
||||
"/js/": "/base/node_modules/sql.js/dist/"
|
||||
'/_karma_webpack_/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm',
|
||||
'/base/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
|
||||
}
|
||||
});
|
||||
};
|
||||
})
|
||||
// Fix the timezone
|
||||
process.env.TZ = 'Europe/Amsterdam'
|
||||
}
|
||||
|
||||
21
karma.files.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import Vue from 'vue'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
|
||||
Vue.use(VuePlugin)
|
||||
Vue.use(VModal)
|
||||
Vue.config.productionTip = false
|
||||
|
||||
// require all test files (files that ends with .spec.js)
|
||||
const testsContext = require.context('./tests', true, /\.spec.js$/)
|
||||
|
||||
// Read more about why we need to call testContext:
|
||||
// https://www.npmjs.com/package/require-context#context-api
|
||||
testsContext.keys().forEach(testsContext)
|
||||
|
||||
// require all src files except main.js and router/index.js for coverage.
|
||||
// you can also change this to match only the subset of files that
|
||||
// you want coverage for.
|
||||
// We don't include router/index.js to avoid installing VueRouter globally in tests
|
||||
const srcContext = require.context('./src', true, /^\.\/(?!(main|(router(\/)?(index)?))(\.js)?$)/)
|
||||
srcContext.keys().forEach(srcContext)
|
||||
2
lib/sql-js/.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
benchmark
|
||||
dist
|
||||
9
lib/sql-js/Dockerfile
Normal 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
|
||||
73
lib/sql-js/README.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# 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
|
||||
|
||||
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])
|
||||
|
||||
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.
|
||||
|
||||
## 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
|
||||
4
lib/sql-js/benchmark/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/lib/build-*
|
||||
/lib/dist
|
||||
/build-*-result.json
|
||||
/sample.csv
|
||||
19
lib/sql-js/benchmark/Dockerfile
Normal 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
|
||||
14
lib/sql-js/benchmark/README.md
Normal 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.
|
||||
52
lib/sql-js/benchmark/karma.conf.js
Normal file
@@ -0,0 +1,52 @@
|
||||
module.exports = function (config) {
|
||||
const timeout = 15 * 60 * 1000
|
||||
config.set({
|
||||
|
||||
frameworks: ['mocha'],
|
||||
|
||||
files: [
|
||||
'suite.js',
|
||||
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm', served: true, included: false },
|
||||
{ pattern: 'sample.csv', served: true, included: false }
|
||||
],
|
||||
|
||||
reporters: ['progress', 'json-to-file'],
|
||||
|
||||
singleRun: true,
|
||||
|
||||
customLaunchers: {
|
||||
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] }
|
||||
},
|
||||
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
|
||||
concurrency: 1,
|
||||
|
||||
browserDisconnectTimeout: timeout,
|
||||
browserNoActivityTimeout: timeout,
|
||||
captureTimeout: timeout,
|
||||
browserSocketTimeout: timeout,
|
||||
pingTimeout: timeout,
|
||||
client: {
|
||||
captureConsole: true,
|
||||
mocha: { timeout: timeout }
|
||||
},
|
||||
|
||||
logLevel: config.LOG_INFO,
|
||||
browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
|
||||
|
||||
preprocessors: { 'suite.js': [ 'webpack' ] },
|
||||
webpack: {
|
||||
mode: 'development',
|
||||
module: {
|
||||
noParse: [ __dirname + '/node_modules/benchmark/benchmark.js' ]
|
||||
},
|
||||
node: { fs: 'empty' }
|
||||
},
|
||||
|
||||
proxies: {
|
||||
'/sql-wasm.wasm': '/base/node_modules/sql.js/dist/sql-wasm.wasm'
|
||||
},
|
||||
|
||||
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
|
||||
|
||||
})
|
||||
}
|
||||
5
lib/sql-js/benchmark/lib/package.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sql.js",
|
||||
"main": "./dist/sql-wasm.js",
|
||||
"private": true
|
||||
}
|
||||
21
lib/sql-js/benchmark/make.sh
Executable 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
|
||||
23
lib/sql-js/benchmark/package.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "sqlite-webassembly-microbenchmark",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core" : "^7.14.8",
|
||||
"babel-loader": "^8.2.2",
|
||||
"benchmark": "^2.1.4",
|
||||
"lodash": "^4.17.4",
|
||||
"papaparse": "^5.3.1",
|
||||
"mocha": "^9.0.3",
|
||||
"karma": "^6.3.4",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-firefox-launcher": "^2.1.1",
|
||||
"karma-json-to-file-reporter" : "^1.0.1",
|
||||
"karma-mocha": "^2.0.1",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"webpack": "^4.46.0",
|
||||
"sql.js": "file:./lib"
|
||||
},
|
||||
"scripts": {
|
||||
"benchmark": "karma start karma.conf.js"
|
||||
}
|
||||
}
|
||||
207
lib/sql-js/benchmark/result-analysis.ipynb
Normal file
140
lib/sql-js/benchmark/suite.js
Normal file
@@ -0,0 +1,140 @@
|
||||
import benchmark from 'benchmark'
|
||||
import initSqlJs from 'sql.js'
|
||||
import lodash from 'lodash'
|
||||
import Papa from 'papaparse'
|
||||
import useragent from 'ua-parser-js'
|
||||
|
||||
|
||||
describe('SQLite build benchmark', function () {
|
||||
let parsedCsv
|
||||
let sqlModule
|
||||
let selectDb
|
||||
|
||||
before(async function () {
|
||||
parsedCsv = await parseCsv('http://localhost:9876/base/sample.csv')
|
||||
sqlModule = await initSqlJs()
|
||||
|
||||
selectDb = new sqlModule.Database()
|
||||
importToTable(selectDb, parsedCsv)
|
||||
})
|
||||
|
||||
function benchmarkImport () {
|
||||
const db = new sqlModule.Database()
|
||||
try {
|
||||
importToTable(db, parsedCsv)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
||||
function benchmarkSelect () {
|
||||
const result = selectDb.exec(`
|
||||
SELECT county, AVG(avg_depth) avg_depth_c
|
||||
FROM (
|
||||
SELECT s.county, s.town, COUNT(*) cnt, AVG(s.DrilledDepth) avg_depth
|
||||
FROM csv_import s
|
||||
JOIN csv_import USING(hole)
|
||||
WHERE s.town IS NOT NULL
|
||||
GROUP BY 1, 2
|
||||
ORDER BY 4 DESC
|
||||
)
|
||||
GROUP BY 1
|
||||
ORDER BY 2 DESC
|
||||
`)
|
||||
console.assert(result.values.length == 56, 'Unexpected size of result set')
|
||||
}
|
||||
|
||||
it('run', async function () {
|
||||
const suite = createSuite()
|
||||
suite.add('import', { initCount: 3, minSamples: 50, fn: benchmarkImport })
|
||||
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
|
||||
await run(suite)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
|
||||
function importToTable (db, parsedCsv, chunkSize = 1024) {
|
||||
const columnListString = parsedCsv.meta.fields.join(', ')
|
||||
db.exec(`CREATE TABLE csv_import(${columnListString})`)
|
||||
|
||||
const params = parsedCsv.meta.fields.map(name => '?').join(', ')
|
||||
const insertStmt = db.prepare(`INSERT INTO csv_import VALUES(${params})`)
|
||||
chunkArray(parsedCsv.data, chunkSize).map(function (chunk) {
|
||||
db.exec('BEGIN')
|
||||
chunk.map(row => insertStmt.run(Object.values(row)))
|
||||
db.exec('COMMIT')
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
class PromiseWrapper {
|
||||
constructor() {
|
||||
this.promise = new Promise((resolve, reject) => {
|
||||
this.reject = reject
|
||||
this.resolve = resolve
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function parseCsv(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Papa.parse(url, {
|
||||
header: true,
|
||||
download: true,
|
||||
skipEmptyLines: 'greedy',
|
||||
complete: results => resolve(results),
|
||||
error: (error, file) => reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function chunkArray (arr, size) {
|
||||
return arr.reduce(function (result, value, index) {
|
||||
const chunkIndex = Math.floor(index / size)
|
||||
|
||||
if(!(chunkIndex in result)) {
|
||||
result[chunkIndex] = []
|
||||
}
|
||||
result[chunkIndex].push(value)
|
||||
|
||||
return result
|
||||
}, [])
|
||||
}
|
||||
|
||||
|
||||
function createSuite () {
|
||||
// Combined workaround from:
|
||||
// - https://github.com/bestiejs/benchmark.js/issues/106
|
||||
// - https://github.com/bestiejs/benchmark.js/issues/237
|
||||
|
||||
// Benchmark could not pick up lodash otherwise
|
||||
const bm = benchmark.runInContext({ _: lodash })
|
||||
// Avoid `ReferenceError: Benchmark is not defined` error because Benchmark is assumed
|
||||
// to be in window
|
||||
window.Benchmark = bm
|
||||
|
||||
return new bm.Suite()
|
||||
}
|
||||
|
||||
function run (suite) {
|
||||
const suiteResult = new PromiseWrapper()
|
||||
suite
|
||||
.on('cycle', function (event) {
|
||||
console.info(String(event.target))
|
||||
})
|
||||
.on('complete', function () {
|
||||
console.log(JSON.stringify({
|
||||
browser: useragent(navigator.userAgent).browser,
|
||||
result: this.filter('successful')
|
||||
}))
|
||||
suiteResult.resolve()
|
||||
})
|
||||
.on('error', function (event) {
|
||||
console.error('Benchmark failed', String(event.target))
|
||||
suiteResult.reject()
|
||||
})
|
||||
.run({async: true})
|
||||
|
||||
return suiteResult.promise
|
||||
}
|
||||
86
lib/sql-js/build.py
Normal 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)
|
||||
105
lib/sql-js/configure.py
Normal file
@@ -0,0 +1,105 @@
|
||||
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'),
|
||||
# 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
BIN
lib/sql-js/dist/sql-wasm.wasm
vendored
Executable file
9
lib/sql-js/make.sh
Executable 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
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "sql.js",
|
||||
"main": "./dist/sql-wasm.js",
|
||||
"private": true
|
||||
}
|
||||
5124
package-lock.json
generated
26
package.json
@@ -1,29 +1,35 @@
|
||||
{
|
||||
"name": "sqliteviz",
|
||||
"version": "0.8.0",
|
||||
"version": "0.15.2",
|
||||
"license": "Apache-2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"serve": "vue-cli-service serve",
|
||||
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
|
||||
"test:unit": "vue-cli-service test:unit",
|
||||
"test": "vue-cli-service karma",
|
||||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"codemirror": "^5.57.0",
|
||||
"core-js": "^3.6.5",
|
||||
"debounce": "^1.2.0",
|
||||
"html2canvas": "^1.1.4",
|
||||
"jquery": "^3.6.0",
|
||||
"nanoid": "^3.1.12",
|
||||
"plotly.js": "^1.57.1",
|
||||
"papaparse": "^5.3.1",
|
||||
"pivottable": "^2.23.0",
|
||||
"plotly.js": "^1.58.4",
|
||||
"promise-worker": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-chart-editor": "^0.42.0",
|
||||
"react-chart-editor": "^0.45.0",
|
||||
"react-dom": "^16.13.1",
|
||||
"sql.js": "^1.3.0",
|
||||
"sql.js": "file:./lib/sql-js",
|
||||
"sqlite-parser": "^1.0.1",
|
||||
"vue": "^2.6.11",
|
||||
"vue-codemirror": "^4.0.6",
|
||||
"vue-js-modal": "^2.0.0-rc.6",
|
||||
"vue-multiselect": "^2.1.6",
|
||||
"vue-router": "^3.2.0",
|
||||
"vue2-teleport": "^1.0.1",
|
||||
"vuejs-paginate": "^2.1.0",
|
||||
"vuera": "^0.2.7",
|
||||
"vuex": "^3.4.0"
|
||||
@@ -35,9 +41,10 @@
|
||||
"@vue/cli-plugin-vuex": "^4.4.0",
|
||||
"@vue/cli-service": "^4.4.0",
|
||||
"@vue/eslint-config-standard": "^5.1.2",
|
||||
"@vue/test-utils": "^1.0.3",
|
||||
"@vue/test-utils": "^1.1.2",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"eslint": "^6.7.2",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
@@ -45,8 +52,11 @@
|
||||
"eslint-plugin-standard": "^4.0.0",
|
||||
"eslint-plugin-vue": "^6.2.2",
|
||||
"karma": "^3.1.4",
|
||||
"karma-firefox-launcher": "^2.1.0",
|
||||
"karma-webpack": "^4.0.2",
|
||||
"vue-cli-plugin-ui-karma": "^0.2.5",
|
||||
"vue-template-compiler": "^2.6.11"
|
||||
"vue-template-compiler": "^2.6.11",
|
||||
"workbox-webpack-plugin": "^6.1.5",
|
||||
"worker-loader": "^3.0.8"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/Logo192x192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/Logo48x48.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
public/Logo512x512.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 798 B After Width: | Height: | Size: 774 B |
@@ -5,13 +5,94 @@
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<link rel="icon" href="<%= BASE_URL %>favicon.png">
|
||||
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest">
|
||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||
<style>
|
||||
#sqliteviz-loading-wrapper {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-text {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #506784;
|
||||
font-family: sans-serif;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper svg {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
stroke-width: 5px;
|
||||
stroke-linecap: round;
|
||||
stroke: #119DFF;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle.bg {
|
||||
stroke: #C8D4E3;
|
||||
}
|
||||
|
||||
#sqliteviz-loading-wrapper circle.front {
|
||||
stroke-dasharray: 402px;
|
||||
animation: sqliteviz-loading 2s linear 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes sqliteviz-loading {
|
||||
0% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 251px;
|
||||
stroke-dashoffset: -251px;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 100px 402px;
|
||||
stroke-dashoffset: -502px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||
</noscript>
|
||||
<div id="app"></div>
|
||||
<div id="app">
|
||||
<div id="sqliteviz-loading-wrapper">
|
||||
<div id="sqliteviz-loading-text">LOADING</div>
|
||||
<svg height="170" width="170" viewBox="0 0 170 170">
|
||||
<circle
|
||||
class="bg"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
/>
|
||||
<circle
|
||||
class="front"
|
||||
cx="85"
|
||||
cy="85"
|
||||
r="80"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<!-- built files will be auto injected -->
|
||||
</body>
|
||||
</html>
|
||||
|
||||
1
public/inquiries.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
30
public/manifest.webmanifest
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"background_color": "white",
|
||||
"description": "Sqliteviz is a single-page application for fully client-side visualisation of SQLite databases or CSV.",
|
||||
"display": "fullscreen",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "32x32",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo48x48.png",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "Logo512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"name": "sqliteviz",
|
||||
"short_name": "sqliteviz",
|
||||
"start_url": "index.html"
|
||||
}
|
||||
@@ -60,4 +60,7 @@ button,
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
.CodeMirror-hints {
|
||||
z-index: 999 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
BIN
src/assets/images/Screenshot_editor.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
3
src/assets/images/arrow-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.51581 7.54801C4.95181 7.10201 5.55881 7.06701 6.09181 7.54801L9.99981 11.295L13.9078 7.54801C14.4408 7.06701 15.0488 7.10201 15.4818 7.54801C15.9178 7.99301 15.8898 8.74501 15.4818 9.16301C15.0758 9.58101 10.7868 13.665 10.7868 13.665C10.5698 13.888 10.2848 14 9.99981 14C9.71481 14 9.42981 13.888 9.21081 13.665C9.21081 13.665 4.92381 9.58101 4.51581 9.16301C4.10781 8.74501 4.07981 7.99301 4.51581 7.54801V7.54801Z" fill="#119DFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 550 B |
3
src/assets/images/arrow.svg
Normal file
@@ -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 |
17
src/assets/images/checkbox_checked_disabled.svg
Normal file
@@ -0,0 +1,17 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="17" height="17" rx="2.5" fill="#DFE8F3" stroke="#C8D4E3"/>
|
||||
<g filter="url(#filter0_d)">
|
||||
<path d="M15.75 5.25L6.75 14.25L2.625 10.125L3.6825 9.0675L6.75 12.1275L14.6925 4.1925L15.75 5.25Z" fill="#A2B1C6"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d" x="0.625" y="3.1925" width="17.125" height="14.0575" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="1"/>
|
||||
<feGaussianBlur stdDeviation="1"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.164706 0 0 0 0 0.247059 0 0 0 0 0.372549 0 0 0 0.2 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 983 B |
3
src/assets/images/delete-tag-hover.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0436 10.3943C9.7153 10.7226 9.1833 10.7226 8.8557 10.3943L7 8.27329L5.1443 10.3936C4.816 10.7219 4.284 10.7219 3.9564 10.3936C3.6281 10.0653 3.6281 9.53329 3.9564 9.20569L5.887 7.00069L3.9557 4.79429C3.6274 4.46599 3.6274 3.93469 3.9557 3.60639C4.284 3.27809 4.8153 3.27809 5.1436 3.60639L7 5.72809L8.8557 3.60639C9.184 3.27809 9.7153 3.27809 10.0436 3.60639C10.3719 3.93469 10.3719 4.46669 10.0436 4.79429L8.113 7.00069L10.0436 9.20569C10.3719 9.53399 10.3719 10.066 10.0436 10.3943V10.3943Z" fill="#DE350B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 627 B |
3
src/assets/images/delete-tag.svg
Normal file
@@ -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 |
5
src/assets/images/error.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99984 16.6667C6.32484 16.6667 3.33317 13.675 3.33317 10C3.33317 6.32502 6.32484 3.33335 9.99984 3.33335C13.6748 3.33335 16.6665 6.32502 16.6665 10C16.6665 13.675 13.6748 16.6667 9.99984 16.6667ZM9.99984 1.66669C8.90549 1.66669 7.82186 1.88224 6.81081 2.30102C5.79976 2.71981 4.8811 3.33364 4.10728 4.10746C2.54448 5.67027 1.6665 7.78988 1.6665 10C1.6665 12.2102 2.54448 14.3298 4.10728 15.8926C4.8811 16.6664 5.79976 17.2802 6.81081 17.699C7.82186 18.1178 8.90549 18.3334 9.99984 18.3334C12.21 18.3334 14.3296 17.4554 15.8924 15.8926C17.4552 14.3298 18.3332 12.2102 18.3332 10C18.3332 8.90567 18.1176 7.82204 17.6988 6.81099C17.28 5.79995 16.6662 4.88129 15.8924 4.10746C15.1186 3.33364 14.1999 2.71981 13.1889 2.30102C12.1778 1.88224 11.0942 1.66669 9.99984 1.66669Z" fill="#EF553B"/>
|
||||
<rect x="6.36768" y="7.54855" width="1.67" height="8.60363" transform="rotate(-45 6.36768 7.54855)" fill="#EF553B"/>
|
||||
<rect x="12.4517" y="6.36771" width="1.67" height="8.60363" transform="rotate(45 12.4517 6.36771)" fill="#EF553B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
11
src/assets/images/sort.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="6" height="12" viewBox="0 0 6 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0)">
|
||||
<path d="M2.99932 -3.63032e-05C2.75092 -3.63032e-05 2.54932 0.201563 2.54932 0.449964L2.54932 11.55C2.54932 11.7984 2.75092 12 2.99932 12C3.24772 12 3.44932 11.7984 3.44932 11.55L3.44932 0.449964C3.44932 0.201563 3.24772 -3.63032e-05 2.99932 -3.63032e-05Z" fill="#506784"/>
|
||||
<path d="M2.99915 1.80492e-05C2.8839 1.80492e-05 2.76865 0.0438534 2.68109 0.132073L0.581055 2.232C0.405273 2.40789 0.405273 2.69287 0.581055 2.86865C0.756946 3.04443 1.04193 3.04443 1.21771 2.86865L2.99969 1.08667L4.78168 2.86865C4.95746 3.04443 5.24255 3.04443 5.41833 2.86865C5.59412 2.69287 5.59412 2.40789 5.41833 2.232L3.3183 0.132073C3.22953 0.0438534 3.11428 1.80492e-05 2.99915 1.80492e-05V1.80492e-05Z" fill="#506784"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0">
|
||||
<rect width="6" height="12" fill="white" transform="matrix(1 0 0 -1 0 12)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 964 B |
5
src/assets/images/success.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="21" height="20" viewBox="0 0 21 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.4998 16.6667C6.82484 16.6667 3.83317 13.675 3.83317 10C3.83317 6.32502 6.82484 3.33335 10.4998 3.33335C14.1748 3.33335 17.1665 6.32502 17.1665 10C17.1665 13.675 14.1748 16.6667 10.4998 16.6667ZM10.4998 1.66669C9.40549 1.66669 8.32186 1.88224 7.31081 2.30102C6.29976 2.71981 5.3811 3.33364 4.60728 4.10746C3.04448 5.67027 2.1665 7.78988 2.1665 10C2.1665 12.2102 3.04448 14.3298 4.60728 15.8926C5.3811 16.6664 6.29976 17.2802 7.31081 17.699C8.32186 18.1178 9.40549 18.3334 10.4998 18.3334C12.71 18.3334 14.8296 17.4554 16.3924 15.8926C17.9552 14.3298 18.8332 12.2102 18.8332 10C18.8332 8.90567 18.6176 7.82204 18.1988 6.81099C17.78 5.79995 17.1662 4.88129 16.3924 4.10746C15.6186 3.33364 14.6999 2.71981 13.6889 2.30102C12.6778 1.88224 11.5942 1.66669 10.4998 1.66669Z" fill="#00CC96"/>
|
||||
<rect x="6.09717" y="10.2771" width="1.67" height="4.345" transform="rotate(-45 6.09717 10.2771)" fill="#00CC96"/>
|
||||
<rect x="13.752" y="6.97589" width="1.67" height="7.74359" transform="rotate(45 13.752 6.97589)" fill="#00CC96"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -27,14 +27,6 @@ button.primary:hover {
|
||||
text-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
button.primary:disabled {
|
||||
background: var(--color-bg-light-2);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-light-2);
|
||||
text-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
button.secondary {
|
||||
background: white;
|
||||
border: 1px solid var(--color-border);
|
||||
@@ -59,5 +51,13 @@ button.toolbar:hover {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
|
||||
button.primary:disabled,
|
||||
button.secondary:disabled {
|
||||
background: var(--color-bg-light-2);
|
||||
border: 1px solid var(--color-border);
|
||||
color: var(--color-text-light-2);
|
||||
text-shadow: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
|
||||
|
||||
136
src/assets/styles/multiselect.css
Normal file
@@ -0,0 +1,136 @@
|
||||
.sqliteviz-select,
|
||||
.sqliteviz-select .multiselect__tags {
|
||||
min-height: 36px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select {
|
||||
height: 34px;
|
||||
min-height: 34px;
|
||||
padding: 6px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tags {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
border: 1px solid var(--color-border);
|
||||
padding: 4px 32px 0 6px;
|
||||
}
|
||||
|
||||
.sqliteviz-select,
|
||||
.sqliteviz-select .multiselect__input,
|
||||
.sqliteviz-select .multiselect__single,
|
||||
.sqliteviz-select .multiselect__placeholder {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__single,
|
||||
.sqliteviz-select .multiselect__placeholder,
|
||||
.sqliteviz-select .multiselect__input {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__input {
|
||||
width: 0 !important;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__input {
|
||||
width: auto !important;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__placeholder,
|
||||
.sqliteviz-select .multiselect__input::placeholder {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option.multiselect__option--highlight {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag {
|
||||
background-color: var(--color-bg-light-4);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-small);
|
||||
color: var(--color-text-active);
|
||||
font-size: 11.05px;
|
||||
margin: 2px;
|
||||
}
|
||||
.sqliteviz-select .multiselect__tag-icon:after {
|
||||
content: url('~@/assets/images/delete-tag.svg');
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag-icon:focus:after,
|
||||
.sqliteviz-select .multiselect__tag-icon:hover:after {
|
||||
content: url('~@/assets/images/delete-tag-hover.svg');
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__tag-icon:focus,
|
||||
.sqliteviz-select .multiselect__tag-icon:hover {
|
||||
background-color: var(--color-bg-danger);
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option {
|
||||
min-height: 29px;
|
||||
padding: 8px 12px;
|
||||
line-height: 13px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option:after {
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__content-wrapper {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
border: 1px solid var(--color-border);
|
||||
box-shadow: var(--shadow-1);
|
||||
top: calc(100% - 1px);
|
||||
max-height: 292px !important;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--above .multiselect__content-wrapper {
|
||||
top: unset;
|
||||
bottom: calc(100% - 1px);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select:before {
|
||||
content: url('~@/assets/images/arrow.svg');
|
||||
border: none;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__select {
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.sqliteviz-select:hover .multiselect__tags {
|
||||
border-color: var(--color-border-dark);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__select:hover:before {
|
||||
content: url('~@/assets/images/arrow-hover.svg');
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--active .multiselect__tags {
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select .multiselect__option .no-results {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--disabled {
|
||||
opacity: unset;
|
||||
}
|
||||
|
||||
.sqliteviz-select.multiselect--disabled .multiselect__select {
|
||||
background: unset;
|
||||
}
|
||||
@@ -1,11 +1,17 @@
|
||||
.rounded-bg {
|
||||
padding: 40px 5px 5px;
|
||||
padding: 35px 5px 5px;
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
position: relative;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.straight .rounded-bg {
|
||||
border-radius: 0;
|
||||
border-width: 0 0 1px 0;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
@@ -18,6 +24,19 @@
|
||||
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) {
|
||||
.header-container {
|
||||
top: 0;
|
||||
@@ -32,22 +51,25 @@
|
||||
}
|
||||
.table-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
table {
|
||||
table.sqliteviz-table {
|
||||
min-width: 100%;
|
||||
margin-top: -40px;
|
||||
margin-top: -35px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
thead th, .fixed-header {
|
||||
.sqliteviz-table thead th, .fixed-header {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--color-bg-dark);
|
||||
color: var(--color-text-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
tbody td {
|
||||
.sqliteviz-table tbody td {
|
||||
font-size: 13px;
|
||||
background-color:white;
|
||||
color: var(--color-text-base);
|
||||
@@ -55,18 +77,20 @@ tbody td {
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
td, th, .fixed-header {
|
||||
padding: 12px 24px;
|
||||
.sqliteviz-table td,
|
||||
.sqliteviz-table th,
|
||||
.fixed-header {
|
||||
padding: 8px 24px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr td:last-child,
|
||||
thead tr th:last-child,
|
||||
.sqliteviz-table tbody tr td:last-child,
|
||||
.sqliteviz-table thead tr th:last-child,
|
||||
.header-container div .fixed-header:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
td > div.cell-data {
|
||||
.sqliteviz-table td > div.cell-data {
|
||||
width: -webkit-max-content;
|
||||
width: -moz-max-content;
|
||||
width: max-content;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
.icon-tooltip {
|
||||
visibility: hidden;
|
||||
background-color: rgba(80, 103, 132, 0.85);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
padding: 0 6px;
|
||||
line-height: 19px;;
|
||||
position: fixed;
|
||||
z-index: 5;
|
||||
height: 19px;
|
||||
border-radius: var(--border-radius-medium);
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
--color-blue-dark: #0D76BF;
|
||||
--color-blue-dark-2: #2A3F5F;
|
||||
--color-red: #EF553B;
|
||||
--color-red-2: #DE350B;
|
||||
--color-red-light: #FFBDAD;
|
||||
--color-yellow: #FBEFCB;
|
||||
|
||||
|
||||
@@ -18,12 +20,16 @@
|
||||
--color-bg-light: var(--color-gray-light);
|
||||
--color-bg-light-2: var(--color-gray-light-2);
|
||||
--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-warning: var(--color-yellow);
|
||||
--color-bg-danger: var(--color-red-light);
|
||||
--color-danger: var(--color-red-2);
|
||||
--color-accent: var(--color-blue-medium);
|
||||
--color-accent-shade: var(--color-blue-dark);
|
||||
--color-border-light: var(--color-gray-light-2);
|
||||
--color-border: var(--color-gray-light-3);
|
||||
--color-border-dark: var(--color-gray-medium);
|
||||
--color-text-light: var(--color-white);
|
||||
--color-text-light-2: var(--color-gray-medium);
|
||||
--color-text-base: var(--color-gray-dark);
|
||||
|
||||
@@ -1,113 +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"
|
||||
:useResizeHandler="true"
|
||||
:debug="true"
|
||||
:advancedTraceTypeSelector="true"
|
||||
class="chart"
|
||||
: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 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 () {
|
||||
if (!this.sqlResult) {
|
||||
return {}
|
||||
}
|
||||
const dataSorces = {}
|
||||
const matrix = this.sqlResult.values
|
||||
const [row] = matrix
|
||||
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
|
||||
this.sqlResult.columns.forEach((column, index) => {
|
||||
dataSorces[column] = transposedMatrix[index]
|
||||
})
|
||||
return dataSorces
|
||||
},
|
||||
dataSourceOptions () {
|
||||
return Object.keys(this.dataSources).map(name => ({
|
||||
value: name,
|
||||
label: name
|
||||
}))
|
||||
}
|
||||
},
|
||||
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: {
|
||||
update (data, layout, frames) {
|
||||
this.state = { data, layout, frames }
|
||||
this.$emit('update')
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
// we don't need to save the data, only settings
|
||||
// so we modify state.data using dereference
|
||||
const stateCopy = JSON.parse(JSON.stringify(this.state))
|
||||
const emptySources = {}
|
||||
for (const key in this.dataSources) {
|
||||
emptySources[key] = []
|
||||
}
|
||||
dereference(stateCopy.data, emptySources)
|
||||
return stateCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -1,20 +1,26 @@
|
||||
<template>
|
||||
<div class="checkbox-container" @click.stop="onClick">
|
||||
<div
|
||||
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]"
|
||||
@click.stop="onClick"
|
||||
>
|
||||
<div v-show="!checked" class="unchecked" />
|
||||
<img
|
||||
v-show="checked && theme === 'accent'"
|
||||
:src="require('@/assets/images/checkbox_checked.svg')"
|
||||
v-show="checked && !disabled"
|
||||
:src="theme === 'light'
|
||||
? require('@/assets/images/checkbox_checked_light.svg')
|
||||
: require('@/assets/images/checkbox_checked.svg')"
|
||||
/>
|
||||
<img
|
||||
v-show="checked && theme === 'light'"
|
||||
:src="require('@/assets/images/checkbox_checked_light.svg')"
|
||||
v-show="checked && disabled"
|
||||
:src="require('@/assets/images/checkbox_checked_disabled.svg')"
|
||||
/>
|
||||
<span v-if="label" class="label">{{ label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'checkBox',
|
||||
name: 'CheckBox',
|
||||
props: {
|
||||
theme: {
|
||||
type: String,
|
||||
@@ -28,6 +34,16 @@ export default {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ''
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data () {
|
||||
@@ -37,8 +53,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
onClick () {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
if (!this.disabled) {
|
||||
this.checked = !this.checked
|
||||
this.$emit('click', this.checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,7 +64,7 @@ export default {
|
||||
|
||||
<style scoped>
|
||||
.checkbox-container {
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
.unchecked {
|
||||
@@ -64,4 +82,24 @@ export default {
|
||||
img {
|
||||
display: block;
|
||||
}
|
||||
.label {
|
||||
margin-left: 6px;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.checked .label {
|
||||
color: var(--color-text-active);
|
||||
}
|
||||
|
||||
.disabled.checkbox-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled .label {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.disabled .unchecked,
|
||||
.disabled .unchecked:hover {
|
||||
background-color: var(--color-bg-light-2);
|
||||
}
|
||||
</style>
|
||||
|
||||
130
src/components/CsvImport/DelimiterSelector/ascii.js
Normal file
@@ -0,0 +1,130 @@
|
||||
export default {
|
||||
0: { name: 'null character' },
|
||||
1: { name: 'start of header' },
|
||||
2: { name: 'start of text' },
|
||||
3: { name: 'end of text' },
|
||||
4: { name: 'end of transmission' },
|
||||
5: { name: 'enquiry' },
|
||||
6: { name: 'acknowledge' },
|
||||
7: { name: 'bell (ring)' },
|
||||
8: { name: 'backspace' },
|
||||
9: { name: 'horizontal tab' },
|
||||
10: { name: 'line feed' },
|
||||
11: { name: 'vertical tab' },
|
||||
12: { name: 'form feed' },
|
||||
13: { name: 'carriage return' },
|
||||
14: { name: 'shift out' },
|
||||
15: { name: 'shift in' },
|
||||
16: { name: 'data link escape' },
|
||||
17: { name: 'device control 1' },
|
||||
18: { name: 'device control 2' },
|
||||
19: { name: 'device control 3' },
|
||||
20: { name: 'device control 4' },
|
||||
21: { name: 'negative acknowledge' },
|
||||
22: { name: 'synchronize' },
|
||||
23: { name: 'end transmission block' },
|
||||
24: { name: 'cancel' },
|
||||
25: { name: 'end of medium' },
|
||||
26: { name: 'substitute' },
|
||||
27: { name: 'escape' },
|
||||
28: { name: 'file separator' },
|
||||
29: { name: 'group separator' },
|
||||
30: { name: 'record separator' },
|
||||
31: { name: 'unit separator' },
|
||||
32: { name: 'space' },
|
||||
33: { name: 'exclamation mark' },
|
||||
34: { name: 'quotation mark' },
|
||||
35: { name: 'number sign' },
|
||||
36: { name: 'dollar sign' },
|
||||
37: { name: 'percent sign' },
|
||||
38: { name: 'ampersand' },
|
||||
39: { name: 'apostrophe' },
|
||||
40: { name: 'left parenthesis' },
|
||||
41: { name: 'right parenthesis' },
|
||||
42: { name: 'asterisk' },
|
||||
43: { name: 'plus sign' },
|
||||
44: { name: 'comma' },
|
||||
45: { name: 'hyphen' },
|
||||
46: { name: 'period' },
|
||||
47: { name: 'slash' },
|
||||
48: { name: 'digit 0' },
|
||||
49: { name: 'digit 1' },
|
||||
50: { name: 'digit 2' },
|
||||
51: { name: 'digit 3' },
|
||||
52: { name: 'digit 4' },
|
||||
53: { name: 'digit 5' },
|
||||
54: { name: 'digit 6' },
|
||||
55: { name: 'digit 7' },
|
||||
56: { name: 'digit 8' },
|
||||
57: { name: 'digit 9' },
|
||||
58: { name: 'colon' },
|
||||
59: { name: 'semicolon' },
|
||||
60: { name: 'less-than' },
|
||||
61: { name: 'equals-to' },
|
||||
62: { name: 'greater-than' },
|
||||
63: { name: 'question mark' },
|
||||
64: { name: 'at sign' },
|
||||
65: { name: 'uppercase A' },
|
||||
66: { name: 'uppercase B' },
|
||||
67: { name: 'uppercase C' },
|
||||
68: { name: 'uppercase D' },
|
||||
69: { name: 'uppercase E' },
|
||||
70: { name: 'uppercase F' },
|
||||
71: { name: 'uppercase G' },
|
||||
72: { name: 'uppercase H' },
|
||||
73: { name: 'uppercase I' },
|
||||
74: { name: 'uppercase J' },
|
||||
75: { name: 'uppercase K' },
|
||||
76: { name: 'uppercase L' },
|
||||
77: { name: 'uppercase M' },
|
||||
78: { name: 'uppercase N' },
|
||||
79: { name: 'uppercase O' },
|
||||
80: { name: 'uppercase P' },
|
||||
81: { name: 'uppercase Q' },
|
||||
82: { name: 'uppercase R' },
|
||||
83: { name: 'uppercase S' },
|
||||
84: { name: 'uppercase T' },
|
||||
85: { name: 'uppercase U' },
|
||||
86: { name: 'uppercase V' },
|
||||
87: { name: 'uppercase W' },
|
||||
88: { name: 'uppercase X' },
|
||||
89: { name: 'uppercase Y' },
|
||||
90: { name: 'uppercase Z' },
|
||||
91: { name: 'left square bracket' },
|
||||
92: { name: 'backslash' },
|
||||
93: { name: 'right square bracket' },
|
||||
94: { name: 'caret' },
|
||||
95: { name: 'underscore' },
|
||||
96: { name: 'grave accent' },
|
||||
97: { name: 'lowercase a' },
|
||||
98: { name: 'lowercase b' },
|
||||
99: { name: 'lowercase c' },
|
||||
100: { name: 'lowercase d' },
|
||||
101: { name: 'lowercase e' },
|
||||
102: { name: 'lowercase f' },
|
||||
103: { name: 'lowercase g' },
|
||||
104: { name: 'lowercase h' },
|
||||
105: { name: 'lowercase i' },
|
||||
106: { name: 'lowercase j' },
|
||||
107: { name: 'lowercase k' },
|
||||
108: { name: 'lowercase l' },
|
||||
109: { name: 'lowercase m' },
|
||||
110: { name: 'lowercase n' },
|
||||
111: { name: 'lowercase o' },
|
||||
112: { name: 'lowercase p' },
|
||||
113: { name: 'lowercase q' },
|
||||
114: { name: 'lowercase r' },
|
||||
115: { name: 'lowercase s' },
|
||||
116: { name: 'lowercase t' },
|
||||
117: { name: 'lowercase u' },
|
||||
118: { name: 'lowercase v' },
|
||||
119: { name: 'lowercase w' },
|
||||
120: { name: 'lowercase x' },
|
||||
121: { name: 'lowercase y' },
|
||||
122: { name: 'lowercase z' },
|
||||
123: { name: 'left curly brace' },
|
||||
124: { name: 'vertical bar' },
|
||||
125: { name: 'right curly brace' },
|
||||
126: { name: 'tilde' },
|
||||
127: { name: 'delete (rubout)' }
|
||||
}
|
||||
204
src/components/CsvImport/DelimiterSelector/index.vue
Normal file
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<div :class="{ 'disabled': disabled }">
|
||||
<div class="text-field-label">Delimiter</div>
|
||||
<div
|
||||
class="delimiter-selector-container"
|
||||
:style="{ width: width }"
|
||||
@click="onContainerClick"
|
||||
>
|
||||
<div class="value">
|
||||
<input
|
||||
:class="{ 'filled': filled }"
|
||||
ref="delimiterInput"
|
||||
type="text"
|
||||
maxlength="1"
|
||||
v-model="inputValue"
|
||||
@click.stop
|
||||
:disabled="disabled"
|
||||
/>
|
||||
<div class="name">{{ getSymbolName(value) }}</div>
|
||||
</div>
|
||||
<div class="controls" @click.stop>
|
||||
<clear-icon @click.native="clear" :disabled="disabled"/>
|
||||
<drop-down-chevron
|
||||
:disabled="disabled"
|
||||
@click.native="!disabled && (showOptions = !showOptions)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="showOptions" class="options" :style="{ width: width }">
|
||||
<div
|
||||
v-for="(option, index) in options"
|
||||
:key="index"
|
||||
@click="chooseOption(option)"
|
||||
class="option"
|
||||
>
|
||||
<pre>{{option}}</pre><div>{{ getSymbolName(option) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ascii from './ascii'
|
||||
import DropDownChevron from '@/components/svg/dropDownChevron'
|
||||
import ClearIcon from '@/components/svg/clear'
|
||||
|
||||
export default {
|
||||
name: 'DelimiterSelector',
|
||||
props: ['value', 'width', 'disabled'],
|
||||
components: { DropDownChevron, ClearIcon },
|
||||
data () {
|
||||
return {
|
||||
showOptions: false,
|
||||
options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'],
|
||||
filled: false,
|
||||
inputValue: ''
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
inputValue () {
|
||||
if (this.inputValue) {
|
||||
this.filled = true
|
||||
if (this.inputValue !== this.value) {
|
||||
this.$emit('input', this.inputValue)
|
||||
}
|
||||
} else {
|
||||
this.filled = false
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.inputValue = this.value
|
||||
},
|
||||
methods: {
|
||||
getSymbolName (str) {
|
||||
if (!str) {
|
||||
return ''
|
||||
}
|
||||
return ascii[str.charCodeAt(0).toString()].name
|
||||
},
|
||||
chooseOption (option) {
|
||||
this.inputValue = option
|
||||
this.showOptions = false
|
||||
},
|
||||
onContainerClick (event) {
|
||||
this.$refs.delimiterInput.focus()
|
||||
},
|
||||
|
||||
clear () {
|
||||
if (!this.disabled) {
|
||||
this.inputValue = ''
|
||||
this.$refs.delimiterInput.focus()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.delimiter-selector-container {
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
height: 36px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.value .name {
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.options {
|
||||
background: var(--color-white);
|
||||
border: 1px solid var(--color-border);
|
||||
border-width: 0 1px 1px 1px;
|
||||
color: var(--color-text-base);
|
||||
border-radius: var(--border-radius-medium-2);
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 24px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.option:hover {
|
||||
background-color: var(--color-bg-light);
|
||||
color: var(--color-text-active);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.option pre {
|
||||
background-color: var(--color-bg-warning);
|
||||
line-height: 20px;
|
||||
margin-right: 6px;
|
||||
tab-size: 1;
|
||||
font-family: monospace;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input {
|
||||
background: var(--color-white);
|
||||
border: none;
|
||||
color: var(--color-text-base);
|
||||
height: 20px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
box-sizing: border-box;
|
||||
width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input.filled {
|
||||
background: var(--color-bg-warning);
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.text-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-base);
|
||||
padding-left: 8px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.disabled .text-field-label {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.disabled .delimiter-selector-container {
|
||||
background: var(--color-bg-light);
|
||||
}
|
||||
</style>
|
||||
94
src/components/CsvImport/csv.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import Papa from 'papaparse'
|
||||
|
||||
const hintsByCode = {
|
||||
MissingQuotes: 'Edit your CSV so that the field has a closing quote char.',
|
||||
TooFewFields: 'Add fields or try another delimiter.',
|
||||
TooManyFields: 'Edit your CSV or try another delimiter.'
|
||||
}
|
||||
|
||||
export default {
|
||||
getResult (source) {
|
||||
const result = {
|
||||
columns: []
|
||||
}
|
||||
const values = {}
|
||||
if (source.meta.fields) {
|
||||
source.meta.fields.forEach(col => {
|
||||
const colName = col.trim()
|
||||
result.columns.push(colName)
|
||||
values[colName] = source.data.map(row => {
|
||||
let value = row[col]
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
return value
|
||||
})
|
||||
})
|
||||
} else {
|
||||
for (let i = 0; i <= source.data[0].length - 1; i++) {
|
||||
const colName = `col${i + 1}`
|
||||
result.columns.push(colName)
|
||||
values[colName] = source.data.map(row => {
|
||||
let value = row[i]
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString()
|
||||
}
|
||||
return value
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
result.values = values
|
||||
return result
|
||||
},
|
||||
|
||||
parse (file, config = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const defaultConfig = {
|
||||
delimiter: '', // auto-detect
|
||||
newline: '', // auto-detect
|
||||
quoteChar: '"',
|
||||
escapeChar: '"',
|
||||
header: false,
|
||||
transformHeader: undefined,
|
||||
dynamicTyping: true,
|
||||
preview: 0,
|
||||
encoding: 'UTF-8',
|
||||
worker: true,
|
||||
comments: false,
|
||||
step: undefined,
|
||||
complete: results => {
|
||||
const res = {
|
||||
data: this.getResult(results),
|
||||
delimiter: results.meta.delimiter,
|
||||
hasErrors: false,
|
||||
rowCount: results.data.length
|
||||
}
|
||||
res.messages = results.errors.map(msg => {
|
||||
msg.type = msg.code === 'UndetectableDelimiter' ? 'info' : 'error'
|
||||
if (msg.type === 'error') res.hasErrors = true
|
||||
msg.hint = hintsByCode[msg.code]
|
||||
return msg
|
||||
})
|
||||
resolve(res)
|
||||
},
|
||||
error: (error, file) => {
|
||||
reject(error)
|
||||
},
|
||||
download: false,
|
||||
downloadRequestHeaders: undefined,
|
||||
downloadRequestBody: undefined,
|
||||
skipEmptyLines: 'greedy',
|
||||
chunk: undefined,
|
||||
chunkSize: undefined,
|
||||
fastMode: undefined,
|
||||
beforeFirstChunk: undefined,
|
||||
withCredentials: undefined,
|
||||
transform: undefined,
|
||||
delimitersToGuess: [',', '\t', '|', ';', Papa.RECORD_SEP, Papa.UNIT_SEP]
|
||||
}
|
||||
|
||||
Papa.parse(file, { ...defaultConfig, ...config })
|
||||
})
|
||||
}
|
||||
}
|
||||
382
src/components/CsvImport/index.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<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 './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>
|
||||
@@ -1,25 +1,20 @@
|
||||
<template>
|
||||
<div class="db-upload-container">
|
||||
<label for="assetsFieldHandle">
|
||||
<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"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id="assetsFieldHandle"
|
||||
@change="loadDb"
|
||||
ref="file"
|
||||
accept=".db,.sqlite,.sqlite3"
|
||||
/>
|
||||
<div class="text">
|
||||
Drop the database file here or click to choose a file from your computer.
|
||||
Drop the database or CSV file here or click to choose a file from your computer.
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<div v-if="illustrated" id="img-container">
|
||||
</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"
|
||||
@@ -31,7 +26,8 @@
|
||||
ref="fileImg"
|
||||
:class="{
|
||||
'swing': state === 'dragover',
|
||||
'fly': state === 'drop'
|
||||
'fly': state === 'dropping',
|
||||
'hidden': state === 'dropped'
|
||||
}"
|
||||
:src="require('@/assets/images/file.png')"
|
||||
/>
|
||||
@@ -44,30 +40,60 @@
|
||||
/>
|
||||
</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: 'DbUpload',
|
||||
name: 'DbUploader',
|
||||
props: {
|
||||
illustrated: {
|
||||
type: Boolean,
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 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()
|
||||
animationPromise: Promise.resolve(),
|
||||
file: null,
|
||||
newDb: null
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
if (this.illustrated) {
|
||||
if (this.type === 'illustrated') {
|
||||
this.animationPromise = new Promise((resolve) => {
|
||||
this.$refs.fileImg.addEventListener('animationend', event => {
|
||||
if (event.animationName.startsWith('fly')) {
|
||||
this.state = 'dropped'
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
@@ -75,26 +101,57 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
loadDb () {
|
||||
this.state = 'drop'
|
||||
return Promise.all([this.$db.loadDb(this.$refs.file.files[0]), this.animationPromise])
|
||||
.then(([schema]) => {
|
||||
this.$store.commit('saveSchema', schema)
|
||||
if (this.$route.path !== '/editor') {
|
||||
this.$router.push('/editor')
|
||||
}
|
||||
})
|
||||
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.$refs.file.files = event.dataTransfer.files
|
||||
this.loadDb()
|
||||
this.checkFile(event.dataTransfer.files[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
label {
|
||||
.db-uploader-container {
|
||||
position: relative;
|
||||
}
|
||||
.drop-area-container {
|
||||
display: inline-block;
|
||||
border: 1px dashed var(--color-border);
|
||||
padding: 8px;
|
||||
@@ -115,17 +172,14 @@ label {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#img-container {
|
||||
position: absolute;
|
||||
top: calc(50% - 120px);
|
||||
top: 54px;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, 0);
|
||||
width: 450px;
|
||||
height: 338px;
|
||||
pointer-events: none;
|
||||
@@ -190,11 +244,16 @@ input {
|
||||
#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); }
|
||||
100% {
|
||||
transform: rotate(360deg) scale(0.5);
|
||||
top: 183px;
|
||||
left: 225px;
|
||||
}
|
||||
}
|
||||
|
||||
#file-img.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
110
src/components/IconButton.vue
Normal 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>
|
||||
115
src/components/LoadingIndicator.vue
Normal file
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`">
|
||||
<circle
|
||||
class="loader-svg bg"
|
||||
:style="{ strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
<circle
|
||||
class="loader-svg front"
|
||||
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
|
||||
:cx="size / 2"
|
||||
:cy="size / 2"
|
||||
:r="radius"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIndicator',
|
||||
props: {
|
||||
progress: {
|
||||
type: Number,
|
||||
required: false
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 20
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
circleProgress () {
|
||||
const circle = this.radius * 3.14 * 2
|
||||
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3
|
||||
const space = circle - dash
|
||||
return `${dash}px, ${space}px`
|
||||
},
|
||||
animationClass () {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.loader-svg {
|
||||
position: absolute;
|
||||
left: 0; right: 0; top: 0; bottom: 0;
|
||||
fill: none;
|
||||
stroke-linecap: round;
|
||||
stroke: var(--color-accent);
|
||||
}
|
||||
|
||||
.loader-svg.bg {
|
||||
stroke: var(--color-gray-light-3);
|
||||
}
|
||||
|
||||
.loading .loader-svg.front {
|
||||
will-change: transform;
|
||||
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 {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.progress .loader-svg.front {
|
||||
transition: stroke-dasharray 0.2s;
|
||||
}
|
||||
|
||||
.progress .loader-svg.bg {
|
||||
animation: bg-animation 1.5s cubic-bezier(1,1,1,1) 0s infinite;
|
||||
}
|
||||
|
||||
@keyframes bg-animation{
|
||||
0% {
|
||||
r: 8;
|
||||
}
|
||||
50% {
|
||||
stroke: var(--color-gray-light-2);
|
||||
r: 9;
|
||||
}
|
||||
100% {
|
||||
r: 8;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
86
src/components/Logs.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<div class="logs-container" ref="logsContainer">
|
||||
<div v-for="(msg, index) in messages" :key="index" class="msg">
|
||||
<img v-if="msg.type === 'error'" :src="require('@/assets/images/error.svg')">
|
||||
<img v-if="msg.type === 'info'" :src="require('@/assets/images/info.svg')" width="20px">
|
||||
<img v-if="msg.type === 'success'" :src="require('@/assets/images/success.svg')">
|
||||
<loading-indicator v-if="msg.type === 'loading'" :progress="msg.progress" />
|
||||
<span class="msg-text">{{ serializeMessage(msg) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIndicator from '@/components/LoadingIndicator'
|
||||
|
||||
export default {
|
||||
name: 'logs',
|
||||
props: ['messages'],
|
||||
components: { LoadingIndicator },
|
||||
watch: {
|
||||
'messages.length': 'scrollToBottom'
|
||||
},
|
||||
mounted () {
|
||||
this.scrollToBottom()
|
||||
},
|
||||
methods: {
|
||||
async scrollToBottom () {
|
||||
const container = this.$refs.logsContainer
|
||||
if (container) {
|
||||
await this.$nextTick()
|
||||
container.scrollTop = container.scrollHeight
|
||||
}
|
||||
},
|
||||
|
||||
serializeMessage (msg) {
|
||||
let result = ''
|
||||
if (msg.row !== null && msg.row !== undefined) {
|
||||
if (msg.type === 'error') {
|
||||
result += `Error in row ${msg.row}. `
|
||||
} else {
|
||||
result += `Information about row ${msg.row}. `
|
||||
}
|
||||
}
|
||||
|
||||
result += msg.message
|
||||
if (!(/(\.|!|\?)$/.test(result))) {
|
||||
result += '.'
|
||||
}
|
||||
|
||||
if (msg.hint) {
|
||||
result += ` ${msg.hint}`
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.logs-container {
|
||||
background-color: var(--color-white);
|
||||
padding: 0 5px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--color-border-light);
|
||||
box-sizing: border-box;
|
||||
overflow-y: auto;
|
||||
color: var(--color-text-base);
|
||||
}
|
||||
.msg {
|
||||
padding: 12px 7px;
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.msg:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.msg-text {
|
||||
margin-left: 6px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,253 +0,0 @@
|
||||
<template>
|
||||
<nav>
|
||||
<div>
|
||||
<router-link to="/editor">Editor</router-link>
|
||||
<router-link to="/my-queries">My queries</router-link>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
v-if="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="runDisabled"
|
||||
@click="currentQuery.execute"
|
||||
>
|
||||
Run
|
||||
</button>
|
||||
<button
|
||||
v-show="currentQuery && $route.path === '/editor'"
|
||||
class="primary"
|
||||
:disabled="!isUnsaved"
|
||||
@click="checkQueryBeforeSave"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
<button class="primary" @click="createNewQuery">Create</button>
|
||||
</div>
|
||||
|
||||
<!--Save Query dialog -->
|
||||
<modal name="save" classes="dialog" height="auto">
|
||||
<div class="dialog-header">
|
||||
Save query
|
||||
<close-icon @click="cancelSave"/>
|
||||
</div>
|
||||
<div class="dialog-body">
|
||||
<div v-show="isPredefined" id="save-note">
|
||||
<img :src="require('@/assets/images/info.svg')">
|
||||
Note: Predefined queries can't be edited.
|
||||
That's why your modifications will be saved as a new query. Enter the name for it.
|
||||
</div>
|
||||
<text-field
|
||||
label="Query name"
|
||||
:error-msg="errorMsg"
|
||||
v-model="name"
|
||||
width="100%"
|
||||
/>
|
||||
</div>
|
||||
<div class="dialog-buttons-container">
|
||||
<button class="secondary" @click="cancelSave">Cancel</button>
|
||||
<button class="primary" @click="saveQuery">Save</button>
|
||||
</div>
|
||||
</modal>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { nanoid } from 'nanoid'
|
||||
import TextField from '@/components/TextField'
|
||||
import CloseIcon from '@/components/svg/close'
|
||||
|
||||
export default {
|
||||
name: 'MainMenu',
|
||||
components: {
|
||||
TextField,
|
||||
CloseIcon
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
name: '',
|
||||
errorMsg: null
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
currentQuery () {
|
||||
return this.$store.state.currentTab
|
||||
},
|
||||
isUnsaved () {
|
||||
if (!this.currentQuery) {
|
||||
return false
|
||||
}
|
||||
const tabIndex = this.currentQuery.tabIndex
|
||||
return this.$store.state.tabs[tabIndex].isUnsaved
|
||||
},
|
||||
isPredefined () {
|
||||
if (this.currentQuery) {
|
||||
return this.currentQuery.isPredefined
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
runDisabled () {
|
||||
return this.currentQuery && (!this.$store.state.schema || !this.currentQuery.query)
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.$root.$on('createNewQuery', this.createNewQuery)
|
||||
this.$root.$on('saveQuery', this.checkQueryBeforeSave)
|
||||
document.addEventListener('keydown', this._keyListener)
|
||||
},
|
||||
beforeDestroy () {
|
||||
document.removeEventListener('keydown', this._keyListener)
|
||||
},
|
||||
methods: {
|
||||
createNewQuery () {
|
||||
const tab = {
|
||||
id: nanoid(),
|
||||
name: null,
|
||||
tempName: this.$store.state.untitledLastIndex
|
||||
? `Untitled ${this.$store.state.untitledLastIndex}`
|
||||
: 'Untitled',
|
||||
isUnsaved: true
|
||||
}
|
||||
this.$store.commit('addTab', tab)
|
||||
this.$store.commit('setCurrentTabId', tab.id)
|
||||
},
|
||||
cancelSave () {
|
||||
this.$modal.hide('save')
|
||||
this.$root.$off('querySaved')
|
||||
},
|
||||
checkQueryBeforeSave () {
|
||||
this.errorMsg = null
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
|
||||
if (isFromScratch || this.isPredefined) {
|
||||
this.name = ''
|
||||
this.$modal.show('save')
|
||||
} else {
|
||||
this.saveQuery()
|
||||
}
|
||||
},
|
||||
saveQuery () {
|
||||
const isFromScratch = !this.currentQuery.initName
|
||||
if ((isFromScratch || this.isPredefined) && !this.name) {
|
||||
this.errorMsg = 'Query name can\'t be empty'
|
||||
return
|
||||
}
|
||||
const dataSet = this.currentQuery.result
|
||||
const tabView = this.currentQuery.view
|
||||
// Prepare query
|
||||
const value = {
|
||||
id: this.isPredefined ? nanoid() : this.currentQuery.id,
|
||||
query: this.currentQuery.query,
|
||||
chart: this.currentQuery.getChartSatateForSave(),
|
||||
name: (!this.isPredefined && this.currentQuery.initName) || this.name,
|
||||
createdAt: new Date()
|
||||
}
|
||||
|
||||
// Save query
|
||||
let myQueries = JSON.parse(localStorage.getItem('myQueries'))
|
||||
if (!myQueries) {
|
||||
myQueries = [value]
|
||||
} else if (isFromScratch || this.isPredefined) {
|
||||
myQueries.push(value)
|
||||
} else {
|
||||
const queryIndex = myQueries.findIndex(query => query.id === this.currentQuery.id)
|
||||
value.createdAt = myQueries[queryIndex].createdAt
|
||||
myQueries[queryIndex] = value
|
||||
}
|
||||
localStorage.setItem('myQueries', JSON.stringify(myQueries))
|
||||
|
||||
// Update tab
|
||||
this.$store.commit('updateTab', {
|
||||
index: this.currentQuery.tabIndex,
|
||||
name: value.name,
|
||||
id: value.id,
|
||||
query: value.query,
|
||||
chart: value.chart,
|
||||
isUnsaved: false
|
||||
})
|
||||
|
||||
// Restore data:
|
||||
// e.g. if we save predefined query the tab will be created again
|
||||
// (because of new id) and
|
||||
// it will be without sql result and has default view - table.
|
||||
// That's why we need to restore data and view
|
||||
this.$nextTick(() => {
|
||||
this.currentQuery.result = dataSet
|
||||
this.currentQuery.view = tabView
|
||||
})
|
||||
|
||||
// Hide dialog
|
||||
this.$modal.hide('save')
|
||||
|
||||
// Signal about saving
|
||||
this.$root.$emit('querySaved')
|
||||
},
|
||||
_keyListener (e) {
|
||||
if (this.$route.path === '/editor') {
|
||||
// Run query Ctrl+R
|
||||
if (e.key === 'r' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (!this.runDisabled) {
|
||||
this.currentQuery.execute()
|
||||
}
|
||||
}
|
||||
|
||||
// Save query Ctrl+S
|
||||
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
if (this.isUnsaved) {
|
||||
this.checkQueryBeforeSave()
|
||||
}
|
||||
}
|
||||
|
||||
// New (blank) query Ctrl+B
|
||||
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
this.createNewQuery()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
nav {
|
||||
height: 68px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--color-bg-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
box-shadow: var(--shadow-1);
|
||||
box-sizing: border-box;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
padding: 0 52px;
|
||||
z-index: 999;
|
||||
}
|
||||
a {
|
||||
font-size: 18px;
|
||||
color: var(--color-text-base);
|
||||
text-transform: none;
|
||||
text-decoration: none;
|
||||
margin-right: 28px;
|
||||
}
|
||||
a.router-link-active {
|
||||
color: var(--color-accent);
|
||||
}
|
||||
button {
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
#save-note {
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
#save-note img {
|
||||
margin: -3px 6px 0 0;
|
||||
}
|
||||
</style>
|
||||
@@ -3,13 +3,13 @@
|
||||
ref="container"
|
||||
:class="[
|
||||
'splitpanes',
|
||||
`splitpanes--${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes--dragging': touch.dragging }
|
||||
`splitpanes-${horizontal ? 'horizontal' : 'vertical'}`,
|
||||
{ 'splitpanes-dragging': dragging }
|
||||
]"
|
||||
>
|
||||
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
class="splitpanes-pane"
|
||||
ref="left"
|
||||
:size="paneBefore.size"
|
||||
max-size="30"
|
||||
@@ -19,20 +19,20 @@
|
||||
</div>
|
||||
<!-- Splitter start-->
|
||||
<div
|
||||
class="splitpanes__splitter"
|
||||
@mousedown="onMouseDown"
|
||||
@touchstart="onMouseDown"
|
||||
class="splitpanes-splitter"
|
||||
@mousedown="bindEvents"
|
||||
@touchstart="bindEvents"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'toggle-btns',
|
||||
{'both': after.max === 100 && before.max === 100 && after.size > 0 && before.size > 0}
|
||||
{'both': after.max === 100 && before.max === 100 && paneAfter.size > 0 && paneBefore.size > 0}
|
||||
]"
|
||||
>
|
||||
<div
|
||||
v-if="after.max === 100 && after.size > 0"
|
||||
v-if="after.max === 100 && paneAfter.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane('before')"
|
||||
@click="togglePane(paneBefore)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
@@ -41,9 +41,9 @@
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
v-if="before.max === 100 && before.size > 0"
|
||||
v-if="before.max === 100 && paneBefore.size > 0"
|
||||
class="toggle-btn"
|
||||
@click="togglePane('after')"
|
||||
@click="togglePane(paneAfter)"
|
||||
>
|
||||
<img
|
||||
class="direction-icon"
|
||||
@@ -55,7 +55,7 @@
|
||||
</div>
|
||||
<!-- splitter end -->
|
||||
<div
|
||||
class="splitpanes__pane"
|
||||
class="splitpanes-pane"
|
||||
ref="right"
|
||||
:style="styles.after"
|
||||
>
|
||||
@@ -65,6 +65,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import splitter from './splitter'
|
||||
|
||||
export default {
|
||||
name: 'Splitpanes',
|
||||
@@ -82,10 +83,7 @@ export default {
|
||||
before: this.before.size,
|
||||
after: this.after.size
|
||||
},
|
||||
touch: {
|
||||
mouseDown: false,
|
||||
dragging: false
|
||||
},
|
||||
dragging: false,
|
||||
movableSplitter: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
@@ -108,38 +106,42 @@ export default {
|
||||
},
|
||||
directionBeforeIconStyle () {
|
||||
const expanded = this.paneBefore.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
const translation = 'translate(-50%, -50%) '
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(90deg)' : 'rotate(-90deg)'}`
|
||||
}
|
||||
rotation = expanded ? 'rotate(90deg)' : 'rotate(-90deg)'
|
||||
} else {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(0deg)' : 'rotate(180deg)'}`
|
||||
}
|
||||
rotation = expanded ? 'rotate(0deg)' : 'rotate(180deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
},
|
||||
directionAfterIconStyle () {
|
||||
const expanded = this.paneAfter.size !== 0
|
||||
const translation = 'translate(-50%, -50%)'
|
||||
let rotation = ''
|
||||
|
||||
if (this.horizontal) {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(-90deg)' : 'rotate(90deg)'}`
|
||||
}
|
||||
rotation = expanded ? 'rotate(-90deg)' : 'rotate(90deg)'
|
||||
} else {
|
||||
return {
|
||||
transform: `${translation} ${expanded ? 'rotate(180deg)' : 'rotate(0deg)'}`
|
||||
}
|
||||
rotation = expanded ? 'rotate(180deg)' : 'rotate(0deg)'
|
||||
}
|
||||
|
||||
return {
|
||||
transform: translation + rotation
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
bindEvents () {
|
||||
// Passive: false to prevent scrolling while touch dragging.
|
||||
document.addEventListener('mousemove', this.onMouseMove, { passive: false })
|
||||
document.addEventListener('mouseup', this.onMouseUp)
|
||||
|
||||
// Passive: false to prevent scrolling while touch dragging.
|
||||
if ('ontouchstart' in window) {
|
||||
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
|
||||
document.addEventListener('touchend', this.onMouseUp)
|
||||
@@ -156,24 +158,15 @@ export default {
|
||||
}
|
||||
},
|
||||
|
||||
onMouseDown () {
|
||||
this.bindEvents()
|
||||
this.touch.mouseDown = true
|
||||
},
|
||||
|
||||
onMouseMove (event) {
|
||||
if (this.touch.mouseDown) {
|
||||
// Prevent scrolling while touch dragging (only works with an active event, eg. passive: false).
|
||||
event.preventDefault()
|
||||
this.touch.dragging = true
|
||||
this.$set(this.movableSplitter, 'visibility', 'visible')
|
||||
this.moveSplitter(event)
|
||||
}
|
||||
event.preventDefault()
|
||||
this.dragging = true
|
||||
this.movableSplitter.visibility = 'visible'
|
||||
this.moveSplitter(event)
|
||||
},
|
||||
|
||||
onMouseUp () {
|
||||
this.touch.mouseDown = false
|
||||
if (this.touch.dragging) {
|
||||
if (this.dragging) {
|
||||
const dragPercentage = this.horizontal
|
||||
? this.movableSplitter.top
|
||||
: this.movableSplitter.left
|
||||
@@ -186,65 +179,31 @@ export default {
|
||||
left: 0,
|
||||
visibility: 'hidden'
|
||||
}
|
||||
|
||||
this.dragging = false
|
||||
}
|
||||
|
||||
// Keep dragging flag until click event is finished (click happens immediately after mouseup)
|
||||
// in order to prevent emitting `splitter-click` event if splitter was dragged.
|
||||
setTimeout(() => {
|
||||
this.touch.dragging = false
|
||||
this.unbindEvents()
|
||||
}, 100)
|
||||
},
|
||||
|
||||
// Get the cursor position relative to the splitpane container.
|
||||
getCurrentMouseDrag (event) {
|
||||
const rect = this.container.getBoundingClientRect()
|
||||
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
|
||||
? event.touches[0]
|
||||
: event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
|
||||
// if the sum of size of the 2 cells is 60%, the dragPercentage range will be 0 to 100% of this 60%.
|
||||
getCurrentDragPercentage (drag) {
|
||||
drag = drag[this.horizontal ? 'y' : 'x']
|
||||
// In the code bellow 'size' refers to 'width' for vertical and 'height' for horizontal layout.
|
||||
const containerSize = this.container[this.horizontal ? 'clientHeight' : 'clientWidth']
|
||||
return drag * 100 / containerSize
|
||||
this.unbindEvents()
|
||||
},
|
||||
|
||||
moveSplitter (event) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(this.getCurrentMouseDrag(event))
|
||||
const paneBefore = this.paneBefore
|
||||
const paneAfter = this.paneAfter
|
||||
|
||||
const paneBeforeMaxReached = paneBefore.max < 100 && (dragPercentage >= paneBefore.max)
|
||||
const paneAfterMaxReached = paneAfter.max < 100 && (dragPercentage <= 100 - paneAfter.max)
|
||||
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
if (paneBeforeMaxReached) {
|
||||
this.$set(this.movableSplitter, dir, paneBefore.max)
|
||||
} else {
|
||||
this.$set(this.movableSplitter, dir, Math.max(100 - paneAfter.max, 0))
|
||||
}
|
||||
} else {
|
||||
this.$set(this.movableSplitter, dir, Math.min(Math.max(dragPercentage, 0), paneBefore.max))
|
||||
const splitterInfo = {
|
||||
container: this.container,
|
||||
paneBeforeMax: this.paneBefore.max,
|
||||
paneAfterMax: this.paneAfter.max,
|
||||
isHorisontal: this.horizontal
|
||||
}
|
||||
const offset = splitter.calculateOffset(event, splitterInfo)
|
||||
const dir = this.horizontal ? 'top' : 'left'
|
||||
this.movableSplitter[dir] = offset
|
||||
},
|
||||
togglePane (toggledPane) {
|
||||
const pane = toggledPane === 'before' ? this.paneBefore : this.paneAfter
|
||||
|
||||
togglePane (pane) {
|
||||
if (pane.size > 0) {
|
||||
this.beforeMinimising.before = this.paneBefore.size
|
||||
this.beforeMinimising.after = this.paneAfter.size
|
||||
pane.size = 0
|
||||
const otherPane = toggledPane === 'before' ? this.paneAfter : this.paneBefore
|
||||
const otherPane = pane === this.paneBefore ? this.paneAfter : this.paneBefore
|
||||
otherPane.size = 100 - pane.size
|
||||
} else {
|
||||
this.paneBefore.size = this.beforeMinimising.before
|
||||
@@ -265,11 +224,11 @@ export default {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.splitpanes--vertical {flex-direction: row;}
|
||||
.splitpanes--horizontal {flex-direction: column;}
|
||||
.splitpanes--dragging * {user-select: none;}
|
||||
.splitpanes-vertical {flex-direction: row;}
|
||||
.splitpanes-horizontal {flex-direction: column;}
|
||||
.splitpanes-dragging * {user-select: none;}
|
||||
|
||||
.splitpanes__pane {
|
||||
.splitpanes-pane {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
@@ -277,16 +236,16 @@ export default {
|
||||
|
||||
/* Splitter */
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical.splitpanes--dragging {
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical.splitpanes-dragging {
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal.splitpanes--dragging {
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal.splitpanes-dragging {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.splitpanes__splitter {
|
||||
.splitpanes-splitter {
|
||||
touch-action: none;
|
||||
background-color: var(--color-bg-light);
|
||||
box-sizing: border-box;
|
||||
@@ -295,12 +254,12 @@ export default {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter {
|
||||
.splitpanes-horizontal > .splitpanes-splitter {
|
||||
border-top: 1px solid var(--color-border-light);
|
||||
border-bottom: 1px solid var(--color-border-light);
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter {
|
||||
.splitpanes-vertical > .splitpanes-splitter {
|
||||
border-left: 1px solid var(--color-border-light);
|
||||
border-right: 1px solid var(--color-border-light);
|
||||
}
|
||||
@@ -310,15 +269,15 @@ export default {
|
||||
background-color:rgba(162, 177, 198, 0.5);
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter,
|
||||
.splitpanes--vertical > .movable-splitter {
|
||||
.splitpanes-vertical > .splitpanes-splitter,
|
||||
.splitpanes-vertical > .movable-splitter {
|
||||
width: 8px;
|
||||
z-index: 5;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter,
|
||||
.splitpanes--horizontal > .movable-splitter {
|
||||
.splitpanes-horizontal > .splitpanes-splitter,
|
||||
.splitpanes-horizontal > .movable-splitter {
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
z-index: 5;
|
||||
@@ -332,11 +291,11 @@ export default {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns {
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns {
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
@@ -349,12 +308,12 @@ export default {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btn {
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btn {
|
||||
height: 49px;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btn {
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btn {
|
||||
width: 49px;
|
||||
height: 8px;
|
||||
}
|
||||
@@ -365,20 +324,20 @@ export default {
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
|
||||
}
|
||||
|
||||
.splitpanes--horizontal > .splitpanes__splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
|
||||
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
|
||||
}
|
||||
|
||||
.splitpanes--vertical > .splitpanes__splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
|
||||
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
|
||||
margin-top: -1px;
|
||||
}
|
||||
36
src/components/Splitpanes/splitter.js
Normal file
@@ -0,0 +1,36 @@
|
||||
export default {
|
||||
// Get the cursor position relative to the splitpane container.
|
||||
getCurrentMouseDrag (event, container) {
|
||||
const rect = container.getBoundingClientRect()
|
||||
const { clientX, clientY } = ('ontouchstart' in window && event.touches)
|
||||
? event.touches[0]
|
||||
: event
|
||||
return {
|
||||
x: clientX - rect.left,
|
||||
y: clientY - rect.top
|
||||
}
|
||||
},
|
||||
|
||||
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
|
||||
getCurrentDragPercentage (event, container, isHorisontal) {
|
||||
let drag = this.getCurrentMouseDrag(event, container)
|
||||
drag = drag[isHorisontal ? 'y' : 'x']
|
||||
const containerSize = container[isHorisontal ? 'clientHeight' : 'clientWidth']
|
||||
return drag * 100 / containerSize
|
||||
},
|
||||
|
||||
// Returns the new position in percents.
|
||||
calculateOffset (event, { container, isHorisontal, paneBeforeMax, paneAfterMax }) {
|
||||
const dragPercentage = this.getCurrentDragPercentage(event, container, isHorisontal)
|
||||
|
||||
const paneBeforeMaxReached = paneBeforeMax < 100 && (dragPercentage >= paneBeforeMax)
|
||||
const paneAfterMaxReached = paneAfterMax < 100 && (dragPercentage <= 100 - paneAfterMax)
|
||||
|
||||
// Prevent dragging beyond pane max.
|
||||
if (paneBeforeMaxReached || paneAfterMaxReached) {
|
||||
return paneBeforeMaxReached ? paneBeforeMax : Math.max(100 - paneAfterMax, 0)
|
||||
} else {
|
||||
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
<template>
|
||||
<div class="codemirror-container">
|
||||
<codemirror v-model="query" :options="cmOptions" @changes="onCmChange" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CM from 'codemirror'
|
||||
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.js'
|
||||
import 'codemirror/addon/hint/show-hint.css'
|
||||
import 'codemirror/addon/hint/sql-hint.js'
|
||||
import { debounce } from 'debounce'
|
||||
|
||||
const sqlHint = CM.hint.sql
|
||||
CM.hint.sql = (cm, options) => {
|
||||
const token = cm.getTokenAt(cm.getCursor()).string.toUpperCase()
|
||||
const result = sqlHint(cm, options)
|
||||
// Don't show the hint if there is only one option
|
||||
// and the token is already completed with this option
|
||||
if (result.list.length === 1 && result.list[0].text.toUpperCase() === token) {
|
||||
result.list = []
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
tables () {
|
||||
const tables = {}
|
||||
if (this.$store.state.schema) {
|
||||
this.$store.state.schema.forEach(table => {
|
||||
tables[table.name] = table.columns.map(column => column.name)
|
||||
})
|
||||
}
|
||||
return tables
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
query () {
|
||||
this.$emit('input', this.query)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onCmChange: debounce(function (editor) {
|
||||
// Don't show autocomplete after a space or semicolon or in string literals
|
||||
const ch = editor.getTokenAt(editor.getCursor()).string.slice(-1)
|
||||
const tokenType = editor.getTokenAt(editor.getCursor()).type
|
||||
if (tokenType === 'string' || !ch || ch === ' ' || ch === ';') {
|
||||
return
|
||||
}
|
||||
|
||||
const hintOptions = {
|
||||
tables: this.tables,
|
||||
completeSingle: false,
|
||||
completeOnSingleClick: true,
|
||||
alignWithWord: false
|
||||
}
|
||||
|
||||
CM.showHint(editor, CM.hint.sql, hintOptions)
|
||||
}, 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>
|
||||
@@ -49,6 +49,7 @@ export default {
|
||||
.paginator-continer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
>>> .paginator-page-link {
|
||||
padding: 2px 3px;
|
||||
@@ -17,20 +17,21 @@
|
||||
class="table-container"
|
||||
ref="table-container"
|
||||
@scroll="onScrollTable"
|
||||
:style="{maxHeight: `${height}px`}"
|
||||
>
|
||||
<table ref="table">
|
||||
<table ref="table" class="sqliteviz-table">
|
||||
<thead>
|
||||
<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>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(row,index) in currentPageData" :key="index">
|
||||
<td v-for="(value, valIndex) in row" :key="valIndex">
|
||||
<div class="cell-data" :style="cellStyle">{{ value }}</div>
|
||||
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
|
||||
<td v-for="(col, colIndex) in columns" :key="colIndex">
|
||||
<div class="cell-data" :style="cellStyle">
|
||||
{{ dataSet.values[col][rowIndex - 1 + currentPageData.start] }}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -39,7 +40,9 @@
|
||||
</div>
|
||||
<div class="table-footer">
|
||||
<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="time">in {{ time }}</span>
|
||||
</div>
|
||||
<pager v-show="pageCount > 1" :page-count="pageCount" v-model="currentPage" />
|
||||
</div>
|
||||
@@ -47,12 +50,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Pager from '@/components/Pager'
|
||||
import Pager from './Pager'
|
||||
|
||||
export default {
|
||||
name: 'SqlTable',
|
||||
components: { Pager },
|
||||
props: ['dataSet', 'height'],
|
||||
props: {
|
||||
dataSet: Object,
|
||||
time: String,
|
||||
pageSize: {
|
||||
type: Number,
|
||||
default: 20
|
||||
},
|
||||
preview: Boolean
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
header: null,
|
||||
@@ -62,20 +73,30 @@ export default {
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
columns () {
|
||||
return this.dataSet.columns
|
||||
},
|
||||
rowCount () {
|
||||
return this.dataSet.values[this.columns[0]].length
|
||||
},
|
||||
cellStyle () {
|
||||
const eq = this.tableWidth / this.dataSet.columns.length
|
||||
|
||||
const eq = this.tableWidth / this.columns.length
|
||||
return { maxWidth: `${Math.max(eq, 100)}px` }
|
||||
},
|
||||
pageSize () {
|
||||
return Math.max(Math.floor(this.height / 40), 20)
|
||||
},
|
||||
pageCount () {
|
||||
return Math.ceil(this.dataSet.values.length / this.pageSize)
|
||||
return Math.ceil(this.rowCount / this.pageSize)
|
||||
},
|
||||
currentPageData () {
|
||||
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: {
|
||||
@@ -89,9 +110,6 @@ export default {
|
||||
},
|
||||
onScrollTable () {
|
||||
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
|
||||
},
|
||||
functionName () {
|
||||
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
@@ -1,192 +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">
|
||||
Run your query and get results here
|
||||
</div>
|
||||
<div v-show="isGettingResults" class="table-preview">
|
||||
Fetching results...
|
||||
</div>
|
||||
<div v-show="result === undefined && !isGettingResults && !error" class="table-preview">
|
||||
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('updateTabState', { index: tabIndex, isUnsaved: true })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</splitpanes>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SqlTable from '@/components/SqlTable'
|
||||
import SqlEditor from '@/components/SqlEditor'
|
||||
import Splitpanes from '@/components/Splitpanes'
|
||||
import ViewSwitcher from '@/components/ViewSwitcher'
|
||||
import Chart from '@/components/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('updateTabState', { index: this.tabIndex, isUnsaved: true })
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// Run a command in the database
|
||||
execute () {
|
||||
// this.$refs.output.textContent = 'Fetching results...' */
|
||||
this.isGettingResults = true
|
||||
this.result = null
|
||||
this.error = null
|
||||
this.$db.execute(this.query + ';')
|
||||
.then(result => {
|
||||
this.result = result
|
||||
})
|
||||
.catch(err => {
|
||||
this.error = err
|
||||
})
|
||||
.finally(() => {
|
||||
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)
|
||||
},
|
||||
getChartSatateForSave () {
|
||||
return this.$refs.chart.getChartSatateForSave()
|
||||
}
|
||||
}
|
||||
}
|
||||
</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>
|
||||
@@ -1,22 +1,28 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="['text-field-label', { error: errorMsg }]">{{ label }}</div>
|
||||
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]">
|
||||
{{ label }}
|
||||
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="placeholder"
|
||||
:class="{ error: errorMsg }"
|
||||
:style="{ width: width }"
|
||||
:value="value"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
:disabled="disabled"
|
||||
@input="$emit('input', $event.target.value)"
|
||||
/>
|
||||
<div class="text-field-error">{{ errorMsg }}</div>
|
||||
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import HintIcon from '@/components/svg/hint'
|
||||
export default {
|
||||
name: 'textField',
|
||||
props: ['placeholder', 'label', 'errorMsg', 'value', 'width']
|
||||
props: ['placeholder', 'label', 'errorMsg', 'value', 'width', 'hint', 'maxHintWidth', 'disabled'],
|
||||
components: { HintIcon }
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -30,6 +36,7 @@ input {
|
||||
padding: 0 8px;
|
||||
font-size: 13px;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
@@ -40,24 +47,47 @@ input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: var(--color-bg-light);
|
||||
color: var(--color-text-light-2);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input.error {
|
||||
border-color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-base);
|
||||
padding-left: 8px;
|
||||
margin-bottom: 2px;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.text-field-label .hint{
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -22px;
|
||||
}
|
||||
|
||||
.text-field-label.error {
|
||||
color: var(--color-text-error);
|
||||
}
|
||||
|
||||
.text-field-label.disabled {
|
||||
color: var(--color-text-light-2);
|
||||
}
|
||||
|
||||
.text-field-error {
|
||||
color: var(--color-text-error);
|
||||
font-size: 12px;
|
||||
padding-left: 8px;
|
||||
margin-top: 2px;
|
||||
position: absolute;
|
||||
}
|
||||
.text-field-error:first-letter {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
67
src/components/svg/addTable.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<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>
|
||||
@@ -6,27 +6,33 @@
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Change database
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
Load another database or CSV
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'changeDbIcon',
|
||||
mixins: [tooltipMixin]
|
||||
mixins: [tooltipMixin],
|
||||
methods: {
|
||||
onClick () {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
22
src/components/svg/chart.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<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>
|
||||
41
src/components/svg/clear.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="['clear-icon', {'disabled': disabled}]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<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"
|
||||
fill="#C8D4E3"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'ClearIcon',
|
||||
props: ['disabled']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.clear-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.clear-icon:hover path {
|
||||
fill: var(--color-danger);
|
||||
}
|
||||
|
||||
.disabled.clear-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled.clear-icon:hover path {
|
||||
fill: #C8D4E3;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<svg
|
||||
@click.stop="$emit('click')"
|
||||
class="icon"
|
||||
:class="['icon', {'disabled': disabled }]"
|
||||
:width="size"
|
||||
:height="size"
|
||||
viewBox="0 0 14 14"
|
||||
@@ -23,6 +23,11 @@ export default {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 14
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,6 +37,9 @@ export default {
|
||||
.icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
.icon:hover path {
|
||||
fill: var(--color-text-base);
|
||||
}
|
||||
|
||||
19
src/components/svg/dataView.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<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>
|
||||
38
src/components/svg/dropDownChevron.vue
Normal file
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<svg
|
||||
:class="['chevron-icon', {'disabled': disabled}]"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
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"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'DropDownChevron',
|
||||
props: ['disabled']
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chevron-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chevron-icon:hover path {
|
||||
fill: var(--color-accent);
|
||||
}
|
||||
|
||||
.disabled.chevron-icon {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled.chevron-icon:hover path {
|
||||
fill: #C8D4E3;
|
||||
}
|
||||
</style>
|
||||
@@ -6,35 +6,42 @@
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@click.stop="$emit('click')"
|
||||
@mouseover="showTooltip"
|
||||
@mouseout="$set(tooltipStyle, 'visibility', 'hidden')"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip($event, tooltipPosition)"
|
||||
@mouseleave="hideTooltip"
|
||||
>
|
||||
<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"
|
||||
fill="#A2B1C6"
|
||||
/>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle">
|
||||
Export query to file
|
||||
</span>
|
||||
</svg>
|
||||
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
|
||||
{{ tooltip }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/mixins/tooltips'
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'ExportIcon',
|
||||
mixins: [tooltipMixin]
|
||||
mixins: [tooltipMixin],
|
||||
props: ['tooltip', 'tooltipPosition'],
|
||||
methods: {
|
||||
onClick () {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.icon {
|
||||
vertical-align: middle;
|
||||
display: block;
|
||||
margin: 0 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon:hover path {
|
||||
|
||||
54
src/components/svg/hint.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<div>
|
||||
<svg
|
||||
class="hint-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
@click.stop="onClick"
|
||||
@mouseenter="showTooltip"
|
||||
@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 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>
|
||||
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip">
|
||||
{{ hint }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import tooltipMixin from '@/tooltipMixin'
|
||||
|
||||
export default {
|
||||
name: 'HintIcon',
|
||||
props: ['hint', 'maxWidth'],
|
||||
mixins: [tooltipMixin],
|
||||
methods: {
|
||||
onClick () {
|
||||
this.hideTooltip()
|
||||
this.$emit('click')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.hint-icon {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.hint-icon:hover path {
|
||||
fill: var(--color-text-base);
|
||||
}
|
||||
.icon-tooltip {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
height: auto;
|
||||
line-height: normal;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
</style>
|
||||
20
src/components/svg/pivot.vue
Normal file
@@ -0,0 +1,20 @@
|
||||
<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>
|
||||
5
src/components/svg/png.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686 14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231 5.51953 9 5.51953ZM9 12.9023C7.35226 12.9023 6.01172 11.5618 6.01172 9.91406C6.01172 8.26632 7.35226 6.92578 9 6.92578C10.6477 6.92578 11.9883 8.26632 11.9883 9.91406C11.9883 11.5618 10.6477 12.9023 9 12.9023Z" fill="#A2B1C6"/>
|
||||
<path d="M15.8906 3.41016H13.304C13.2221 3.41016 13.1483 3.36547 13.1104 3.29319L12.3948 1.78945C12.3928 1.78534 12.3908 1.78126 12.3887 1.77718C12.1117 1.22312 11.5548 0.878906 10.9353 0.878906H7.11478C6.49529 0.878906 5.93835 1.22312 5.66135 1.77722C5.65928 1.7813 5.65731 1.78538 5.65534 1.78949L4.9397 3.2933C4.90173 3.36547 4.82797 3.41016 4.74609 3.41016H2.10938C0.946266 3.41016 0 4.35642 0 5.51953V15.0117C0 16.1748 0.946266 17.1211 2.10938 17.1211H15.8906C17.0537 17.1211 18 16.1748 18 15.0117V5.51953C18 4.35642 17.0537 3.41016 15.8906 3.41016ZM16.5938 15.0117C16.5938 15.3994 16.2783 15.7148 15.8906 15.7148H2.10938C1.72167 15.7148 1.40625 15.3994 1.40625 15.0117V5.51953C1.40625 5.13183 1.72167 4.81641 2.10938 4.81641H4.74609C5.36555 4.81641 5.92249 4.47223 6.19952 3.91816C6.2016 3.91409 6.20357 3.90997 6.20557 3.90586L6.92121 2.40205C6.95914 2.32984 7.0329 2.28516 7.11478 2.28516H10.9353C11.0172 2.28516 11.091 2.32984 11.1289 2.40202L11.8445 3.90582C11.8465 3.90994 11.8485 3.91405 11.8506 3.91813C12.1276 4.47219 12.6846 4.81637 13.304 4.81637H15.8906C16.2783 4.81637 16.5938 5.13179 16.5938 5.5195V15.0117Z" fill="#A2B1C6"/>
|
||||
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
18
src/components/svg/png.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<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>
|
||||
17
src/components/svg/run.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<svg
|
||||
width="12"
|
||||
height="13"
|
||||
viewBox="0 0 12 13"
|
||||
fill="none"
|
||||
>
|
||||
<path d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z" fill="#A2B1C6"/>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: 'RunIcon'
|
||||
}
|
||||
</script>
|
||||
45
src/components/svg/sort.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<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>
|
||||
25
src/components/svg/sqlEditor.vue
Normal file
@@ -0,0 +1,25 @@
|
||||
<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>
|
||||
21
src/components/svg/table.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<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>
|
||||
@@ -1,47 +0,0 @@
|
||||
import store from '@/store'
|
||||
const worker = new Worker('js/worker.sql-wasm.js')
|
||||
|
||||
export default {
|
||||
loadDb (file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const dbName = file.name
|
||||
store.commit('saveDbName', dbName)
|
||||
const f = file
|
||||
const r = new FileReader()
|
||||
r.onload = () => {
|
||||
// on 'action: open' completed
|
||||
worker.onmessage = () => {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';`
|
||||
|
||||
// on 'action: exec' completed
|
||||
worker.onmessage = event => {
|
||||
resolve(event.data.results[0].values)
|
||||
}
|
||||
worker.postMessage({ action: 'exec', sql: getSchemaSql })
|
||||
}
|
||||
|
||||
try {
|
||||
worker.postMessage({ action: 'open', buffer: r.result }, [r.result])
|
||||
} catch (exception) {
|
||||
worker.postMessage({ action: 'open', buffer: r.result })
|
||||
}
|
||||
}
|
||||
r.readAsArrayBuffer(f)
|
||||
})
|
||||
},
|
||||
execute (commands) {
|
||||
return new Promise((resolve, reject) => {
|
||||
worker.onmessage = (event) => {
|
||||
if (event.data.error) {
|
||||
reject(event.data.error)
|
||||
}
|
||||
// if it was more than one select - take only the first one
|
||||
resolve(event.data.results[0])
|
||||
}
|
||||
worker.postMessage({ action: 'exec', sql: commands })
|
||||
})
|
||||
}
|
||||
}
|
||||
100
src/lib/database/_sql.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import initSqlJs from 'sql.js/dist/sql-wasm.js'
|
||||
import dbUtils from './_statements'
|
||||
|
||||
let SQL = null
|
||||
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 {
|
||||
constructor () {
|
||||
this.db = null
|
||||
}
|
||||
|
||||
static build () {
|
||||
return sqlModuleReady
|
||||
.then(() => {
|
||||
return new Sql()
|
||||
})
|
||||
}
|
||||
|
||||
createDb (buffer) {
|
||||
if (this.db != null) this.db.close()
|
||||
this.db = new SQL.Database(buffer)
|
||||
return this.db
|
||||
}
|
||||
|
||||
open (buffer) {
|
||||
this.createDb(buffer && new Uint8Array(buffer))
|
||||
return {
|
||||
ready: true
|
||||
}
|
||||
}
|
||||
|
||||
exec (sql, params) {
|
||||
if (this.db === null) {
|
||||
this.createDb()
|
||||
}
|
||||
if (!sql) {
|
||||
throw new Error('exec: Missing query string')
|
||||
}
|
||||
const sqlResults = this.db.exec(sql, params)
|
||||
return sqlResults.map(result => {
|
||||
return {
|
||||
columns: result.columns,
|
||||
values: _getDataSourcesFromSqlResult(result)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
|
||||
if (this.db === null) {
|
||||
this.createDb()
|
||||
}
|
||||
const columns = data.columns
|
||||
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
|
||||
const insertStr = dbUtils.getInsertStmt(tabName, columns)
|
||||
const insertStmt = this.db.prepare(insertStr)
|
||||
|
||||
progressCallback({ progress: 0, id: progressCounterId })
|
||||
for (const chunk of chunks) {
|
||||
this.db.exec('BEGIN')
|
||||
for (const row of chunk) {
|
||||
insertStmt.run(row)
|
||||
}
|
||||
this.db.exec('COMMIT')
|
||||
count++
|
||||
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId })
|
||||
}
|
||||
|
||||
return {
|
||||
finish: true
|
||||
}
|
||||
}
|
||||
|
||||
export () {
|
||||
return this.db.export()
|
||||
}
|
||||
|
||||
close () {
|
||||
if (this.db) {
|
||||
this.db.close()
|
||||
}
|
||||
return {
|
||||
finished: true
|
||||
}
|
||||
}
|
||||
}
|
||||
95
src/lib/database/_statements.js
Normal file
@@ -0,0 +1,95 @@
|
||||
import sqliteParser from 'sqlite-parser'
|
||||
|
||||
export default {
|
||||
* generateChunks (data, 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++) {
|
||||
const start = size * i
|
||||
const end = start + size
|
||||
yield transposedMatrix.slice(start, end)
|
||||
}
|
||||
},
|
||||
|
||||
getInsertStmt (tabName, columns) {
|
||||
const colList = `"${columns.join('", "')}"`
|
||||
const params = columns.map(() => '?').join(', ')
|
||||
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
|
||||
},
|
||||
|
||||
getCreateStatement (tabName, data) {
|
||||
let result = `CREATE table "${tabName}"(`
|
||||
for (const col in data) {
|
||||
// Get the first row of values to determine types
|
||||
const value = data[col][0]
|
||||
let type = ''
|
||||
switch (typeof value) {
|
||||
case 'number': {
|
||||
type = 'REAL'
|
||||
break
|
||||
}
|
||||
case 'boolean': {
|
||||
type = 'INTEGER'
|
||||
break
|
||||
}
|
||||
case 'string': {
|
||||
type = 'TEXT'
|
||||
break
|
||||
}
|
||||
default: type = 'TEXT'
|
||||
}
|
||||
result += `"${col}" ${type}, `
|
||||
}
|
||||
|
||||
result = result.replace(/,\s$/, ');')
|
||||
return result
|
||||
},
|
||||
|
||||
getAst (sql) {
|
||||
// There is a bug is sqlite-parser
|
||||
// It throws an error if tokenizer has an arguments:
|
||||
// https://github.com/codeschool/sqlite-parser/issues/59
|
||||
const fixedSql = sql
|
||||
.replace(/(tokenize=[^,]+)"tokenchars=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"remove_diacritics=.+?"/, '$1')
|
||||
.replace(/(tokenize=[^,]+)"separators=.+?"/, '$1')
|
||||
.replace(/tokenize=.+?(,|\))/, 'tokenize=unicode61$1')
|
||||
|
||||
return sqliteParser(fixedSql)
|
||||
},
|
||||
|
||||
/*
|
||||
* Return an array of columns with name and type. E.g.:
|
||||
* [
|
||||
* { name: 'id', type: 'INTEGER' },
|
||||
* { name: 'title', type: 'NVARCHAR(30)' },
|
||||
* ]
|
||||
*/
|
||||
getColumns (sql) {
|
||||
const columns = []
|
||||
const ast = this.getAst(sql)
|
||||
|
||||
const columnDefinition = ast.statement[0].format === 'table'
|
||||
? ast.statement[0].definition
|
||||
: ast.statement[0].result.args.expression // virtual table
|
||||
|
||||
columnDefinition.forEach(item => {
|
||||
if (item.variant === 'column' && ['identifier', 'definition'].includes(item.type)) {
|
||||
let type = item.datatype ? item.datatype.variant : 'N/A'
|
||||
if (item.datatype && item.datatype.args) {
|
||||
type = type + '(' + item.datatype.args.expression[0].value
|
||||
if (item.datatype.args.expression.length === 2) {
|
||||
type = type + ', ' + item.datatype.args.expression[1].value
|
||||
}
|
||||
type = type + ')'
|
||||
}
|
||||
columns.push({ name: item.name, type: type })
|
||||
}
|
||||
})
|
||||
return columns
|
||||
}
|
||||
}
|
||||
41
src/lib/database/_worker.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import registerPromiseWorker from 'promise-worker/register'
|
||||
import Sql from './_sql'
|
||||
|
||||
const sqlReady = Sql.build()
|
||||
|
||||
function processMsg (sql) {
|
||||
const data = this
|
||||
switch (data && data.action) {
|
||||
case 'open':
|
||||
return sql.open(data.buffer)
|
||||
case 'reopen':
|
||||
return sql.open(sql.export())
|
||||
case 'exec':
|
||||
return sql.exec(data.sql, data.params)
|
||||
case 'import':
|
||||
return sql.import(
|
||||
data.tabName,
|
||||
data.data,
|
||||
data.progressCounterId,
|
||||
postMessage
|
||||
)
|
||||
case 'export':
|
||||
return sql.export()
|
||||
case 'close':
|
||||
return sql.close()
|
||||
default:
|
||||
throw new Error('Invalid action : ' + (data && data.action))
|
||||
}
|
||||
}
|
||||
|
||||
function onError (error) {
|
||||
return {
|
||||
error: error.message
|
||||
}
|
||||
}
|
||||
|
||||
registerPromiseWorker(data => {
|
||||
return sqlReady
|
||||
.then(processMsg.bind(data))
|
||||
.catch(onError)
|
||||
})
|
||||
146
src/lib/database/index.js
Normal file
@@ -0,0 +1,146 @@
|
||||
import stms from './_statements'
|
||||
import fu from '@/lib/utils/fileIo'
|
||||
// We can import workers like so because of worker-loader:
|
||||
// https://webpack.js.org/loaders/worker-loader/
|
||||
import Worker from './_worker.js'
|
||||
|
||||
// Use promise-worker in order to turn worker into the promise based one:
|
||||
// https://github.com/nolanlawson/promise-worker
|
||||
import PromiseWorker from 'promise-worker'
|
||||
|
||||
function getNewDatabase () {
|
||||
const worker = new Worker()
|
||||
return new Database(worker)
|
||||
}
|
||||
|
||||
export default {
|
||||
getNewDatabase
|
||||
}
|
||||
|
||||
let progressCounterIds = 0
|
||||
class Database {
|
||||
constructor (worker) {
|
||||
this.dbName = null
|
||||
this.schema = null
|
||||
this.worker = worker
|
||||
this.pw = new PromiseWorker(worker)
|
||||
|
||||
this.importProgresses = {}
|
||||
worker.addEventListener('message', e => {
|
||||
const progress = e.data.progress
|
||||
if (progress !== undefined) {
|
||||
const id = e.data.id
|
||||
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
|
||||
detail: progress
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
shutDown () {
|
||||
this.worker.terminate()
|
||||
}
|
||||
|
||||
createProgressCounter (callback) {
|
||||
const id = progressCounterIds++
|
||||
this.importProgresses[id] = new EventTarget()
|
||||
this.importProgresses[id].addEventListener('progress', e => { callback(e.detail) })
|
||||
return id
|
||||
}
|
||||
|
||||
deleteProgressCounter (id) {
|
||||
delete this.importProgresses[id]
|
||||
}
|
||||
|
||||
async addTableFromCsv (tabName, data, progressCounterId) {
|
||||
const result = await this.pw.postMessage({
|
||||
action: 'import',
|
||||
data,
|
||||
progressCounterId,
|
||||
tabName
|
||||
})
|
||||
|
||||
if (result.error) {
|
||||
throw new Error(result.error)
|
||||
}
|
||||
this.dbName = this.dbName || 'database'
|
||||
this.refreshSchema()
|
||||
}
|
||||
|
||||
async loadDb (file) {
|
||||
const fileContent = file ? await fu.readAsArrayBuffer(file) : null
|
||||
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent })
|
||||
|
||||
if (res.error) {
|
||||
throw new Error(res.error)
|
||||
}
|
||||
|
||||
this.dbName = file ? fu.getFileName(file) : 'database'
|
||||
this.refreshSchema()
|
||||
}
|
||||
|
||||
async refreshSchema () {
|
||||
const getSchemaSql = `
|
||||
SELECT name, sql
|
||||
FROM sqlite_master
|
||||
WHERE type='table' AND name NOT LIKE 'sqlite_%';
|
||||
`
|
||||
const result = await this.execute(getSchemaSql)
|
||||
// Parse DDL statements to get column names and types
|
||||
const parsedSchema = []
|
||||
if (result && result.values && result.values.name) {
|
||||
result.values.name.forEach((table, index) => {
|
||||
parsedSchema.push({
|
||||
name: table,
|
||||
columns: stms.getColumns(result.values.sql[index])
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Refresh schema
|
||||
this.schema = parsedSchema
|
||||
}
|
||||
|
||||
async execute (commands) {
|
||||
await this.pw.postMessage({ action: 'reopen' })
|
||||
const results = await this.pw.postMessage({ action: 'exec', sql: commands })
|
||||
|
||||
if (results.error) {
|
||||
throw new Error(results.error)
|
||||
}
|
||||
// if it was more than one select - take only the last one
|
||||
return results[results.length - 1]
|
||||
}
|
||||
|
||||
async export (fileName) {
|
||||
const data = await this.pw.postMessage({ action: 'export' })
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
fu.exportToFile(data, fileName)
|
||||
}
|
||||
|
||||
async validateTableName (name) {
|
||||
if (name.startsWith('sqlite_')) {
|
||||
throw new Error("Table name can't start with sqlite_")
|
||||
}
|
||||
|
||||
if (/[^\w]/.test(name)) {
|
||||
throw new Error('Table name can contain only letters, digits and underscores')
|
||||
}
|
||||
|
||||
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 _
|
||||
}
|
||||
}
|
||||
12
src/lib/storedInquiries/_migrations.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export default {
|
||||
_migrate (installedVersion, inquiries) {
|
||||
if (installedVersion === 1) {
|
||||
inquiries.forEach(inquire => {
|
||||
inquire.viewType = 'chart'
|
||||
inquire.viewOptions = inquire.chart
|
||||
delete inquire.chart
|
||||
})
|
||||
return inquiries
|
||||
}
|
||||
}
|
||||
}
|
||||
120
src/lib/storedInquiries/index.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/lib/utils/fileIo.js
Normal file
@@ -0,0 +1,86 @@
|
||||
export default {
|
||||
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
|
||||
const downloader = document.createElement('a')
|
||||
downloader.href = url
|
||||
downloader.download = fileName
|
||||
|
||||
// Trigger click
|
||||
downloader.click()
|
||||
|
||||
// Clean up
|
||||
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
|
||||
* it will be an unsettled promise. But it's grabbed by
|
||||
* the garbage collector (tested with FinalizationRegistry).
|
||||
*/
|
||||
getFileFromUser (type) {
|
||||
return new Promise(resolve => {
|
||||
const uploader = document.createElement('input')
|
||||
|
||||
uploader.type = 'file'
|
||||
uploader.accept = type
|
||||
|
||||
uploader.addEventListener('change', () => {
|
||||
const file = uploader.files[0]
|
||||
resolve(file)
|
||||
})
|
||||
|
||||
uploader.click()
|
||||
})
|
||||
},
|
||||
|
||||
importFile () {
|
||||
const reader = new FileReader()
|
||||
|
||||
return this.getFileFromUser('.json')
|
||||
.then(file => {
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onload = e => {
|
||||
resolve(e.target.result)
|
||||
}
|
||||
reader.readAsText(file)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
readFile (path) {
|
||||
return fetch(path)
|
||||
},
|
||||
|
||||
readAsArrayBuffer (file) {
|
||||
const fileReader = new FileReader()
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
fileReader.onerror = () => {
|
||||
fileReader.abort()
|
||||
reject(new Error('Problem parsing input file.'))
|
||||
}
|
||||
|
||||
fileReader.onload = () => {
|
||||
resolve(fileReader.result)
|
||||
}
|
||||
fileReader.readAsArrayBuffer(file)
|
||||
})
|
||||
}
|
||||
}
|
||||
15
src/lib/utils/time.js
Normal file
@@ -0,0 +1,15 @@
|
||||
export default {
|
||||
getPeriod (start, end) {
|
||||
const diff = end.getTime() - start.getTime()
|
||||
const seconds = diff / 1000
|
||||
return seconds.toFixed(3) + 's'
|
||||
},
|
||||
|
||||
debounce (func, ms) {
|
||||
let timeout
|
||||
return function () {
|
||||
clearTimeout(timeout)
|
||||
timeout = setTimeout(() => func.apply(this, arguments), ms)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
src/main.js
@@ -1,10 +1,9 @@
|
||||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
import store from '@/store'
|
||||
import { VuePlugin } from 'vuera'
|
||||
import VModal from 'vue-js-modal'
|
||||
import db from '@/dataBase'
|
||||
|
||||
import '@/assets/styles/variables.css'
|
||||
import '@/assets/styles/buttons.css'
|
||||
@@ -12,12 +11,17 @@ import '@/assets/styles/tables.css'
|
||||
import '@/assets/styles/dialogs.css'
|
||||
import '@/assets/styles/tooltips.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)) {
|
||||
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
|
||||
}
|
||||
|
||||
Vue.use(VuePlugin)
|
||||
Vue.use(VModal)
|
||||
|
||||
Vue.config.productionTip = false
|
||||
Vue.prototype.$db = db
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export default {
|
||||
data () {
|
||||
return {
|
||||
tooltipStyle: {}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showTooltip (e) {
|
||||
this.tooltipStyle = {
|
||||
visibility: 'visible',
|
||||
position: 'fixed',
|
||||
top: e.clientY - 12 + 'px',
|
||||
left: e.clientX + 12 + 'px'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/registerServiceWorker.js
Normal file
@@ -0,0 +1,44 @@
|
||||
let refresh = false
|
||||
|
||||
function invokeServiceWorkerUpdateFlow (registration) {
|
||||
const agree = confirm('New version of the app is available. Refresh now?')
|
||||
if (agree) {
|
||||
if (registration.waiting) {
|
||||
// let waiting Service Worker know it should became active
|
||||
refresh = true
|
||||
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', async () => {
|
||||
const registration = await navigator.serviceWorker.register('service-worker.js')
|
||||
// ensure the case when the updatefound event was missed is also handled
|
||||
// by re-invoking the prompt when there's a waiting Service Worker
|
||||
if (registration.waiting) {
|
||||
invokeServiceWorkerUpdateFlow(registration)
|
||||
}
|
||||
|
||||
// detect Service Worker update available and wait for it to become installed
|
||||
registration.addEventListener('updatefound', () => {
|
||||
const newRegestration = registration.installing
|
||||
if (newRegestration) {
|
||||
// wait until the new Service worker is actually installed (ready to take over)
|
||||
newRegestration.addEventListener('statechange', () => {
|
||||
if (registration.waiting && navigator.serviceWorker.controller) {
|
||||
invokeServiceWorkerUpdateFlow(registration)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// detect controller change and refresh the page
|
||||
navigator.serviceWorker.addEventListener('controllerchange', () => {
|
||||
if (refresh) {
|
||||
window.location.reload()
|
||||
refresh = false
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
50
src/router.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Workspace from '@/views/Main/Workspace'
|
||||
import Inquiries from '@/views/Main/Inquiries'
|
||||
import Welcome from '@/views/Welcome'
|
||||
import Main from '@/views/Main'
|
||||
import store from '@/store'
|
||||
import database from '@/lib/database'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
component: Welcome
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Main',
|
||||
component: Main,
|
||||
children: [
|
||||
{
|
||||
path: '/workspace',
|
||||
name: 'Workspace',
|
||||
component: Workspace
|
||||
},
|
||||
{
|
||||
path: '/inquiries',
|
||||
name: 'Inquiries',
|
||||
component: Inquiries
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
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
|
||||
@@ -1,39 +0,0 @@
|
||||
import Vue from 'vue'
|
||||
import VueRouter from 'vue-router'
|
||||
import Editor from '../views/Editor'
|
||||
import MyQueries from '../views/MyQueries'
|
||||
import Home from '../views/Home'
|
||||
import MainView from '../views/MainView'
|
||||
|
||||
Vue.use(VueRouter)
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Welcome',
|
||||
component: Home
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'MainView',
|
||||
component: MainView,
|
||||
children: [
|
||||
{
|
||||
path: '/editor',
|
||||
name: 'Editor',
|
||||
component: Editor
|
||||
},
|
||||
{
|
||||
path: '/my-queries',
|
||||
name: 'MyQueries',
|
||||
component: MyQueries
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const router = new VueRouter({
|
||||
routes
|
||||
})
|
||||
|
||||
export default router
|
||||
32
src/store/actions.js
Normal file
@@ -0,0 +1,32 @@
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export default {
|
||||
async addTab ({ state }, data) {
|
||||
const tab = data ? JSON.parse(JSON.stringify(data)) : {}
|
||||
// If no data then create a new blank one...
|
||||
// No data.id means to create new tab, but not blank,
|
||||
// e.g. with 'select * from csv_import' inquiry after csv import
|
||||
if (!data || !data.id) {
|
||||
tab.id = nanoid()
|
||||
tab.name = null
|
||||
tab.tempName = state.untitledLastIndex
|
||||
? `Untitled ${state.untitledLastIndex}`
|
||||
: 'Untitled'
|
||||
tab.viewType = 'chart'
|
||||
tab.viewOptions = undefined
|
||||
tab.isSaved = false
|
||||
} else {
|
||||
tab.isSaved = true
|
||||
}
|
||||
|
||||
// add new tab only if was not already opened
|
||||
if (!state.tabs.some(openedTab => openedTab.id === tab.id)) {
|
||||
state.tabs.push(tab)
|
||||
if (!tab.name) {
|
||||
state.untitledLastIndex += 1
|
||||
}
|
||||
}
|
||||
|
||||
return tab.id
|
||||
}
|
||||
}
|
||||