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

38 Commits

Author SHA1 Message Date
lana-k
3ee825defe fix standart chart resize in pivot, improve performance 2025-04-03 22:36:50 +02:00
lana-k
77df3a8446 use only camel case for props 2025-03-30 21:01:06 +02:00
lana-k
559e04200c fix config name 2025-03-30 16:40:43 +02:00
lana-k
4568780526 fix release action 2025-03-30 16:33:23 +02:00
lana-k
fa9108bc08 add source maps 2025-03-30 16:23:51 +02:00
lana-k
df16383d49 linter 2025-03-30 15:57:47 +02:00
lana-k
6f7961e1b4 #63 update node and npm 2025-03-30 15:13:55 +02:00
lana-k
2741aa6f33 add titles, align row title to the left 2025-03-30 15:13:36 +02:00
lana-k
6ceac83db9 update version 2025-03-29 17:32:33 +01:00
lana-k
a46625ebe7 #122 add tests 2025-03-29 17:09:33 +01:00
lana-k
5ef0b32549 #63 test for lot resize 2025-03-29 16:09:33 +01:00
lana-k
f49fa0ea96 #63 test for chart updating 2025-03-29 13:32:20 +01:00
lana-k
108ae454c1 #122 add line wrapping 2025-03-26 22:18:55 +01:00
lana-k
43b6110c28 minimize column name cell in record view 2025-03-26 21:50:55 +01:00
lana-k
5a805fba80 fix plot update 2025-03-26 20:59:17 +01:00
lana-k
58cdab94c1 fix styles 2025-03-25 21:19:22 +01:00
lana-k
b3d81666be add more chunks 2025-03-25 21:18:23 +01:00
lana-k
fdf180d340 fix plot resize 2025-03-23 21:09:12 +01:00
lana-k
f2ff5aa2af slot comment 2025-03-20 22:17:16 +01:00
lana-k
0c1b91ab2f format 2025-03-20 22:04:15 +01:00
lana-k
5e2b34a856 fix codemirror styles 2025-03-17 21:43:07 +01:00
lana-k
24786c9069 add service worker 2025-03-16 23:04:03 +01:00
lana-k
c28d31b019 fix tests 2025-03-11 22:24:57 +01:00
lana-k
6009ebb447 fix resize 2025-03-09 22:26:53 +01:00
lana-k
b5504b91ce fix tests 2025-03-09 21:57:36 +01:00
lana-k
828cad6439 migrate to vite 2025-02-01 20:54:26 +01:00
lana-k
8fa3c2ae58 fix tests 2025-01-23 21:36:11 +01:00
lana-k
aa5c907095 Merge branch 'master' of github.com:lana-k/sqliteviz into migrate_to_vue3 2025-01-13 22:19:00 +01:00
lana-k
3a05b27400 #121 0.25.1 2025-01-12 22:03:06 +01:00
lana-k
108d96a753 #121 fix lint 2025-01-12 22:00:48 +01:00
lana-k
f55a8caa92 #121 tests 2025-01-12 21:42:17 +01:00
lana-k
87f9f9eb01 use actions, add store tests 2025-01-05 22:30:12 +01:00
lana-k
d6408bdd85 #121 save inquiries in store 2025-01-05 21:06:06 +01:00
lana-k
e14696b59e update tests 2025-01-05 17:13:55 +01:00
lana-k
eee67763b5 plotly in pivot 2024-12-10 21:41:07 +01:00
lana-k
637d8d26dd #63 migrate to Vue 3 2024-10-13 16:12:21 +02:00
lana-k
b30b2181e4 #63 update slot syntax 2024-10-05 15:43:22 +02:00
lana-k
378b9fb580 #113 upgrade plotly 2024-09-23 16:46:50 +02:00
170 changed files with 17907 additions and 40363 deletions

29
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,29 @@
module.exports = {
root: true,
env: {
node: true,
es2022: true
},
extends: ['eslint:recommended', 'plugin:vue/vue3-recommended', 'prettier'],
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-case-declarations': 'off',
'max-len': [2, 100, 4, { ignoreUrls: true }],
'vue/multi-word-component-names': 'off',
'vue/no-mutating-props': 'warn',
'vue/no-reserved-component-names': 'warn',
'vue/no-v-model-argument': 'off',
'vue/require-default-prop': 'off',
'vue/custom-event-name-casing': ['error', 'camelCase'],
'vue/attribute-hyphenation': ['error', 'never']
},
overrides: [
{
files: ['**/__tests__/*.{j,t}s?(x)', '**/tests/**/*.spec.{j,t}s?(x)'],
env: {
mocha: true
}
}
]
}

View File

@@ -1,30 +0,0 @@
module.exports = {
root: true,
env: {
node: true
},
extends: [
'plugin:vue/essential',
'@vue/standard'
],
parserOptions: {
parser: 'babel-eslint'
},
rules: {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-case-declarations': 'off',
'max-len': [2, 100, 4, { ignoreUrls: true }]
},
overrides: [
{
files: [
'**/__tests__/*.{j,t}s?(x)',
'**/tests/**/*.spec.{j,t}s?(x)'
],
env: {
mocha: true
}
}
]
}

14
.github/workflows/config.grenrc.cjs vendored Normal file
View File

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

View File

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

View File

@@ -3,43 +3,43 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: tags:
- '*' - '*'
jobs: jobs:
deploy: deploy:
name: Create release name: Create release
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10.x node-version: 18.x
- name: Update npm - name: Update npm
run: npm install -g npm@7 run: npm install -g npm@10
- name: npm install and build - name: npm install and build
run: | run: |
npm install npm install
npm run build npm run build
- name: Create archives - name: Create archives
run: | run: |
cd dist cd dist
zip -9 -r ../dist.zip . -x "js/*.map" -x "/*.map" zip -9 -r ../dist.zip . -x "*.map"
zip -9 -r ../dist_map.zip . zip -9 -r ../dist_map.zip .
- name: Create Release Notes - name: Create Release Notes
run: | run: |
npm install github-release-notes@0.16.0 -g npm install github-release-notes@0.16.0 -g
gren changelog --generate --config="/.github/workflows/config.grenrc.js" gren changelog --generate --config="/.github/workflows/config.grenrc.cjs"
env: env:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release - name: Create release
uses: ncipollo/release-action@v1 uses: ncipollo/release-action@v1
with: with:
artifacts: "dist.zip,dist_map.zip" artifacts: 'dist.zip,dist_map.zip'
token: ${{ secrets.GITHUB_TOKEN }} token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md" bodyFile: 'CHANGELOG.md'

View File

@@ -3,35 +3,35 @@ on:
workflow_dispatch: workflow_dispatch:
push: push:
branches: branches:
- 'master' - 'master'
pull_request: pull_request:
branches: branches:
- 'master' - 'master'
jobs: jobs:
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v1 uses: actions/setup-node@v1
with: with:
node-version: 10.x node-version: 18.x
- name: Install browsers - name: Install browsers
run: | run: |
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
sudo apt-get update sudo apt-get update
sudo apt-get install -y chromium-browser firefox sudo apt-get install -y chromium-browser firefox
- name: Update npm - name: Update npm
run: npm install -g npm@7 run: npm install -g npm@10
- name: Install the project - name: Install the project
run: npm install run: npm install
- name: Run lint - name: Run lint
run: npm run lint -- --no-fix run: npm run lint -- --no-fix
- name: Run karma tests - name: Run karma tests
run: npm run test run: npm run test

7
.prettierrc Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "none",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"arrowParens": "avoid"
}

View File

@@ -5,9 +5,10 @@
# sqliteviz # sqliteviz
Sqliteviz is a single-page offline-first PWA for fully client-side visualisation Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
of SQLite databases, CSV, JSON or NDJSON files. of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can: With sqliteviz you can:
- run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets - run SQL queries against a SQLite database and create [Plotly][11] charts and pivot tables based on the result sets
- import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data - import a CSV/JSON/NDJSON file into a SQLite database and visualize imported data
- export result set to CSV file - export result set to CSV file
@@ -19,15 +20,19 @@ With sqliteviz you can:
https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4 https://user-images.githubusercontent.com/24638357/128249848-f8fab0f5-9add-46e0-a9c1-dd5085a8623e.mp4
## Quickstart ## Quickstart
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6]. The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki ## Wiki
For user documentation, check out sqliteviz [documentation][7]. For user documentation, check out sqliteviz [documentation][7].
## Motivation ## Motivation
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2]. It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components ## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9]. It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
[1]: https://github.com/plotly/falcon [1]: https://github.com/plotly/falcon

View File

@@ -1,5 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: ['@vue/cli-plugin-babel/preset']
'@vue/cli-plugin-babel/preset'
]
} }

View File

@@ -1,12 +1,12 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.png"> <link rel="icon" href="favicon.png" />
<link rel="manifest" href="<%= BASE_URL %>manifest.webmanifest"> <link rel="manifest" href="manifest.webmanifest" />
<title><%= htmlWebpackPlugin.options.title %></title> <title>sqliteviz</title>
<style> <style>
#sqliteviz-loading-wrapper { #sqliteviz-loading-wrapper {
position: fixed; position: fixed;
@@ -38,15 +38,18 @@
#sqliteviz-loading-wrapper circle { #sqliteviz-loading-wrapper circle {
position: absolute; position: absolute;
left: 0; right: 0; top: 0; bottom: 0; left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none; fill: none;
stroke-width: 5px; stroke-width: 5px;
stroke-linecap: round; stroke-linecap: round;
stroke: #119DFF; stroke: #119dff;
} }
#sqliteviz-loading-wrapper circle.bg { #sqliteviz-loading-wrapper circle.bg {
stroke: #C8D4E3; stroke: #c8d4e3;
} }
#sqliteviz-loading-wrapper circle.front { #sqliteviz-loading-wrapper circle.front {
@@ -74,29 +77,22 @@
</head> </head>
<body> <body>
<noscript> <noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong> <strong>
We're sorry but this app doesn't work properly without JavaScript
enabled. Please enable it to continue.
</strong>
</noscript> </noscript>
<div id="app"> <div id="app">
<div id="sqliteviz-loading-wrapper"> <div id="sqliteviz-loading-wrapper">
<div id="sqliteviz-loading-text">LOADING</div> <div id="sqliteviz-loading-text">LOADING</div>
<svg height="170" width="170" viewBox="0 0 170 170"> <svg height="170" width="170" viewBox="0 0 170 170">
<circle <circle class="bg" cx="85" cy="85" r="80" />
class="bg" <circle class="front" cx="85" cy="85" r="80" />
cx="85"
cy="85"
r="80"
/>
<circle
class="front"
cx="85"
cy="85"
r="80"
/>
</svg> </svg>
</div> </div>
</div> </div>
<!-- extention slot start --> <!-- extention slot start -->
<!-- extention slot end --> <!-- extention slot end -->
<!-- built files will be auto injected --> <script type="module" src="/src/main.js"></script>
</body> </body>
</html> </html>

10
jsconfig.json Normal file
View File

@@ -0,0 +1,10 @@
{
"include": ["src/**/*", "tests/**/*"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@*": ["./src/*"]
}
}
}

122
karma.conf.cjs Normal file
View File

@@ -0,0 +1,122 @@
module.exports = function (config) {
config.set({
vite: {
config: {
resolve: {
alias: {
vue: 'vue/dist/vue.esm-bundler.js'
}
},
server: {
preTransformRequests: false
}
},
coverage: {
enable: true,
include: 'src/*',
exclude: ['node_modules', 'src/components/svg/*'],
extension: ['.js', '.vue'],
requireEnv: false
}
},
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon-chai', 'vite'],
// list of files / patterns to load in the browser
files: [
{
pattern: 'test.setup.js',
type: 'module',
watched: false,
served: false
},
{
pattern: 'tests/**/*.spec.js',
type: 'module',
watched: false,
served: false
},
{
pattern: 'src/assets/styles/*.css',
type: 'css',
watched: false,
served: false
}
],
plugins: [
'karma-vite',
'karma-mocha',
'karma-sinon-chai',
'karma-firefox-launcher',
'karma-chrome-launcher',
'karma-spec-reporter',
'karma-coverage'
],
// list of files / patterns to exclude
exclude: [],
// test results reporter to use
// possible values: 'dots', 'progress'
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['spec', 'coverage'],
coverageReporter: {
dir: 'coverage',
reporters: [{ type: 'lcov', subdir: '.' }, { type: 'text-summary' }]
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
// config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
customLaunchers: {
FirefoxHeadlessTouch: {
base: 'FirefoxHeadless',
prefs: {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 2,
client: {
captureConsole: true,
mocha: {
timeout: 7000
}
},
browserConsoleLogOptions: {
terminal: true,
level: ''
}
})
// Fix the timezone
process.env.TZ = 'Europe/Amsterdam'
}

View File

@@ -1,203 +0,0 @@
// Karma configuration
'use strict'
const path = require('path')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
function resolve (dir) {
return path.join(__dirname, dir)
}
module.exports = function (config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: '',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['mocha', 'sinon-chai'],
// list of files / patterns to load in the browser
files: [
'./karma.files.js',
{
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
watched: false,
included: false,
served: true,
nocache: false
}
],
// list of files / patterns to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'./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'],
coverageReporter: {
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',
isSynchronous: true
},
junitReporter: {
outputDir: 'report', // results will be saved as $outputDir/$browserName.xml
// if included, results will be saved as $outputDir/$browserName/$outputFile
outputFile: undefined,
suite: '', // suite will become the package name attribute in xml testsuite element
useBrowserName: true, // add browser name to report and classes names
// function (browser, result) to customize the name attribute in xml testcase element
nameFormatter: undefined,
// function (browser, result) to customize the classname attribute in xml testcase element
classNameFormatter: undefined,
properties: {} // key value pairs add to the <properties> section of the report
},
// web server port
port: 9876,
// enable / disable colors in the output (reporters and logs)
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN ||
// config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// enable / disable watching file and executing tests whenever any file changes
autoWatch: false,
customLaunchers: {
FirefoxHeadlessTouch: {
base: 'FirefoxHeadless',
prefs: {
'dom.w3c_touch_events.enabled': 1,
'dom.events.asyncClipboard.clipboardItem': true
}
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['ChromiumHeadless', 'FirefoxHeadlessTouch'],
// Continuous Integration mode
// if true, Karma captures browsers, runs the tests and exits
singleRun: true,
// Concurrency level
// how many browser should be started simultaneous
concurrency: 2,
client: {
captureConsole: true,
mocha: {
timeout: 7000
}
},
browserConsoleLogOptions: {
terminal: true,
level: ''
},
webpack: {
mode: 'development',
entry: './src/main.js',
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
vue$: 'vue/dist/vue.esm.js',
'@': resolve('src')
}
},
module: {
rules: [
{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: [
{
loader: 'babel-loader'
}
]
},
{
test: /\.js$/,
include: /src/,
exclude: /(node_modules|bower_components|\.spec\.js$)/,
use: [
{
loader: 'istanbul-instrumenter-loader',
options: {
esModules: true
}
}
]
},
{
test: /worker\.js$/,
loader: 'worker-loader'
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader'
},
{
test: /\.vue$/,
loader: 'vue-loader',
options: {
loaders: {
js: 'babel-loader'
},
postLoaders: {
js: 'istanbul-instrumenter-loader?esModules=true'
}
}
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.scss$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: resolve('fonts/[name].[hash:7].[ext]')
}
}
]
},
plugins: [new VueLoaderPlugin()],
node: {
fs: 'empty'
}
},
webpackMiddleware: {
watchOptions: {
ignored: /node_modules/
}
},
proxies: {
'/_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'
}

View File

@@ -1,21 +0,0 @@
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)

View File

@@ -5,7 +5,7 @@ a custom version of [sql.js][1]. It allows sqliteviz to have more recent
version of SQLite build with a number of useful extensions. version of SQLite build with a number of useful extensions.
`Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py` `Makefile` from [sql.js][1] is rewritten as more comprehensible `configure.py`
and `build.py` Python scripts that run in `emscripten/emsdk` Docker container. and `build.py` Python scripts that run in `emscripten/emsdk` Docker container.
## Extension ## Extension
@@ -84,15 +84,15 @@ described in [this message from SQLite Forum][12]:
> amalgamation code and the extensions would thereafter be automatically > amalgamation code and the extensions would thereafter be automatically
> initialized on each connection. > initialized on each connection.
[1]: https://github.com/sql-js/sql.js [1]: https://github.com/sql-js/sql.js
[2]: https://sqlite.org/amalgamation.html [2]: https://sqlite.org/amalgamation.html
[3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc [3]: https://sqlite.org/src/dir?ci=trunk&name=ext/misc
[4]: https://sqlite.org/fts5.html [4]: https://sqlite.org/fts5.html
[5]: https://github.com/jakethaw/pivot_vtab [5]: https://github.com/jakethaw/pivot_vtab
[6]: https://sqlite.org/series.html [6]: https://sqlite.org/series.html
[7]: https://sqlite.org/src/file/ext/misc/series.c [7]: https://sqlite.org/src/file/ext/misc/series.c
[8]: https://sqlite.org/src/file/ext/misc/closure.c [8]: https://sqlite.org/src/file/ext/misc/closure.c
[9]: https://sqlite.org/src/file/ext/misc/uuid.c [9]: https://sqlite.org/src/file/ext/misc/uuid.c
[10]: https://sqlite.org/src/file/ext/misc/regexp.c [10]: https://sqlite.org/src/file/ext/misc/regexp.c
[11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/ [11]: https://charlesleifer.com/blog/querying-tree-structures-in-sqlite-using-python-and-the-transitive-closure-extension/
[12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw [12]: https://sqlite.org/forum/forumpost/6ad7d4f4bebe5e06?raw

View File

@@ -1,12 +1,15 @@
module.exports = function (config) { module.exports = function (config) {
const timeout = 15 * 60 * 1000 const timeout = 15 * 60 * 1000
config.set({ config.set({
frameworks: ['mocha'], frameworks: ['mocha'],
files: [ files: [
'suite.js', 'suite.js',
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm', served: true, included: false }, {
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
served: true,
included: false
},
{ pattern: 'sample.csv', served: true, included: false } { pattern: 'sample.csv', served: true, included: false }
], ],
@@ -15,7 +18,10 @@ module.exports = function (config) {
singleRun: true, singleRun: true,
customLaunchers: { customLaunchers: {
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] } ChromiumHeadlessNoSandbox: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox']
}
}, },
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'], browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
concurrency: 1, concurrency: 1,
@@ -33,11 +39,11 @@ module.exports = function (config) {
logLevel: config.LOG_INFO, logLevel: config.LOG_INFO,
browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO }, browserConsoleLogOptions: { terminal: true, level: config.LOG_INFO },
preprocessors: { 'suite.js': [ 'webpack' ] }, preprocessors: { 'suite.js': ['webpack'] },
webpack: { webpack: {
mode: 'development', mode: 'development',
module: { module: {
noParse: [ __dirname + '/node_modules/benchmark/benchmark.js' ] noParse: [__dirname + '/node_modules/benchmark/benchmark.js']
}, },
node: { fs: 'empty' } node: { fs: 'empty' }
}, },
@@ -47,6 +53,5 @@ module.exports = function (config) {
}, },
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' } jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
}) })
} }

View File

@@ -2,7 +2,7 @@
"name": "sqlite-webassembly-microbenchmark", "name": "sqlite-webassembly-microbenchmark",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@babel/core" : "^7.14.8", "@babel/core": "^7.14.8",
"babel-loader": "^8.2.2", "babel-loader": "^8.2.2",
"benchmark": "^2.1.4", "benchmark": "^2.1.4",
"lodash": "^4.17.4", "lodash": "^4.17.4",
@@ -11,7 +11,7 @@
"karma": "^6.3.4", "karma": "^6.3.4",
"karma-chrome-launcher": "^3.1.0", "karma-chrome-launcher": "^3.1.0",
"karma-firefox-launcher": "^2.1.1", "karma-firefox-launcher": "^2.1.1",
"karma-json-to-file-reporter" : "^1.0.1", "karma-json-to-file-reporter": "^1.0.1",
"karma-mocha": "^2.0.1", "karma-mocha": "^2.0.1",
"karma-webpack": "^4.0.2", "karma-webpack": "^4.0.2",
"webpack": "^4.46.0", "webpack": "^4.46.0",

View File

@@ -4,7 +4,6 @@ import lodash from 'lodash'
import Papa from 'papaparse' import Papa from 'papaparse'
import useragent from 'ua-parser-js' import useragent from 'ua-parser-js'
describe('SQLite build benchmark', function () { describe('SQLite build benchmark', function () {
let parsedCsv let parsedCsv
let sqlModule let sqlModule
@@ -18,7 +17,7 @@ describe('SQLite build benchmark', function () {
importToTable(selectDb, parsedCsv) importToTable(selectDb, parsedCsv)
}) })
function benchmarkImport () { function benchmarkImport() {
const db = new sqlModule.Database() const db = new sqlModule.Database()
try { try {
importToTable(db, parsedCsv) importToTable(db, parsedCsv)
@@ -27,7 +26,7 @@ describe('SQLite build benchmark', function () {
} }
} }
function benchmarkSelect () { function benchmarkSelect() {
const result = selectDb.exec(` const result = selectDb.exec(`
SELECT county, AVG(avg_depth) avg_depth_c SELECT county, AVG(avg_depth) avg_depth_c
FROM ( FROM (
@@ -50,11 +49,9 @@ describe('SQLite build benchmark', function () {
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect }) suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
await run(suite) await run(suite)
}) })
}) })
function importToTable(db, parsedCsv, chunkSize = 1024) {
function importToTable (db, parsedCsv, chunkSize = 1024) {
const columnListString = parsedCsv.meta.fields.join(', ') const columnListString = parsedCsv.meta.fields.join(', ')
db.exec(`CREATE TABLE csv_import(${columnListString})`) db.exec(`CREATE TABLE csv_import(${columnListString})`)
@@ -67,7 +64,6 @@ function importToTable (db, parsedCsv, chunkSize = 1024) {
}) })
} }
class PromiseWrapper { class PromiseWrapper {
constructor() { constructor() {
this.promise = new Promise((resolve, reject) => { this.promise = new Promise((resolve, reject) => {
@@ -89,11 +85,11 @@ function parseCsv(url) {
}) })
} }
function chunkArray (arr, size) { function chunkArray(arr, size) {
return arr.reduce(function (result, value, index) { return arr.reduce(function (result, value, index) {
const chunkIndex = Math.floor(index / size) const chunkIndex = Math.floor(index / size)
if(!(chunkIndex in result)) { if (!(chunkIndex in result)) {
result[chunkIndex] = [] result[chunkIndex] = []
} }
result[chunkIndex].push(value) result[chunkIndex].push(value)
@@ -102,8 +98,7 @@ function chunkArray (arr, size) {
}, []) }, [])
} }
function createSuite() {
function createSuite () {
// Combined workaround from: // Combined workaround from:
// - https://github.com/bestiejs/benchmark.js/issues/106 // - https://github.com/bestiejs/benchmark.js/issues/106
// - https://github.com/bestiejs/benchmark.js/issues/237 // - https://github.com/bestiejs/benchmark.js/issues/237
@@ -117,24 +112,26 @@ function createSuite () {
return new bm.Suite() return new bm.Suite()
} }
function run (suite) { function run(suite) {
const suiteResult = new PromiseWrapper() const suiteResult = new PromiseWrapper()
suite suite
.on('cycle', function (event) { .on('cycle', function (event) {
console.info(String(event.target)) console.info(String(event.target))
}) })
.on('complete', function () { .on('complete', function () {
console.log(JSON.stringify({ console.log(
browser: useragent(navigator.userAgent).browser, JSON.stringify({
result: this.filter('successful') browser: useragent(navigator.userAgent).browser,
})) result: this.filter('successful')
})
)
suiteResult.resolve() suiteResult.resolve()
}) })
.on('error', function (event) { .on('error', function (event) {
console.error('Benchmark failed', String(event.target)) console.error('Benchmark failed', String(event.target))
suiteResult.reject() suiteResult.reject()
}) })
.run({async: true}) .run({ async: true })
return suiteResult.promise return suiteResult.promise
} }

46054
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,21 @@
{ {
"name": "sqliteviz", "name": "sqliteviz",
"version": "0.25.0", "version": "0.26.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"private": true, "private": true,
"type": "module",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "dev": "vite",
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build", "build": "vite build",
"test": "vue-cli-service karma", "serve": "vite preview",
"lint": "vue-cli-service lint" "test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"codemirror": "^5.57.0", "buffer": "^6.0.3",
"codemirror": "^5.65.18",
"codemirror-editor-vue3": "^2.8.0",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"dataurl-to-blob": "^0.0.1", "dataurl-to-blob": "^0.0.1",
"html2canvas": "^1.1.4", "html2canvas": "^1.1.4",
@@ -18,46 +23,57 @@
"nanoid": "^3.1.12", "nanoid": "^3.1.12",
"papaparse": "^5.4.1", "papaparse": "^5.4.1",
"pivottable": "^2.23.0", "pivottable": "^2.23.0",
"plotly.js": "^1.58.4", "plotly.js": "^2.35.2",
"promise-worker": "^2.0.1", "promise-worker": "^2.0.1",
"react": "^16.13.1", "react": "^16.14.0",
"react-chart-editor": "^0.45.0", "react-chart-editor": "^0.46.1",
"react-dom": "^16.13.1", "react-dom": "^16.14.0",
"sql.js": "file:./lib/sql-js", "sql.js": "file:./lib/sql-js",
"vue": "^2.6.11", "tiny-emitter": "^2.1.0",
"vue-codemirror": "^4.0.6", "veaury": "^2.5.1",
"vue-js-modal": "^2.0.0-rc.6", "vue": "^3.5.11",
"vue-multiselect": "^2.1.6", "vue-final-modal": "^4.5.5",
"vue-router": "^3.2.0", "vue-multiselect": "^3.0.0-beta.3",
"vue2-teleport": "^1.0.1", "vue-router": "^4.4.5",
"vuejs-paginate": "^2.1.0", "vuejs-paginate-next": "^1.0.2",
"vuera": "^0.2.7", "vuex": "^4.1.0"
"vuex": "^3.4.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^4.4.0", "@babel/core": "^7.25.7",
"@vue/cli-plugin-eslint": "^4.4.0", "@vitejs/plugin-vue": "^5.2.1",
"@vue/cli-plugin-router": "^4.4.0", "@vue/eslint-config-standard": "^8.0.1",
"@vue/cli-plugin-vuex": "^4.4.0", "@vue/test-utils": "^2.4.6",
"@vue/cli-service": "^4.4.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^1.1.2",
"babel-eslint": "^10.1.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^8.0.1",
"eslint": "^6.7.2", "eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.20.2", "eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^9.28.0",
"flush-promises": "^1.0.2", "flush-promises": "^1.0.2",
"karma": "^3.1.4", "karma": "^6.4.4",
"karma-firefox-launcher": "^2.1.0", "karma-coverage": "^2.2.1",
"karma-webpack": "^4.0.2", "karma-coverage-istanbul-reporter": "^3.0.3",
"vue-cli-plugin-ui-karma": "^0.2.5", "karma-firefox-launcher": "^2.1.3",
"vue-template-compiler": "^2.6.11", "karma-mocha": "^1.3.0",
"workbox-webpack-plugin": "^6.1.5", "karma-spec-reporter": "^0.0.36",
"worker-loader": "^3.0.8" "karma-vite": "^1.0.5",
"mocha": "^5.2.0",
"prettier": "3.5.3",
"process": "^0.11.10",
"url-loader": "^4.1.1",
"vite": "^5.4.14",
"vite-plugin-istanbul": "^5.0.0",
"vite-plugin-node-polyfills": "^0.23.0",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-static-copy": "^2.2.0",
"vue-cli-plugin-ui-karma": "^0.2.5"
},
"overrides": {
"karma-vite": {
"vite-plugin-istanbul": "$vite-plugin-istanbul"
}
} }
} }

View File

@@ -1,58 +1,85 @@
<template> <template>
<div id="app"> <div id="app">
<router-view/> <router-view />
<modals-container />
</div> </div>
</template> </template>
<script>
import storedInquiries from '@/lib/storedInquiries'
import { ModalsContainer } from 'vue-final-modal'
export default {
components: { ModalsContainer },
computed: {
inquiries() {
return this.$store.state.inquiries
}
},
watch: {
inquiries: {
deep: true,
handler() {
storedInquiries.updateStorage(this.inquiries)
}
}
},
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
}
}
</script>
<style> <style>
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-Regular.woff2"); src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400; font-weight: 400;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-SemiBold.woff2"); src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600; font-weight: 600;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-Bold.woff2"); src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-Italic.woff2"); src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400; font-weight: 400;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-SemiBoldItalic.woff2"); src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600; font-weight: 600;
font-style: italic; font-style: italic;
} }
@font-face { @font-face {
font-family: "Open Sans"; font-family: 'Open Sans';
src: url("~@/assets/fonts/OpenSans-BoldItalic.woff2"); src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700; font-weight: 700;
font-style: italic; font-style: italic;
} }
#app, #app,
.dialog,
input, input,
label, label,
button, button,
.plotly_editor * { .plotly_editor * {
font-family: "Open Sans", Helvetica, Arial, sans-serif; font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }

View File

@@ -59,5 +59,3 @@ button.secondary:disabled {
text-shadow: none; text-shadow: none;
cursor: default; cursor: default;
} }

View File

@@ -1,6 +1,13 @@
.dialog { .dialog {
display: flex;
justify-content: center;
align-items: center;
}
.dialog .vfm__content {
border-radius: var(--border-radius-big); border-radius: var(--border-radius-big);
box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8); box-shadow: 0px 2px 9px rgba(80, 103, 132, 0.8);
background-color: white;
overflow: hidden;
} }
.dialog-header { .dialog-header {
@@ -16,7 +23,7 @@
} }
.dialog-body { .dialog-body {
min-height: 60px; min-height: 56px;
background-color: var(--color-bg-light); background-color: var(--color-bg-light);
padding: 24px; padding: 24px;
border-top: 1px solid var(--color-border-light); border-top: 1px solid var(--color-border-light);
@@ -35,6 +42,6 @@
margin-left: 16px; margin-left: 16px;
} }
.vm--overlay { .vfm__overlay.vfm--overlay {
background-color: rgba(162, 177, 198, 0.5); background-color: rgba(162, 177, 198, 0.5);
} }

View File

@@ -62,14 +62,14 @@
margin: 2px; margin: 2px;
} }
.sqliteviz-select .multiselect__tag-icon:after { .sqliteviz-select .multiselect__tag-icon:after {
content: url('~@/assets/images/delete-tag.svg'); content: url('@/assets/images/delete-tag.svg');
height: 14px; height: 14px;
width: 14px; width: 14px;
} }
.sqliteviz-select .multiselect__tag-icon:focus:after, .sqliteviz-select .multiselect__tag-icon:focus:after,
.sqliteviz-select .multiselect__tag-icon:hover:after { .sqliteviz-select .multiselect__tag-icon:hover:after {
content: url('~@/assets/images/delete-tag-hover.svg'); content: url('@/assets/images/delete-tag-hover.svg');
} }
.sqliteviz-select .multiselect__tag-icon:focus, .sqliteviz-select .multiselect__tag-icon:focus,
@@ -102,7 +102,7 @@
} }
.sqliteviz-select .multiselect__select:before { .sqliteviz-select .multiselect__select:before {
content: url('~@/assets/images/arrow.svg'); content: url('@/assets/images/arrow.svg');
border: none; border: none;
top: 0; top: 0;
} }
@@ -116,14 +116,14 @@
} }
.sqliteviz-select .multiselect__select:hover:before { .sqliteviz-select .multiselect__select:hover:before {
content: url('~@/assets/images/arrow-hover.svg'); content: url('@/assets/images/arrow-hover.svg');
} }
.sqliteviz-select.multiselect--active .multiselect__tags { .sqliteviz-select.multiselect--active .multiselect__tags {
border-radius: var(--border-radius-medium-2); border-radius: var(--border-radius-medium-2);
} }
.sqliteviz-select .multiselect__option .no-results { .sqliteviz-select .multiselect__option .no-results {
color: var(--color-text-light-2); color: var(--color-text-light-2);
} }

View File

@@ -37,7 +37,7 @@
height: calc(100% - 27px); height: calc(100% - 27px);
} }
@supports (-moz-appearance:none) { @supports (-moz-appearance: none) {
.header-container { .header-container {
top: 0; top: 0;
padding-left: 6px; padding-left: 6px;
@@ -59,7 +59,8 @@ table.sqliteviz-table {
margin-top: -35px; margin-top: -35px;
border-collapse: collapse; border-collapse: collapse;
} }
.sqliteviz-table thead th, .fixed-header { .sqliteviz-table thead th,
.fixed-header {
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
box-sizing: border-box; box-sizing: border-box;
@@ -71,7 +72,7 @@ table.sqliteviz-table {
} }
.sqliteviz-table tbody td { .sqliteviz-table tbody td {
font-size: 13px; font-size: 13px;
background-color:white; background-color: white;
color: var(--color-text-base); color: var(--color-text-base);
box-sizing: border-box; box-sizing: border-box;
border-bottom: 1px solid var(--color-border-light); border-bottom: 1px solid var(--color-border-light);
@@ -108,8 +109,8 @@ table.sqliteviz-table {
color: var(--color-text-base); color: var(--color-text-base);
} }
.sqliteviz-table tbody td[data-isNull="true"], .sqliteviz-table tbody td[data-isNull='true'],
.sqliteviz-table tbody td[data-isBlob="true"] { .sqliteviz-table tbody td[data-isBlob='true'] {
color: var(--color-text-light-2); color: var(--color-text-light-2);
font-style: italic; font-style: italic;
} }

View File

@@ -4,7 +4,7 @@
text-align: center; text-align: center;
font-size: 12px; font-size: 12px;
padding: 0 6px; padding: 0 6px;
line-height: 19px;; line-height: 19px;
position: fixed; position: fixed;
height: 19px; height: 19px;
border-radius: var(--border-radius-medium); border-radius: var(--border-radius-medium);

View File

@@ -1,21 +1,19 @@
:root { :root {
--color-white: #ffffff; --color-white: #ffffff;
--color-gray-light: #F3F6FA; --color-gray-light: #f3f6fa;
--color-gray-light-2: #DFE8F3; --color-gray-light-2: #dfe8f3;
--color-gray-light-3: #C8D4E3; --color-gray-light-3: #c8d4e3;
--color-gray-light-4:#EBF0F8; --color-gray-light-4: #ebf0f8;
--color-gray-light-5:#f8f8f9; --color-gray-light-5: #f8f8f9;
--color-gray-medium: #A2B1C6; --color-gray-medium: #a2b1c6;
--color-gray-dark: #506784; --color-gray-dark: #506784;
--color-blue-medium: #119DFF; --color-blue-medium: #119dff;
--color-blue-dark: #0D76BF; --color-blue-dark: #0d76bf;
--color-blue-dark-2: #2A3F5F; --color-blue-dark-2: #2a3f5f;
--color-red: #EF553B; --color-red: #ef553b;
--color-red-2: #DE350B; --color-red-2: #de350b;
--color-red-light: #FFBDAD; --color-red-light: #ffbdad;
--color-yellow: #FBEFCB; --color-yellow: #fbefcb;
--color-bg-light: var(--color-gray-light); --color-bg-light: var(--color-gray-light);
--color-bg-light-2: var(--color-gray-light-2); --color-bg-light-2: var(--color-gray-light-2);
@@ -48,6 +46,3 @@
.plotly-editor--theme-provider { .plotly-editor--theme-provider {
--sidebar-width: 112px; --sidebar-width: 112px;
} }

View File

@@ -1,18 +1,27 @@
<template> <template>
<div <div
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]" :class="[
'checkbox-container',
{ checked: checked },
{ disabled: disabled }
]"
@click.stop="onClick" @click.stop="onClick"
> >
<div v-show="!checked" class="unchecked" /> <div v-show="!checked" class="unchecked" />
<img <img
v-show="checked && !disabled" v-show="checked && !disabled && theme === 'light'"
:src="theme === 'light' class="checked-light"
? require('@/assets/images/checkbox_checked_light.svg') src="~@/assets/images/checkbox_checked_light.svg"
: require('@/assets/images/checkbox_checked.svg')" />
<img
v-show="checked && !disabled && theme !== 'light'"
class="checked"
src="~@/assets/images/checkbox_checked.svg"
/> />
<img <img
v-show="checked && disabled" v-show="checked && disabled"
:src="require('@/assets/images/checkbox_checked_disabled.svg')" class="checked-disabled"
src="~@/assets/images/checkbox_checked_disabled.svg"
/> />
<span v-if="label" class="label">{{ label }}</span> <span v-if="label" class="label">{{ label }}</span>
</div> </div>
@@ -26,7 +35,7 @@ export default {
type: String, type: String,
required: false, required: false,
default: 'accent', default: 'accent',
validator: (value) => { validator: value => {
return ['accent', 'light'].includes(value) return ['accent', 'light'].includes(value)
} }
}, },
@@ -46,13 +55,14 @@ export default {
default: false default: false
} }
}, },
data () { emits: ['click'],
data() {
return { return {
checked: this.init checked: this.init
} }
}, },
methods: { methods: {
onClick () { onClick() {
if (!this.disabled) { if (!this.disabled) {
this.checked = !this.checked this.checked = !this.checked
this.$emit('click', this.checked) this.$emit('click', this.checked)
@@ -80,7 +90,7 @@ export default {
} }
img { img {
display: block; display: block;
} }
.label { .label {
margin-left: 6px; margin-left: 6px;
@@ -100,6 +110,6 @@ img {
.disabled .unchecked, .disabled .unchecked,
.disabled .unchecked:hover { .disabled .unchecked:hover {
background-color: var(--color-bg-light-2); background-color: var(--color-bg-light-2);
} }
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<div :class="{ 'disabled': disabled }"> <div :class="{ disabled: disabled }">
<div class="text-field-label">Delimiter</div> <div class="text-field-label">Delimiter</div>
<div <div
class="delimiter-selector-container" class="delimiter-selector-container"
@@ -8,21 +8,21 @@
> >
<div class="value"> <div class="value">
<input <input
:class="{ 'filled': filled }"
ref="delimiterInput" ref="delimiterInput"
v-model="inputValue"
:class="{ filled: filled }"
type="text" type="text"
maxlength="1" maxlength="1"
v-model="inputValue"
@click.stop
:disabled="disabled" :disabled="disabled"
@click.stop
/> />
<div class="name">{{ getSymbolName(value) }}</div> <div class="name">{{ getSymbolName(modelValue) }}</div>
</div> </div>
<div class="controls" @click.stop> <div class="controls" @click.stop>
<clear-icon @click.native="clear" :disabled="disabled"/> <clear-icon :disabled="disabled" @click="clear" />
<drop-down-chevron <drop-down-chevron
:disabled="disabled" :disabled="disabled"
@click.native="!disabled && (showOptions = !showOptions)" @click="!disabled && (showOptions = !showOptions)"
/> />
</div> </div>
</div> </div>
@@ -30,10 +30,11 @@
<div <div
v-for="(option, index) in options" v-for="(option, index) in options"
:key="index" :key="index"
@click="chooseOption(option)"
class="option" class="option"
@click="chooseOption(option)"
> >
<pre>{{option}}</pre><div>{{ getSymbolName(option) }}</div> <pre>{{ option }}</pre>
<div>{{ getSymbolName(option) }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -46,9 +47,14 @@ import ClearIcon from '@/components/svg/clear'
export default { export default {
name: 'DelimiterSelector', name: 'DelimiterSelector',
props: ['value', 'width', 'disabled'],
components: { DropDownChevron, ClearIcon }, components: { DropDownChevron, ClearIcon },
data () { props: {
modelValue: String,
width: String,
disabled: Boolean
},
emits: ['update:modelValue'],
data() {
return { return {
showOptions: false, showOptions: false,
options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'], options: [',', '\t', ' ', '|', ';', '\u001F', '\u001E'],
@@ -57,36 +63,36 @@ export default {
} }
}, },
watch: { watch: {
inputValue () { inputValue() {
if (this.inputValue) { if (this.inputValue) {
this.filled = true this.filled = true
if (this.inputValue !== this.value) { if (this.inputValue !== this.modelValue) {
this.$emit('input', this.inputValue) this.$emit('update:modelValue', this.inputValue)
} }
} else { } else {
this.filled = false this.filled = false
} }
} }
}, },
created () { created() {
this.inputValue = this.value this.inputValue = this.modelValue
}, },
methods: { methods: {
getSymbolName (str) { getSymbolName(str) {
if (!str) { if (!str) {
return '' return ''
} }
return ascii[str.charCodeAt(0).toString()].name return ascii[str.charCodeAt(0).toString()].name
}, },
chooseOption (option) { chooseOption(option) {
this.inputValue = option this.inputValue = option
this.showOptions = false this.showOptions = false
}, },
onContainerClick (event) { onContainerClick() {
this.$refs.delimiterInput.focus() this.$refs.delimiterInput.focus()
}, },
clear () { clear() {
if (!this.disabled) { if (!this.disabled) {
this.inputValue = '' this.inputValue = ''
this.$refs.delimiterInput.focus() this.$refs.delimiterInput.focus()

View File

@@ -1,24 +1,23 @@
<template> <template>
<modal <modal
:name="dialogName" :modalId="dialogName"
classes="dialog" class="dialog"
height="auto" contentClass="import-modal"
width="80%"
scrollable scrollable
:clickToClose="false" :clickToClose="false"
> >
<div class="dialog-header"> <div class="dialog-header">
{{ typeName }} import {{ typeName }} import
<close-icon @click="cancelImport" :disabled="disableDialog"/> <close-icon :disabled="disableDialog" @click="cancelImport" />
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<text-field <text-field
label="Table name" id="csv-json-table-name"
v-model="tableName" v-model="tableName"
label="Table name"
width="484px" width="484px"
:disabled="disableDialog" :disabled="disableDialog"
:error-msg="tableNameError" :errorMsg="tableNameError"
id="csv-json-table-name"
/> />
<div v-if="!isJson && !isNdJson" class="chars"> <div v-if="!isJson && !isNdJson" class="chars">
<delimiter-selector <delimiter-selector
@@ -29,27 +28,27 @@
@input="preview" @input="preview"
/> />
<text-field <text-field
id="quote-char"
v-model="quoteChar"
label="Quote char" label="Quote char"
hint="The character used to quote fields." hint="The character used to quote fields."
v-model="quoteChar"
width="93px" width="93px"
:disabled="disableDialog" :disabled="disableDialog"
class="char-input" class="char-input"
id="quote-char"
@input="preview" @input="preview"
/> />
<text-field <text-field
id="escape-char"
v-model="escapeChar"
label="Escape char" label="Escape char"
hint=' hint='
The character used to escape the quote character within a field The character used to escape the quote character within a field
(e.g. "column with ""quotes"" in text"). (e.g. "column with ""quotes"" in text").
' '
max-hint-width="242px" maxHintWidth="242px"
v-model="escapeChar"
width="93px" width="93px"
:disabled="disableDialog" :disabled="disableDialog"
class="char-input" class="char-input"
id="escape-char"
@input="preview" @input="preview"
/> />
</div> </div>
@@ -67,35 +66,32 @@
class="preview-table" class="preview-table"
/> />
<div v-else class="no-data">No data</div> <div v-else class="no-data">No data</div>
<logs <logs class="import-errors" :messages="importMessages" />
class="import-errors"
:messages="importMessages"
/>
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button <button
id="import-cancel"
class="secondary" class="secondary"
:disabled="disableDialog" :disabled="disableDialog"
@click="cancelImport" @click="cancelImport"
id="import-cancel"
> >
Cancel Cancel
</button> </button>
<button <button
v-show="!importCompleted" v-show="!importCompleted"
id="import-start"
class="primary" class="primary"
:disabled="disableDialog || disableImport" :disabled="disableDialog || disableImport"
@click="loadToDb(file)" @click="loadToDb(file)"
id="import-start"
> >
Import Import
</button> </button>
<button <button
v-show="importCompleted" v-show="importCompleted"
id="import-finish"
class="primary" class="primary"
:disabled="disableDialog" :disabled="disableDialog"
@click="finish" @click="finish"
id="import-finish"
> >
Finish Finish
</button> </button>
@@ -130,7 +126,8 @@ export default {
db: Object, db: Object,
dialogName: String dialogName: String
}, },
data () { emits: ['cancel', 'finish'],
data() {
return { return {
disableDialog: false, disableDialog: false,
disableImport: false, disableImport: false,
@@ -147,24 +144,24 @@ export default {
} }
}, },
computed: { computed: {
isJson () { isJson() {
return fIo.isJSON(this.file) return fIo.isJSON(this.file)
}, },
isNdJson () { isNdJson() {
return fIo.isNDJSON(this.file) return fIo.isNDJSON(this.file)
}, },
typeName () { typeName() {
return this.isJson || this.isNdJson ? 'JSON' : 'CSV' return this.isJson || this.isNdJson ? 'JSON' : 'CSV'
} }
}, },
watch: { watch: {
isJson () { isJson() {
if (this.isJson) { if (this.isJson) {
this.delimiter = '\u001E' this.delimiter = '\u001E'
this.header = false this.header = false
} }
}, },
isNdJson () { isNdJson() {
if (this.isNdJson) { if (this.isNdJson) {
this.delimiter = '\u001E' this.delimiter = '\u001E'
this.header = false this.header = false
@@ -175,18 +172,17 @@ export default {
if (!this.tableName) { if (!this.tableName) {
return return
} }
this.db.validateTableName(this.tableName) this.db.validateTableName(this.tableName).catch(err => {
.catch(err => { this.tableNameError = err.message + '. Try another table name.'
this.tableNameError = err.message + '. Try another table name.' })
})
}, 400) }, 400)
}, },
methods: { methods: {
changeHeaderDisplaying (e) { changeHeaderDisplaying(e) {
this.header = e this.header = e
this.preview() this.preview()
}, },
cancelImport () { cancelImport() {
if (!this.disableDialog) { if (!this.disableDialog) {
if (this.addedTable) { if (this.addedTable) {
this.db.execute(`DROP TABLE "${this.addedTable}"`) this.db.execute(`DROP TABLE "${this.addedTable}"`)
@@ -196,7 +192,7 @@ export default {
this.$emit('cancel') this.$emit('cancel')
} }
}, },
reset () { reset() {
this.header = !this.isJson && !this.isNdJson this.header = !this.isJson && !this.isNdJson
this.quoteChar = '"' this.quoteChar = '"'
this.escapeChar = '"' this.escapeChar = '"'
@@ -210,11 +206,11 @@ export default {
this.addedTable = null this.addedTable = null
this.tableNameError = '' this.tableNameError = ''
}, },
open () { open() {
this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file)) this.tableName = this.db.sanitizeTableName(fIo.getFileName(this.file))
this.$modal.show(this.dialogName) this.$modal.show(this.dialogName)
}, },
async preview () { async preview() {
this.disableImport = false this.disableImport = false
if (!this.file) { if (!this.file) {
return return
@@ -257,13 +253,15 @@ export default {
} }
} catch (err) { } catch (err) {
console.error(err) console.error(err)
this.importMessages = [{ this.importMessages = [
message: err, {
type: 'error' message: err,
}] type: 'error'
}
]
} }
}, },
async getJsonParseResult (file) { async getJsonParseResult(file) {
const jsonContent = await fIo.getFileContent(file) const jsonContent = await fIo.getFileContent(file)
const isEmpty = !jsonContent.trim() const isEmpty = !jsonContent.trim()
return { return {
@@ -273,10 +271,10 @@ export default {
}, },
hasErrors: false, hasErrors: false,
messages: [], messages: [],
rowCount: +(!isEmpty) rowCount: +!isEmpty
} }
}, },
async loadToDb (file) { async loadToDb(file) {
if (!this.tableName) { if (!this.tableName) {
this.tableNameError = "Table name can't be empty" this.tableNameError = "Table name can't be empty"
return return
@@ -290,21 +288,22 @@ export default {
delimiter: this.delimiter, delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc'] columns: !this.isJson && !this.isNdJson ? null : ['doc']
} }
const parsingMsg = { let parsingMsg = {}
this.importMessages.push({
message: `Parsing ${this.typeName}...`, message: `Parsing ${this.typeName}...`,
type: 'info' type: 'info'
} })
this.importMessages.push(parsingMsg) // Get *reactive* link to parsing message for later updates
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000) parsingMsg = this.importMessages[this.importMessages.length - 1]
const parsingLoadingIndicator = setTimeout(() => {
parsingMsg.type = 'loading'
}, 1000)
const importMsg = { let importMsg = {}
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
}
let importLoadingIndicator = null let importLoadingIndicator = null
const updateProgress = progress => { const updateProgress = progress => {
this.$set(importMsg, 'progress', progress) importMsg.progress = progress
} }
const progressCounterId = this.db.createProgressCounter(updateProgress) const progressCounterId = this.db.createProgressCounter(updateProgress)
@@ -322,7 +321,9 @@ export default {
parsingMsg.type = 'success' parsingMsg.type = 'success'
if (parseResult.messages.length > 0) { if (parseResult.messages.length > 0) {
this.importMessages = this.importMessages.concat(parseResult.messages) this.importMessages = this.importMessages.concat(
parseResult.messages
)
parsingMsg.message = `${rowCount} rows are parsed in ${period}.` parsingMsg.message = `${rowCount} rows are parsed in ${period}.`
} else { } else {
// Inform about parsing success // Inform about parsing success
@@ -333,7 +334,11 @@ export default {
clearTimeout(parsingLoadingIndicator) clearTimeout(parsingLoadingIndicator)
// Add info about import start // Add info about import start
this.importMessages.push(importMsg) this.importMessages.push({
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
})
importMsg = this.importMessages[this.importMessages.length - 1]
// Show import progress after 1 second // Show import progress after 1 second
importLoadingIndicator = setTimeout(() => { importLoadingIndicator = setTimeout(() => {
@@ -342,14 +347,19 @@ export default {
// Add table // Add table
start = new Date() start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId) await this.db.addTableFromCsv(
this.tableName,
parseResult.data,
progressCounterId
)
end = new Date() end = new Date()
this.addedTable = this.tableName this.addedTable = this.tableName
// Inform about import success // Inform about import success
period = time.getPeriod(start, end) period = time.getPeriod(start, end)
importMsg.message = `Importing ${this.typeName} ` + importMsg.message =
`into a SQLite database is completed in ${period}.` `Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.type = 'success' importMsg.type = 'success'
// Loading indicator for import is not needed anymore // Loading indicator for import is not needed anymore
@@ -382,7 +392,7 @@ export default {
this.db.deleteProgressCounter(progressCounterId) this.db.deleteProgressCounter(progressCounterId)
this.disableDialog = false this.disableDialog = false
}, },
async finish () { async finish() {
this.$modal.hide(this.dialogName) this.$modal.hide(this.dialogName)
const stmt = this.getQueryExample() const stmt = this.getQueryExample()
const tabId = await this.$store.dispatch('addTab', { query: stmt }) const tabId = await this.$store.dispatch('addTab', { query: stmt })
@@ -391,18 +401,20 @@ export default {
this.$emit('finish') this.$emit('finish')
events.send('inquiry.create', null, { auto: true }) events.send('inquiry.create', null, { auto: true })
}, },
getQueryExample () { getQueryExample() {
return this.isNdJson ? this.getNdJsonQueryExample() return this.isNdJson
: this.isJson ? this.getJsonQueryExample() ? this.getNdJsonQueryExample()
: this.isJson
? this.getJsonQueryExample()
: [ : [
'/*', '/*',
` * Your CSV file has been imported into ${this.addedTable} table.`, ` * Your CSV file has been imported into ${this.addedTable} table.`,
' * You can run this SQL query to make all CSV records available for charting.', ' * You can run this SQL query to make all CSV records available for charting.',
' */', ' */',
`SELECT * FROM "${this.addedTable}"` `SELECT * FROM "${this.addedTable}"`
].join('\n') ].join('\n')
}, },
getNdJsonQueryExample () { getNdJsonQueryExample() {
try { try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0]) const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0] const firstKey = Object.keys(firstRowJson)[0]
@@ -410,7 +422,7 @@ export default {
'/*', '/*',
` * Your NDJSON file has been imported into ${this.addedTable} table.`, ` * Your NDJSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` + ` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.', 'and make them available for charting.',
' */', ' */',
`SELECT doc->>'${firstKey}'`, `SELECT doc->>'${firstKey}'`,
`FROM "${this.addedTable}"` `FROM "${this.addedTable}"`
@@ -426,7 +438,7 @@ export default {
].join('\n') ].join('\n')
} }
}, },
getJsonQueryExample () { getJsonQueryExample() {
try { try {
const firstRowJson = JSON.parse(this.previewData.values.doc[0]) const firstRowJson = JSON.parse(this.previewData.values.doc[0])
const firstKey = Object.keys(firstRowJson)[0] const firstKey = Object.keys(firstRowJson)[0]
@@ -434,7 +446,7 @@ export default {
'/*', '/*',
` * Your JSON file has been imported into ${this.addedTable} table.`, ` * Your JSON file has been imported into ${this.addedTable} table.`,
` * Run this SQL query to get values of property ${firstKey} ` + ` * Run this SQL query to get values of property ${firstKey} ` +
'and make them available for charting.', 'and make them available for charting.',
' */', ' */',
'SELECT *', 'SELECT *',
`FROM "${this.addedTable}"`, `FROM "${this.addedTable}"`,
@@ -455,13 +467,22 @@ export default {
} }
</script> </script>
<style>
.import-modal {
width: 80%;
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>
<style scoped> <style scoped>
.dialog-body { .dialog-body {
padding-bottom: 0; padding-bottom: 0;
} }
#csv-json-table-name { #csv-json-table-name {
margin-bottom: 24px; margin-bottom: 24px;
} }
.chars { .chars {
@@ -494,11 +515,4 @@ margin-bottom: 24px;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
/* https://github.com/euvl/vue-js-modal/issues/623 */
>>> .vm--modal {
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style> </style>

View File

@@ -1,43 +1,43 @@
<template> <template>
<div class="db-uploader-container" :style="{ width }"> <div class="db-uploader-container" :style="{ width }">
<change-db-icon v-if="type === 'small'" @click="browse"/> <change-db-icon v-if="type === 'small'" @click="browse" />
<div v-if="type === 'illustrated'" class="drop-area-container"> <div v-if="type === 'illustrated'" class="drop-area-container">
<div <div
class="drop-area" class="drop-area"
@dragover.prevent="state = 'dragover'" @dragover.prevent="state = 'dragover'"
@dragleave.prevent="state=''" @dragleave.prevent="state = ''"
@drop.prevent="drop" @drop.prevent="drop"
@click="browse" @click="browse"
> >
<div class="text"> <div class="text">
Drop the database, CSV, JSON or NDJSON file here Drop the database, CSV, JSON or NDJSON file here or click to choose a
or click to choose a file from your computer. file from your computer.
</div> </div>
</div> </div>
</div> </div>
<div v-if="type === 'illustrated'" id="img-container"> <div v-if="type === 'illustrated'" id="img-container">
<img id="drop-file-top-img" :src="require('@/assets/images/top.svg')" /> <img id="drop-file-top-img" src="~@/assets/images/top.svg" />
<img <img
id="left-arm-img" id="left-arm-img"
:class="{'swing': state === 'dragover'}" :class="{ swing: state === 'dragover' }"
:src="require('@/assets/images/leftArm.svg')" src="~@/assets/images/leftArm.svg"
/> />
<img <img
id="file-img" id="file-img"
ref="fileImg" ref="fileImg"
:class="{ :class="{
'swing': state === 'dragover', swing: state === 'dragover',
'fly': state === 'dropping', fly: state === 'dropping',
'hidden': state === 'dropped' hidden: state === 'dropped'
}" }"
:src="require('@/assets/images/file.png')" src="~@/assets/images/file.png"
/> />
<img id="drop-file-bottom-img" :src="require('@/assets/images/bottom.svg')" /> <img id="drop-file-bottom-img" src="~@/assets/images/bottom.svg" />
<img id="body-img" :src="require('@/assets/images/body.svg')" /> <img id="body-img" src="~@/assets/images/body.svg" />
<img <img
id="right-arm-img" id="right-arm-img"
:class="{'swing': state === 'dragover'}" :class="{ swing: state === 'dragover' }"
:src="require('@/assets/images/rightArm.svg')" src="~@/assets/images/rightArm.svg"
/> />
</div> </div>
<div id="error" class="error"></div> <div id="error" class="error"></div>
@@ -47,7 +47,7 @@
ref="addCsvJson" ref="addCsvJson"
:file="file" :file="file"
:db="newDb" :db="newDb"
dialog-name="importFromCsvJson" dialogName="importFromCsvJson"
@cancel="cancelImport" @cancel="cancelImport"
@finish="finish" @finish="finish"
/> />
@@ -63,12 +63,16 @@ import events from '@/lib/utils/events'
export default { export default {
name: 'DbUploader', name: 'DbUploader',
components: {
ChangeDbIcon,
CsvJsonImport
},
props: { props: {
type: { type: {
type: String, type: String,
required: false, required: false,
default: 'small', default: 'small',
validator: (value) => { validator: value => {
return ['illustrated', 'small'].includes(value) return ['illustrated', 'small'].includes(value)
} }
}, },
@@ -78,11 +82,8 @@ export default {
default: 'unset' default: 'unset'
} }
}, },
components: { emits: [],
ChangeDbIcon, data() {
CsvJsonImport
},
data () {
return { return {
state: '', state: '',
animationPromise: Promise.resolve(), animationPromise: Promise.resolve(),
@@ -90,9 +91,9 @@ export default {
newDb: null newDb: null
} }
}, },
mounted () { mounted() {
if (this.type === 'illustrated') { if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => { this.animationPromise = new Promise(resolve => {
this.$refs.fileImg.addEventListener('animationend', event => { this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) { if (event.animationName.startsWith('fly')) {
this.state = 'dropped' this.state = 'dropped'
@@ -103,26 +104,27 @@ export default {
} }
}, },
methods: { methods: {
cancelImport () { cancelImport() {
if (this.newDb) { if (this.newDb) {
this.newDb.shutDown() this.newDb.shutDown()
this.newDb = null this.newDb = null
} }
}, },
async finish () { async finish() {
this.$store.commit('setDb', this.newDb) this.$store.commit('setDb', this.newDb)
if (this.$route.path !== '/workspace') { if (this.$route.path !== '/workspace') {
this.$router.push('/workspace') this.$router.push('/workspace')
} }
}, },
loadDb (file) { loadDb(file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise]) return Promise.all([this.newDb.loadDb(file), this.animationPromise]).then(
.then(this.finish) this.finish
)
}, },
async checkFile (file) { async checkFile(file) {
this.state = 'dropping' this.state = 'dropping'
this.newDb = database.getNewDatabase() this.newDb = database.getNewDatabase()
@@ -139,16 +141,19 @@ export default {
await this.$nextTick() await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset() csvJsonImportModal.reset()
return Promise.all([csvJsonImportModal.preview(), this.animationPromise]) return Promise.all([
.then(csvJsonImportModal.open) csvJsonImportModal.preview(),
this.animationPromise
]).then(csvJsonImportModal.open)
} }
}, },
browse () { browse() {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson') fIo
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
.then(this.checkFile) .then(this.checkFile)
}, },
drop (event) { drop(event) {
this.checkFile(event.dataTransfer.files[0]) this.checkFile(event.dataTransfer.files[0])
} }
} }
@@ -242,11 +247,15 @@ export default {
transform-origin: 0 56px; transform-origin: 0 56px;
} }
#file-img.swing { #file-img.swing {
transform-origin: -74px 139px; transform-origin: -74px 139px;
} }
@keyframes swing { @keyframes swing {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(-7deg); } transform: rotate(0deg);
}
100% {
transform: rotate(-7deg);
}
} }
#file-img.fly { #file-img.fly {

View File

@@ -10,7 +10,12 @@
<div v-show="loading" class="icon-in-progress"> <div v-show="loading" class="icon-in-progress">
<loading-indicator /> <loading-indicator />
</div> </div>
<span v-if="tooltip" class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span
v-if="tooltip"
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
>
{{ tooltip }} {{ tooltip }}
</span> </span>
</button> </button>
@@ -22,11 +27,18 @@ import LoadingIndicator from '@/components/LoadingIndicator'
export default { export default {
name: 'SideBarButton', name: 'SideBarButton',
props: ['active', 'disabled', 'tooltip', 'tooltipPosition', 'loading'],
components: { LoadingIndicator }, components: { LoadingIndicator },
mixins: [tooltipMixin], mixins: [tooltipMixin],
props: {
active: Boolean,
disabled: Boolean,
tooltip: String,
tooltipPosition: String,
loading: Boolean
},
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }
@@ -51,15 +63,15 @@ export default {
border-radius: var(--border-radius-medium-2); border-radius: var(--border-radius-medium-2);
} }
.icon-btn:hover .icon >>> path, .icon-btn:hover .icon :deep(path),
.icon-btn.active .icon >>> path, .icon-btn.active .icon :deep(path),
.icon-btn:hover .icon >>> circle, .icon-btn:hover .icon :deep(circle),
.icon-btn.active .icon >>> circle { .icon-btn.active .icon :deep(circle) {
fill: var(--color-accent); fill: var(--color-accent);
} }
.icon-btn:disabled .icon >>> path, .icon-btn:disabled .icon :deep(path),
.icon-btn:disabled .icon >>> circle { .icon-btn:disabled .icon :deep(circle) {
fill: var(--color-border); fill: var(--color-border);
} }
@@ -68,7 +80,7 @@ export default {
pointer-events: none; pointer-events: none;
} }
.disabled.icon-btn:hover .icon >>> path { .disabled.icon-btn:hover .icon :deep(path) {
fill: var(--color-border); fill: var(--color-border);
} }

View File

@@ -1,27 +1,32 @@
<template> <template>
<modal <modal
:name="name" :modalId="name"
classes="dialog" class="dialog"
height="auto"
:clickToClose="false" :clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
> >
<div class="dialog-header"> <div class="dialog-header">
{{ title }} {{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/> <close-icon :disabled="loading" @click="$emit('cancel')" />
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<div v-if="loading" class="loading-dialog-body"> <div v-if="loading" class="loading-dialog-body">
<loading-indicator :size="30" class="state-icon"/> <loading-indicator :size="30" class="state-icon" />
{{ loadingMsg }} {{ loadingMsg }}
</div> </div>
<div v-else class="loading-dialog-body"> <div v-else class="loading-dialog-body">
<img :src="require('@/assets/images/success.svg')" class="success-icon state-icon" /> <img
src="~@/assets/images/success.svg"
class="success-icon state-icon"
/>
{{ successMsg }} {{ successMsg }}
</div> </div>
</div> </div>
<div class="dialog-buttons-container"> <div class="dialog-buttons-container">
<button <button
class="secondary" class="secondary"
type="button"
:disabled="loading" :disabled="loading"
@click="$emit('cancel')" @click="$emit('cancel')"
> >
@@ -29,6 +34,7 @@
</button> </button>
<button <button
class="primary" class="primary"
type="button"
:disabled="loading" :disabled="loading"
@click="$emit('action')" @click="$emit('action')"
> >
@@ -43,7 +49,8 @@ import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
export default { export default {
name: 'loadingDialog', name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: { props: {
loadingMsg: String, loadingMsg: String,
successMsg: String, successMsg: String,
@@ -52,22 +59,47 @@ export default {
title: String, title: String,
loading: Boolean loading: Boolean
}, },
emits: ['cancel', 'action'],
watch: { watch: {
loading () { loading() {
if (this.loading) { if (this.loading) {
this.$modal.show(this.name) this.$modal.show(this.name)
} }
} }
}, },
components: { LoadingIndicator, CloseIcon },
methods: { methods: {
cancel () { cancel() {
this.$emit('cancel') this.$emit('cancel')
} }
} }
} }
</script> </script>
<style>
.loading-dialog-enter-active {
animation: show-modal 1s linear 0s 1;
}
.loading-dialog-leave-active {
opacity: 0;
}
@keyframes show-modal {
0% {
opacity: 0;
}
99% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.loading-modal {
width: 400px;
}
</style>
<style scoped> <style scoped>
.loading-dialog-body { .loading-dialog-body {
display: flex; display: flex;

View File

@@ -1,5 +1,10 @@
<template> <template>
<svg :class="animationClass" :height="size" :width="size" :viewBox="`0 0 ${size} ${size}`"> <svg
:class="animationClass"
:height="size"
:width="size"
:viewBox="`0 0 ${size} ${size}`"
>
<circle <circle
class="loader-svg bg" class="loader-svg bg"
:style="{ strokeWidth }" :style="{ strokeWidth }"
@@ -9,7 +14,11 @@
/> />
<circle <circle
class="loader-svg front" class="loader-svg front"
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }" :style="{
strokeDasharray: circleProgress,
strokeDashoffset: offset,
strokeWidth
}"
:cx="size / 2" :cx="size / 2"
:cy="size / 2" :cy="size / 2"
:r="radius" :r="radius"
@@ -31,23 +40,26 @@ export default {
default: 20 default: 20
} }
}, },
emits: [],
computed: { computed: {
circleProgress () { circleProgress() {
const circle = this.radius * 3.14 * 2 const circle = this.radius * 3.14 * 2
const dash = this.progress ? (circle * this.progress) / 100 : circle * 1 / 3 const dash = this.progress
? (circle * this.progress) / 100
: (circle * 1) / 3
const space = circle - dash const space = circle - dash
return `${dash}px, ${space}px` return `${dash}px, ${space}px`
}, },
animationClass () { animationClass() {
return this.progress === undefined ? 'loading' : 'progress' return this.progress === undefined ? 'loading' : 'progress'
}, },
radius () { radius() {
return this.size / 2 - this.strokeWidth return this.size / 2 - this.strokeWidth
}, },
offset () { offset() {
return this.radius * 3.14 / 2 return (this.radius * 3.14) / 2
}, },
strokeWidth () { strokeWidth() {
return this.size / 10 return this.size / 10
} }
} }
@@ -57,7 +69,10 @@ export default {
<style scoped> <style scoped>
.loader-svg { .loader-svg {
position: absolute; position: absolute;
left: 0; right: 0; top: 0; bottom: 0; left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none; fill: none;
stroke-linecap: round; stroke-linecap: round;
stroke: var(--color-accent); stroke: var(--color-accent);
@@ -69,7 +84,7 @@ export default {
.loading .loader-svg.front { .loading .loader-svg.front {
will-change: transform; will-change: transform;
animation: fill-animation-loading 1s cubic-bezier(1,1,1,1) 0s infinite; animation: fill-animation-loading 1s cubic-bezier(1, 1, 1, 1) 0s infinite;
transform-origin: center; transform-origin: center;
} }
@@ -96,10 +111,10 @@ export default {
} }
.progress .loader-svg.bg { .progress .loader-svg.bg {
animation: bg-animation 1.5s cubic-bezier(1,1,1,1) 0s infinite; animation: bg-animation 1.5s cubic-bezier(1, 1, 1, 1) 0s infinite;
} }
@keyframes bg-animation{ @keyframes bg-animation {
0% { 0% {
r: 8; r: 8;
} }
@@ -108,8 +123,7 @@ export default {
r: 9; r: 9;
} }
100% { 100% {
r: 8; r: 8;
} }
} }
</style> </style>

View File

@@ -1,10 +1,17 @@
<template> <template>
<div class="logs-container" ref="logsContainer"> <div ref="logsContainer" class="logs-container">
<div v-for="(msg, index) in messages" :key="index" class="msg"> <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 === 'error'" src="~@/assets/images/error.svg" />
<img v-if="msg.type === 'info'" :src="require('@/assets/images/info.svg')" width="20px"> <img
<img v-if="msg.type === 'success'" :src="require('@/assets/images/success.svg')"> v-if="msg.type === 'info'"
<loading-indicator v-if="msg.type === 'loading'" :progress="msg.progress" /> src="~@/assets/images/info.svg"
width="20px"
/>
<img v-if="msg.type === 'success'" src="~@/assets/images/success.svg" />
<loading-indicator
v-if="msg.type === 'loading'"
:progress="msg.progress"
/>
<span class="msg-text">{{ serializeMessage(msg) }}</span> <span class="msg-text">{{ serializeMessage(msg) }}</span>
</div> </div>
</div> </div>
@@ -14,17 +21,18 @@
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/LoadingIndicator'
export default { export default {
name: 'logs', name: 'Logs',
props: ['messages'],
components: { LoadingIndicator }, components: { LoadingIndicator },
props: { messages: Array },
emits: [],
watch: { watch: {
'messages.length': 'scrollToBottom' 'messages.length': 'scrollToBottom'
}, },
mounted () { mounted() {
this.scrollToBottom() this.scrollToBottom()
}, },
methods: { methods: {
async scrollToBottom () { async scrollToBottom() {
const container = this.$refs.logsContainer const container = this.$refs.logsContainer
if (container) { if (container) {
await this.$nextTick() await this.$nextTick()
@@ -32,7 +40,7 @@ export default {
} }
}, },
serializeMessage (msg) { serializeMessage(msg) {
let result = '' let result = ''
if (msg.row !== null && msg.row !== undefined) { if (msg.row !== null && msg.row !== undefined) {
if (msg.type === 'error') { if (msg.type === 'error') {
@@ -43,7 +51,7 @@ export default {
} }
result += msg.message result += msg.message
if (!(/(\.|!|\?)$/.test(result))) { if (!/(\.|!|\?)$/.test(result)) {
result += '.' result += '.'
} }

View File

@@ -7,10 +7,14 @@
{ 'splitpanes-dragging': dragging } { 'splitpanes-dragging': dragging }
]" ]"
> >
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
<div <div
class="splitpanes-pane" ref="movableSplitter"
class="movable-splitter"
:style="movableSplitterStyle"
/>
<div
ref="left" ref="left"
class="splitpanes-pane"
:size="paneBefore.size" :size="paneBefore.size"
max-size="30" max-size="30"
:style="styles.before" :style="styles.before"
@@ -27,8 +31,11 @@
:class="[ :class="[
'toggle-btns', 'toggle-btns',
{ {
'both': after.max === 100 && before.max === 100 && both:
paneAfter.size > 0 && paneBefore.size > 0 after.max === 100 &&
before.max === 100 &&
paneAfter.size > 0 &&
paneBefore.size > 0
} }
]" ]"
> >
@@ -39,9 +46,9 @@
> >
<img <img
class="direction-icon" class="direction-icon"
:src="require('@/assets/images/chevron.svg')" src="~@/assets/images/chevron.svg"
:style="directionBeforeIconStyle" :style="directionBeforeIconStyle"
> />
</div> </div>
<div <div
v-if="before.max === 100 && paneBefore.size > 0" v-if="before.max === 100 && paneBefore.size > 0"
@@ -50,18 +57,14 @@
> >
<img <img
class="direction-icon" class="direction-icon"
:src="require('@/assets/images/chevron.svg')" src="~@/assets/images/chevron.svg"
:style="directionAfterIconStyle" :style="directionAfterIconStyle"
> />
</div> </div>
</div> </div>
</div> </div>
<!-- splitter end --> <!-- splitter end -->
<div <div ref="right" class="splitpanes-pane" :style="styles.after">
class="splitpanes-pane"
ref="right"
:style="styles.after"
>
<slot name="right-pane" /> <slot name="right-pane" />
</div> </div>
</div> </div>
@@ -86,15 +89,19 @@ export default {
} }
} }
}, },
data () { emits: [],
data() {
return { return {
container: null, container: null,
paneBefore: this.before, paneBefore: this.before,
paneAfter: this.after, paneAfter: this.after,
beforeMinimising: !this.after.size || !this.before.size ? this.default : { beforeMinimising:
before: this.before.size, !this.after.size || !this.before.size
after: this.after.size ? this.default
}, : {
before: this.before.size,
after: this.after.size
},
dragging: false, dragging: false,
movableSplitter: { movableSplitter: {
top: 0, top: 0,
@@ -104,19 +111,23 @@ export default {
} }
}, },
computed: { computed: {
styles () { styles() {
return { return {
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` }, before: {
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` } [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
}
} }
}, },
movableSplitterStyle () { movableSplitterStyle() {
const style = { ...this.movableSplitter } const style = { ...this.movableSplitter }
style.top += '%' style.top += '%'
style.left += '%' style.left += '%'
return style return style
}, },
directionBeforeIconStyle () { directionBeforeIconStyle() {
const expanded = this.paneBefore.size !== 0 const expanded = this.paneBefore.size !== 0
const translation = 'translate(-50%, -50%) ' const translation = 'translate(-50%, -50%) '
let rotation = '' let rotation = ''
@@ -131,7 +142,7 @@ export default {
transform: translation + rotation transform: translation + rotation
} }
}, },
directionAfterIconStyle () { directionAfterIconStyle() {
const expanded = this.paneAfter.size !== 0 const expanded = this.paneAfter.size !== 0
const translation = 'translate(-50%, -50%)' const translation = 'translate(-50%, -50%)'
let rotation = '' let rotation = ''
@@ -147,37 +158,48 @@ export default {
} }
} }
}, },
mounted() {
this.container = this.$refs.container
},
methods: { methods: {
bindEvents () { bindEvents() {
// Passive: false to prevent scrolling while touch dragging. // Passive: false to prevent scrolling while touch dragging.
document.addEventListener('mousemove', this.onMouseMove, { passive: false }) document.addEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.addEventListener('mouseup', this.onMouseUp) document.addEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) { if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, { passive: false }) document.addEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.addEventListener('touchend', this.onMouseUp) document.addEventListener('touchend', this.onMouseUp)
} }
}, },
unbindEvents () { unbindEvents() {
document.removeEventListener('mousemove', this.onMouseMove, { passive: false }) document.removeEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.removeEventListener('mouseup', this.onMouseUp) document.removeEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) { if ('ontouchstart' in window) {
document.removeEventListener('touchmove', this.onMouseMove, { passive: false }) document.removeEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.removeEventListener('touchend', this.onMouseUp) document.removeEventListener('touchend', this.onMouseUp)
} }
}, },
onMouseMove (event) { onMouseMove(event) {
event.preventDefault() event.preventDefault()
this.dragging = true this.dragging = true
this.movableSplitter.visibility = 'visible' this.movableSplitter.visibility = 'visible'
this.moveSplitter(event) this.moveSplitter(event)
}, },
onMouseUp () { onMouseUp() {
if (this.dragging) { if (this.dragging) {
const dragPercentage = this.horizontal const dragPercentage = this.horizontal
? this.movableSplitter.top ? this.movableSplitter.top
@@ -198,7 +220,7 @@ export default {
this.unbindEvents() this.unbindEvents()
}, },
moveSplitter (event) { moveSplitter(event) {
const splitterInfo = { const splitterInfo = {
container: this.container, container: this.container,
paneBeforeMax: this.paneBefore.max, paneBeforeMax: this.paneBefore.max,
@@ -210,21 +232,19 @@ export default {
this.movableSplitter[dir] = offset this.movableSplitter[dir] = offset
}, },
togglePane (pane) { togglePane(pane) {
if (pane.size > 0) { if (pane.size > 0) {
this.beforeMinimising.before = this.paneBefore.size this.beforeMinimising.before = this.paneBefore.size
this.beforeMinimising.after = this.paneAfter.size this.beforeMinimising.after = this.paneAfter.size
pane.size = 0 pane.size = 0
const otherPane = pane === this.paneBefore ? this.paneAfter : this.paneBefore const otherPane =
pane === this.paneBefore ? this.paneAfter : this.paneBefore
otherPane.size = 100 - pane.size otherPane.size = 100 - pane.size
} else { } else {
this.paneBefore.size = this.beforeMinimising.before this.paneBefore.size = this.beforeMinimising.before
this.paneAfter.size = this.beforeMinimising.after this.paneAfter.size = this.beforeMinimising.after
} }
} }
},
mounted () {
this.container = this.$refs.container
} }
} }
</script> </script>
@@ -236,9 +256,15 @@ export default {
position: relative; position: relative;
} }
.splitpanes-vertical {flex-direction: row;} .splitpanes-vertical {
.splitpanes-horizontal {flex-direction: column;} flex-direction: row;
.splitpanes-dragging * {user-select: none;} }
.splitpanes-horizontal {
flex-direction: column;
}
.splitpanes-dragging * {
user-select: none;
}
.splitpanes-pane { .splitpanes-pane {
width: 100%; width: 100%;
@@ -278,14 +304,14 @@ export default {
.movable-splitter { .movable-splitter {
position: absolute; position: absolute;
background-color:rgba(162, 177, 198, 0.5); background-color: rgba(162, 177, 198, 0.5);
} }
.splitpanes-vertical > .splitpanes-splitter, .splitpanes-vertical > .splitpanes-splitter,
.splitpanes-vertical > .movable-splitter { .splitpanes-vertical > .movable-splitter {
width: 8px; width: 8px;
z-index: 5; z-index: 5;
height: 100% height: 100%;
} }
.splitpanes-horizontal > .splitpanes-splitter, .splitpanes-horizontal > .splitpanes-splitter,
@@ -336,20 +362,32 @@ export default {
left: 50%; 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); 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; border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
margin-left: -1px; 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; 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); border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
margin-top: -1px; margin-top: -1px;
} }

View File

@@ -1,10 +1,9 @@
export default { export default {
// Get the cursor position relative to the splitpane container. // Get the cursor position relative to the splitpane container.
getCurrentMouseDrag (event, container) { getCurrentMouseDrag(event, container) {
const rect = container.getBoundingClientRect() const rect = container.getBoundingClientRect()
const { clientX, clientY } = ('ontouchstart' in window && event.touches) const { clientX, clientY } =
? event.touches[0] 'ontouchstart' in window && event.touches ? event.touches[0] : event
: event
return { return {
x: clientX - rect.left, x: clientX - rect.left,
y: clientY - rect.top y: clientY - rect.top
@@ -12,23 +11,35 @@ export default {
}, },
// Returns the drag percentage of the splitter relative to the 2 panes it's inbetween. // Returns the drag percentage of the splitter relative to the 2 panes it's inbetween.
getCurrentDragPercentage (event, container, isHorisontal) { getCurrentDragPercentage(event, container, isHorisontal) {
let drag = this.getCurrentMouseDrag(event, container) let drag = this.getCurrentMouseDrag(event, container)
drag = drag[isHorisontal ? 'y' : 'x'] drag = drag[isHorisontal ? 'y' : 'x']
const containerSize = container[isHorisontal ? 'clientHeight' : 'clientWidth'] const containerSize =
return drag * 100 / containerSize container[isHorisontal ? 'clientHeight' : 'clientWidth']
return (drag * 100) / containerSize
}, },
// Returns the new position in percents. // Returns the new position in percents.
calculateOffset (event, { container, isHorisontal, paneBeforeMax, paneAfterMax }) { calculateOffset(
const dragPercentage = this.getCurrentDragPercentage(event, container, isHorisontal) event,
{ container, isHorisontal, paneBeforeMax, paneAfterMax }
) {
const dragPercentage = this.getCurrentDragPercentage(
event,
container,
isHorisontal
)
const paneBeforeMaxReached = paneBeforeMax < 100 && (dragPercentage >= paneBeforeMax) const paneBeforeMaxReached =
const paneAfterMaxReached = paneAfterMax < 100 && (dragPercentage <= 100 - paneAfterMax) paneBeforeMax < 100 && dragPercentage >= paneBeforeMax
const paneAfterMaxReached =
paneAfterMax < 100 && dragPercentage <= 100 - paneAfterMax
// Prevent dragging beyond pane max. // Prevent dragging beyond pane max.
if (paneBeforeMaxReached || paneAfterMaxReached) { if (paneBeforeMaxReached || paneAfterMaxReached) {
return paneBeforeMaxReached ? paneBeforeMax : Math.max(100 - paneAfterMax, 0) return paneBeforeMaxReached
? paneBeforeMax
: Math.max(100 - paneAfterMax, 0)
} else { } else {
return Math.min(Math.max(dragPercentage, 0), paneBeforeMax) return Math.min(Math.max(dragPercentage, 0), paneBeforeMax)
} }

View File

@@ -1,32 +1,36 @@
<template> <template>
<paginate <paginate
:page-count="pageCount"
:page-range="5"
:margin-pages="1"
:prev-text="chevron"
:next-text="chevron"
:no-li-surround="true"
container-class="paginator-continer"
page-link-class="paginator-page-link"
active-class="paginator-active-page"
break-view-link-class="paginator-break"
next-link-class="paginator-next"
prev-link-class="paginator-prev"
disabled-class="paginator-disabled"
v-model="page" v-model="page"
:pageCount="pageCount"
:pageRange="5"
:marginPages="1"
:prevText="chevron"
:nextText="chevron"
:noLiSurround="true"
containerClass="paginator-continer"
pageLinkClass="paginator-page-link"
activeClass="paginator-active-page"
breakViewLinkClass="paginator-break"
nextLinkClass="paginator-next"
prevLinkClass="paginator-prev"
disabledClass="paginator-disabled"
/> />
</template> </template>
<script> <script>
import Paginate from 'vuejs-paginate' import Paginate from 'vuejs-paginate-next'
export default { export default {
name: 'Pager', name: 'Pager',
components: { Paginate }, components: { Paginate },
props: ['pageCount', 'value'], props: {
data () { pageCount: Number,
modelValue: Number
},
emits: ['update:modelValue'],
data() {
return { return {
page: this.value, page: this.modelValue,
chevron: ` chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none"> <svg width="9" height="9" viewBox="0 0 8 12" fill="none">
<path <path
@@ -38,11 +42,11 @@ export default {
} }
}, },
watch: { watch: {
page () { page() {
this.$emit('input', this.page) this.$emit('update:modelValue', this.page)
}, },
value () { modelValue() {
this.page = this.value this.page = this.modelValue
} }
} }
} }
@@ -54,48 +58,52 @@ export default {
align-items: center; align-items: center;
line-height: 10px; line-height: 10px;
} }
>>> .paginator-page-link { :deep(a) {
cursor: pointer;
}
:deep(.paginator-page-link) {
padding: 2px 3px; padding: 2px 3px;
margin: 0 5px; margin: 0 5px;
display: block; display: block;
color: var(--color-text-base); color: var(--color-text-base);
font-size: 11px; font-size: 11px;
} }
>>> .paginator-page-link:hover { :deep(.paginator-page-link:hover) {
color: var(--color-text-active); color: var(--color-text-active);
} }
>>> .paginator-page-link:active, :deep(.paginator-page-link:active),
>>> .paginator-page-link:visited, :deep(.paginator-page-link:visited),
>>> .paginator-page-link:focus, :deep(.paginator-page-link:focus),
>>> .paginator-next:active, :deep(.paginator-next:active),
>>> .paginator-next:visited, :deep(.paginator-next:visited),
>>> .paginator-next:focus, :deep(.paginator-next:focus),
>>> .paginator-prev:active, :deep(.paginator-prev:active),
>>> .paginator-prev:visited, :deep(.paginator-prev:visited),
>>> .paginator-prev:focus { :deep(.paginator-prev:focus) {
outline: none; outline: none;
} }
>>> .paginator-active-page, :deep(.paginator-active-page),
>>> .paginator-active-page:hover { :deep(.paginator-active-page:hover) {
color: var(--color-accent); color: var(--color-accent);
} }
>>> .paginator-break:hover, :deep(.paginator-break:hover),
>>> .paginator-disabled:hover { :deep(.paginator-disabled:hover) {
cursor: default; cursor: default;
} }
>>> .paginator-prev svg { :deep(.paginator-prev svg) {
transform: rotate(180deg); transform: rotate(180deg);
} }
>>> .paginator-next:hover path, :deep(.paginator-next:hover path),
>>> .paginator-prev:hover path { :deep(.paginator-prev:hover path) {
fill: var(--color-text-active); fill: var(--color-text-active);
} }
>>> .paginator-disabled path, :deep(.paginator-disabled path),
>>> .paginator-disabled:hover path { :deep(.paginator-disabled:hover path) {
fill: var(--color-text-light-2); fill: var(--color-text-light-2);
} }
</style> </style>

View File

@@ -1,74 +1,75 @@
<template> <template>
<div> <div>
<div class="rounded-bg"> <div class="rounded-bg">
<div class="header-container" ref="header-container"> <div ref="header-container" class="header-container">
<div> <div>
<div <div
v-for="(th, index) in header" v-for="(th, index) in header"
:key="index"
class="fixed-header" class="fixed-header"
:style="{ width: `${th.width}px` }" :style="{ width: `${th.width}px` }"
:key="index" :title="th.name"
> >
{{ th.name }} {{ th.name }}
</div> </div>
</div> </div>
</div> </div>
<div <div
class="table-container"
ref="table-container" ref="table-container"
class="table-container"
@scroll="onScrollTable" @scroll="onScrollTable"
> >
<table <table
ref="table" ref="table"
class="sqliteviz-table" class="sqliteviz-table"
tabindex="0" tabindex="0"
@keydown="onTableKeydown" @keydown="onTableKeydown"
> >
<thead> <thead>
<tr> <tr>
<th v-for="(th, index) in columns" :key="index" ref="th"> <th v-for="(th, index) in columns" :key="index" ref="th">
<div class="cell-data" :style="cellStyle">{{ th }}</div> <div class="cell-data" :style="cellStyle">{{ th }}</div>
</th> </th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex"> <tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td <td
v-for="(col, colIndex) in columns" v-for="(col, colIndex) in columns"
:data-col="colIndex" :key="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1" :data-col="colIndex"
:data-isNull="isNull(getCellValue(col, rowIndex))" :data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isBlob="isBlob(getCellValue(col, rowIndex))" :data-isNull="isNull(getCellValue(col, rowIndex))"
:key="colIndex" :data-isBlob="isBlob(getCellValue(col, rowIndex))"
:aria-selected="false" :aria-selected="false"
@click="onCellClick" @click="onCellClick"
> >
<div class="cell-data" :style="cellStyle"> <div class="cell-data" :style="cellStyle">
{{ getCellText(col, rowIndex) }} {{ getCellText(col, rowIndex) }}
</div> </div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
<div class="table-footer"> <div class="table-footer">
<div class="table-footer-count"> <div class="table-footer-count">
{{ rowCount }} {{rowCount === 1 ? 'row' : 'rows'}} retrieved {{ rowCount }} {{ rowCount === 1 ? 'row' : 'rows' }} retrieved
<span v-if="preview">for preview</span> <span v-if="preview">for preview</span>
<span v-if="time">in {{ time }}</span> <span v-if="time">in {{ time }}</span>
</div> </div>
<pager <pager
v-show="pageCount > 1" v-show="pageCount > 1"
:page-count="pageCount"
v-model="currentPage" v-model="currentPage"
:pageCount="pageCount"
/> />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
import Pager from './Pager' import Pager from './Pager.vue'
export default { export default {
name: 'SqlTable', name: 'SqlTable',
@@ -87,7 +88,8 @@ export default {
preview: Boolean, preview: Boolean,
selectedCellCoordinates: Object selectedCellCoordinates: Object
}, },
data () { emits: ['updateSelectedCell'],
data() {
return { return {
header: null, header: null,
tableWidth: null, tableWidth: null,
@@ -97,20 +99,20 @@ export default {
} }
}, },
computed: { computed: {
columns () { columns() {
return this.dataSet.columns return this.dataSet.columns
}, },
rowCount () { rowCount() {
return this.dataSet.values[this.columns[0]].length return this.dataSet.values[this.columns[0]].length
}, },
cellStyle () { cellStyle() {
const eq = this.tableWidth / this.columns.length const eq = this.tableWidth / this.columns.length
return { maxWidth: `${Math.max(eq, 100)}px` } return { maxWidth: `${Math.max(eq, 100)}px` }
}, },
pageCount () { pageCount() {
return Math.ceil(this.rowCount / this.pageSize) return Math.ceil(this.rowCount / this.pageSize)
}, },
currentPageData () { currentPageData() {
const start = (this.currentPage - 1) * this.pageSize const start = (this.currentPage - 1) * this.pageSize
let end = start + this.pageSize let end = start + this.pageSize
if (end > this.rowCount - 1) { if (end > this.rowCount - 1) {
@@ -123,31 +125,44 @@ export default {
} }
} }
}, },
mounted () { watch: {
currentPageData() {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet() {
this.currentPage = 1
}
},
mounted() {
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth) this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table) this.resizeObserver.observe(this.$refs.table)
this.calculateHeadersWidth() this.calculateHeadersWidth()
if (this.selectedCellCoordinates) { if (this.selectedCellCoordinates) {
const { row, col } = this.selectedCellCoordinates const { row, col } = this.selectedCellCoordinates
const cell = this.$refs.table const cell = this.$refs.table.querySelector(
.querySelector(`td[data-col="${col}"][data-row="${row}"]`) `td[data-col="${col}"][data-row="${row}"]`
)
if (cell) { if (cell) {
this.selectCell(cell) this.selectCell(cell)
} }
} }
}, },
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.table)
},
methods: { methods: {
isBlob (value) { isBlob(value) {
return value && ArrayBuffer.isView(value) return value && ArrayBuffer.isView(value)
}, },
isNull (value) { isNull(value) {
return value === null return value === null
}, },
getCellValue (col, rowIndex) { getCellValue(col, rowIndex) {
return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start] return this.dataSet.values[col][rowIndex - 1 + this.currentPageData.start]
}, },
getCellText (col, rowIndex) { getCellText(col, rowIndex) {
const value = this.getCellValue(col, rowIndex) const value = this.getCellValue(col, rowIndex)
if (this.isNull(value)) { if (this.isNull(value)) {
return 'NULL' return 'NULL'
@@ -157,7 +172,7 @@ export default {
} }
return value return value
}, },
calculateHeadersWidth () { calculateHeadersWidth() {
this.tableWidth = this.$refs['table-container'].offsetWidth this.tableWidth = this.$refs['table-container'].offsetWidth
this.$nextTick(() => { this.$nextTick(() => {
this.header = this.$refs.th.map(th => { this.header = this.$refs.th.map(th => {
@@ -165,10 +180,11 @@ export default {
}) })
}) })
}, },
onScrollTable () { onScrollTable() {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft this.$refs['header-container'].scrollLeft =
this.$refs['table-container'].scrollLeft
}, },
onTableKeydown (e) { onTableKeydown(e) {
const keyCodeMap = { const keyCodeMap = {
37: 'left', 37: 'left',
39: 'right', 39: 'right',
@@ -186,10 +202,10 @@ export default {
this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode]) this.moveFocusInTable(this.selectedCellElement, keyCodeMap[e.keyCode])
}, },
onCellClick (e) { onCellClick(e) {
this.selectCell(e.target.closest('td'), false) this.selectCell(e.target.closest('td'), false)
}, },
selectCell (cell, scrollTo = true) { selectCell(cell, scrollTo = true) {
if (!cell) { if (!cell) {
if (this.selectedCellElement) { if (this.selectedCellElement) {
this.selectedCellElement.ariaSelected = 'false' this.selectedCellElement.ariaSelected = 'false'
@@ -212,7 +228,7 @@ export default {
this.$emit('updateSelectedCell', this.selectedCellElement) this.$emit('updateSelectedCell', this.selectedCellElement)
}, },
moveFocusInTable (initialCell, direction) { moveFocusInTable(initialCell, direction) {
const currentRowIndex = +initialCell.dataset.row const currentRowIndex = +initialCell.dataset.row
const currentColIndex = +initialCell.dataset.col const currentColIndex = +initialCell.dataset.col
let newRowIndex, newColIndex let newRowIndex, newColIndex
@@ -241,24 +257,13 @@ export default {
newColIndex = currentColIndex newColIndex = currentColIndex
} }
const newCell = this.$refs.table const newCell = this.$refs.table.querySelector(
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`) `td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
)
if (newCell) { if (newCell) {
this.selectCell(newCell) this.selectCell(newCell)
} }
} }
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData () {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet () {
this.currentPage = 1
}
} }
} }
</script> </script>
@@ -270,7 +275,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover { .sqliteviz-table tbody td:hover {
background-color: var(--color-bg-light-3); background-color: var(--color-bg-light-3);
} }
.sqliteviz-table tbody td[aria-selected="true"] { .sqliteviz-table tbody td[aria-selected='true'] {
box-shadow:inset 0 0 0 1px var(--color-accent); box-shadow: inset 0 0 0 1px var(--color-accent);
} }
</style> </style>

View File

@@ -1,17 +1,25 @@
<template> <template>
<div> <div>
<div v-if="label" :class="['text-field-label', { error: errorMsg }, {'disabled': disabled}]"> <div
v-if="label"
:class="['text-field-label', { error: errorMsg }, { disabled: disabled }]"
>
{{ label }} {{ label }}
<hint-icon class="hint" v-if="hint" :hint="hint" :max-width="maxHintWidth || '149px'"/> <hint-icon
v-if="hint"
class="hint"
:hint="hint"
:maxWidth="maxHintWidth || '149px'"
/>
</div> </div>
<input <input
type="text" type="text"
:placeholder="placeholder" :placeholder="placeholder"
:class="{ error: errorMsg }" :class="{ error: errorMsg }"
:style="{ width: width }" :style="{ width: width }"
:value="value" :value="modelValue"
:disabled="disabled" :disabled="disabled"
@input="$emit('input', $event.target.value)" @input="$emit('update:modelValue', $event.target.value)"
/> />
<div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div> <div v-show="errorMsg" class="text-field-error">{{ errorMsg }}</div>
</div> </div>
@@ -20,9 +28,19 @@
<script> <script>
import HintIcon from '@/components/svg/hint' import HintIcon from '@/components/svg/hint'
export default { export default {
name: 'textField', name: 'TextField',
props: ['placeholder', 'label', 'errorMsg', 'value', 'width', 'hint', 'maxHintWidth', 'disabled'], components: { HintIcon },
components: { HintIcon } props: {
placeholder: String,
label: String,
errorMsg: String,
modelValue: String,
width: String,
hint: String,
maxHintWidth: String,
disabled: Boolean
},
emits: ['update:modelValue']
} }
</script> </script>
@@ -66,7 +84,7 @@ input.error {
position: relative; position: relative;
} }
.text-field-label .hint{ .text-field-label .hint {
position: absolute; position: absolute;
top: -2px; top: -2px;
right: -22px; right: -22px;

View File

@@ -29,11 +29,11 @@
</g> </g>
<defs> <defs>
<clipPath id="clip0"> <clipPath id="clip0">
<rect width="18" height="18" fill="white"/> <rect width="18" height="18" fill="white" />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Add new table from CSV, JSON or NDJSON Add new table from CSV, JSON or NDJSON
</span> </span>
</span> </span>
@@ -45,9 +45,10 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'AddTableIcon', name: 'AddTableIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
props: ['tooltip'], props: { tooltip: String },
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -11,10 +11,9 @@
13.5L16.35 6.75L17.9475 8.33625Z" 13.5L16.35 6.75L17.9475 8.33625Z"
fill="#506784" fill="#506784"
/> />
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>

View File

@@ -1,40 +1,41 @@
<template> <template>
<div> <div>
<svg <svg
class="db-edit-icon" class="db-edit-icon"
width="18" width="18"
height="18" height="18"
viewBox="0 0 18 18" viewBox="0 0 18 18"
fill="none" fill="none"
@click.stop="onClick" @click.stop="onClick"
@mouseenter="showTooltip" @mouseenter="showTooltip"
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
<path <path
d="M3 10.5V12.75C3 14.25 5.2875 15.54 8.25 15.75V13.5825L8.3475 13.5C5.34 13.32 3 12.045 3 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 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 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 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 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 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" 8.3475H15.75ZM14.28 9.66L9.75 14.205V15.75H11.295L15.84 11.1975L14.28 9.66Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Load another database, CSV, JSON or NDJSON Load another database, CSV, JSON or NDJSON
</span> </span>
</div> </div>
</template> </template>
<script> <script>
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'changeDbIcon', name: 'ChangeDbIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -46,7 +41,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'ChartIcon' name: 'ChartIcon'
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg <svg
:class="['clear-icon', {'disabled': disabled}]" :class="['clear-icon', { disabled: disabled }]"
width="20" width="20"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -21,10 +21,9 @@
</template> </template>
<script> <script>
export default { export default {
name: 'ClearIcon', name: 'ClearIcon',
props: ['disabled'] props: { disabled: Boolean }
} }
</script> </script>
@@ -42,6 +41,6 @@ export default {
} }
.disabled.clear-icon:hover path { .disabled.clear-icon:hover path {
fill: #C8D4E3; fill: #c8d4e3;
} }
</style> </style>

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0 d="M14.1917 1.3851H12.4806V0.703125C12.4806 0.314758 12.1658 0 11.7775 0H6.246C5.85764 0
5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422 5.54288 0.314758 5.54288 0.703125V1.3851H3.83203C2.86276 1.3851 2.07422 2.17365 2.07422
@@ -26,7 +21,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'ClipboardIcon' name: 'ClipboardIcon'
} }

View File

@@ -1,12 +1,12 @@
<template> <template>
<svg <svg
@click.stop="$emit('click')" :class="['icon', { disabled: disabled }]"
:class="['icon', {'disabled': disabled }]"
:width="size" :width="size"
:height="size" :height="size"
viewBox="0 0 14 14" viewBox="0 0 14 14"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
> >
<path <path
d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14 d="M14 1.41L12.59 0L7 5.59L1.41 0L0 1.41L5.59 7L0 12.59L1.41 14L7 8.41L12.59 14L14
@@ -30,7 +30,8 @@ export default {
required: false, required: false,
default: false default: false
} }
} },
emits: ['click']
} }
</script> </script>

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -17,7 +12,7 @@
6.91686 13.5552 6.91522Z" 6.91686 13.5552 6.91522Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6"/> <circle cx="5.50049" cy="6.00339" r="1.5" fill="#A2B1C6" />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -27,11 +22,10 @@
1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z" 1.21788ZM16.0374 2.71788L1.96424 2.713L1.96289 15.2773L16.036 15.2821L16.0374 2.71788Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'DataViewIcon' name: 'DataViewIcon'
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<svg <svg
:class="['chevron-icon', {'disabled': disabled}]" :class="['chevron-icon', { disabled: disabled }]"
width="20" width="20"
height="20" height="20"
viewBox="0 0 20 20" viewBox="0 0 20 20"
@@ -15,10 +15,9 @@
</template> </template>
<script> <script>
export default { export default {
name: 'DropDownChevron', name: 'DropDownChevron',
props: ['disabled'] props: { disabled: Boolean }
} }
</script> </script>
@@ -36,6 +35,6 @@ export default {
} }
.disabled.chevron-icon:hover path { .disabled.chevron-icon:hover path {
fill: #C8D4E3; fill: #c8d4e3;
} }
</style> </style>

View File

@@ -21,6 +21,5 @@
</template> </template>
<script> <script>
export default { export default {}
}
</script> </script>

View File

@@ -17,7 +17,7 @@
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
{{ tooltip }} {{ tooltip }}
</span> </span>
</span> </span>
@@ -29,9 +29,13 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'ExportIcon', name: 'ExportIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
props: ['tooltip', 'tooltipPosition'], props: {
tooltip: String,
tooltipPosition: String
},
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="19" height="18" viewBox="0 0 19 18" fill="none">
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<path <path
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043 d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83 15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
@@ -34,7 +29,7 @@
14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407 14.6699C9.53809 14.6699 9.73877 14.6157 9.88525 14.5073C10.0347 14.3959 10.1094 14.2407
10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463 10.1094 14.0414ZM14.9258 14.0019L16.2002 9.34369H17.9229L15.7695 15.7421H14.082L11.9463
9.34369H13.6558L14.9258 14.0019Z" 9.34369H13.6558L14.9258 14.0019Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633 d="M3.03345 0.991333H4.89869V2.49133H3.03345V7.93074H1.53345V2.49133C1.53345 1.66633
@@ -55,7 +50,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'ExportToCsvIcon' name: 'ExportToCsvIcon'
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="19" height="18" viewBox="0 0 19 18" fill="none">
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<path <path
d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604 d="M4.28369 13.9966C4.28369 13.7711 4.20312 13.5953 4.04199 13.4693C3.88379 13.3433 3.604
13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798 13.213 3.20264 13.0782C2.80127 12.9434 2.47314 12.813 2.21826 12.6871C1.38916 12.2798
@@ -54,7 +49,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'ExportToSvgIcon' name: 'ExportToSvgIcon'
} }

View File

@@ -33,7 +33,11 @@
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="{...tooltipStyle, maxWidth: maxWidth }" ref="tooltip"> <span
ref="tooltip"
class="icon-tooltip"
:style="{ ...tooltipStyle, maxWidth: maxWidth }"
>
{{ hint }} {{ hint }}
</span> </span>
</div> </div>
@@ -44,10 +48,14 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'HintIcon', name: 'HintIcon',
props: ['hint', 'maxWidth'],
mixins: [tooltipMixin], mixins: [tooltipMixin],
props: {
hint: String,
maxWidth: String
},
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -11,8 +11,8 @@
7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023 7.89917V9.2439L5.1626 10.0745ZM8.99023 13.3H7.93994L10.124 6.35229H11.1787L8.99023
13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192 13.3ZM14.1099 10.0613L11.7192 9.24829V7.90356L15.582 9.4856V10.6545L11.7192
12.2366V10.8918L14.1099 10.0613Z" 12.2366V10.8918L14.1099 10.0613Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872 d="M2.17041 0.0637207H16.2185V1.56372H2.17041V9.30354H0.67041V1.56372C0.67041 0.73872
1.34541 0.0637207 2.17041 0.0637207Z" 1.34541 0.0637207 2.17041 0.0637207Z"

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -14,14 +9,13 @@
14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z" 14.1914C14.8372 13.965 15.0161 13.5645 15.0161 12.8467V9.43008H13.1914L15.7661 5.13901Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6"/> <path d="M6.41943 0H18.4194V4H6.41943V0Z" fill="#A2B1C6" />
<path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6"/> <path d="M0.419434 6H4.41943V18H0.419434V6Z" fill="#A2B1C6" />
<path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6"/> <path d="M0.419434 0H4.41943V4H0.419434V0Z" fill="#A2B1C6" />
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'PivotIcon' name: 'PivotIcon'
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
d="M9 5.51953C6.57686 5.51953 4.60547 7.49092 4.60547 9.91406C4.60547 12.3372 6.57686 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 14.3086 9 14.3086C11.4231 14.3086 13.3945 12.3372 13.3945 9.91406C13.3945 7.49092 11.4231
@@ -30,7 +25,10 @@
5.5195V15.0117Z" 5.5195V15.0117Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z" fill="#A2B1C6"/> <path
d="M15.1875 6.22266H13.7812V7.62891H15.1875V6.22266Z"
fill="#A2B1C6"
/>
</svg> </svg>
</template> </template>

View File

@@ -1,15 +1,10 @@
<template> <template>
<svg <svg width="19" height="19" viewBox="0 0 19 19" fill="none">
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<g clip-path="url(#clip0_2130_5292)"> <g clip-path="url(#clip0_2130_5292)">
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304 d="M1.85303 11.3794L1.85303 7.80371L5.86304 7.80371L5.86304
11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428 11.3794L1.85303 11.3794ZM7.36304 11.3794L7.36304 7.80371L11.3428
7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853 7.80371L11.3428 11.3794L7.36304 11.3794ZM12.8428 11.3794L16.853
11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353 11.3794L16.853 7.80371L12.8428 7.80371L12.8428 11.3794ZM15.353
@@ -29,24 +24,23 @@
14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685 14.3121L6.76685 12.8794L4.67027 12.8794ZM8.26685 12.8794L8.26685
14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887 14.3121L10.387 14.3121L10.387 12.8794L8.26685 12.8794ZM11.887
12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z" 12.8794L11.887 14.3121L14.0315 14.3121L14.0315 12.8794L11.887 12.8794Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</g> </g>
<defs> <defs>
<clipPath id="clip0_2130_5292"> <clipPath id="clip0_2130_5292">
<rect <rect
width="18" width="18"
height="18" height="18"
fill="white" fill="white"
transform="translate(0.353027 18.5916) rotate(-90)" transform="translate(0.353027 18.5916) rotate(-90)"
/> />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'RowIcon' name: 'RowIcon'
} }

View File

@@ -1,16 +1,13 @@
<template> <template>
<svg <svg width="12" height="13" viewBox="0 0 12 13" fill="none">
width="12" <path
height="13" d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z"
viewBox="0 0 12 13" fill="#A2B1C6"
fill="none" />
>
<path d="M11.1624 6.94358L0.770043 12.9436L0.770043 0.943573L11.1624 6.94358Z" fill="#A2B1C6"/>
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'RunIcon' name: 'RunIcon'
} }

View File

@@ -24,7 +24,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'SortIcon', name: 'SortIcon',
props: { props: {

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="19" viewBox="0 0 18 19" fill="none">
width="18"
height="19"
viewBox="0 0 18 19"
fill="none"
>
<g clip-path="url(#clip0)"> <g clip-path="url(#clip0)">
<path <path
d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3 d="M4.5 1.51343H10.5L15 6.01343V8.45284H13.5V6.76343H9.75V3.01343H4.5V8.45284H3V3.01343C3
@@ -47,14 +42,18 @@
</g> </g>
<defs> <defs>
<clipPath id="clip0"> <clipPath id="clip0">
<rect width="18" height="18" fill="white" transform="translate(0 0.0134277)"/> <rect
width="18"
height="18"
fill="white"
transform="translate(0 0.0134277)"
/>
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'SqlEditorIcon' name: 'SqlEditorIcon'
} }

View File

@@ -1,10 +1,5 @@
<template> <template>
<svg <svg width="18" height="18" viewBox="0 0 18 18" fill="none">
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
>
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
@@ -41,7 +36,6 @@
</template> </template>
<script> <script>
export default { export default {
name: 'TableIcon' name: 'TableIcon'
} }

View File

@@ -17,9 +17,8 @@
</template> </template>
<script> <script>
export default { export default {
name: 'treeChevron', name: 'TreeChevron',
props: { props: {
expanded: { expanded: {
type: Boolean, type: Boolean,
@@ -31,7 +30,7 @@ export default {
<style scoped> <style scoped>
.chevron-icon { .chevron-icon {
-webkit-transition: transform .15s ease-in-out; -webkit-transition: transform 0.15s ease-in-out;
transition: transform .15s ease-in-out; transition: transform 0.15s ease-in-out;
} }
</style> </style>

View File

@@ -1,24 +1,19 @@
<template> <template>
<svg <svg width="19" height="19" viewBox="0 0 19 19" fill="none">
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<g clip-path="url(#clip0_2131_6054)"> <g clip-path="url(#clip0_2131_6054)">
<path <path
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991 d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734 12.9771 7.47439 13.4751 7.53264V3.14734C13.4751 2.31891 12.8035 1.64734
11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784 11.9751 1.64734L3.53784 1.64734C2.70941 1.64734 2.03784 2.31891 2.03784
3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784 3.14734L2.03784 11.5846C2.03784 12.413 2.70942 13.0846 3.53784
13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083 13.0846H10.0831C9.771 12.6184 9.58279 12.1055 9.51083
11.5846H3.53784Z" 11.5846H3.53784Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
<path <path
fill-rule="evenodd" fill-rule="evenodd"
clip-rule="evenodd" clip-rule="evenodd"
d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228 d="M14.7887 9.9291C15.4307 10.8837 15.1773 12.1779 14.2228
12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993 12.8199C13.2682 13.4618 11.974 13.2084 11.332 12.2539C10.69 11.2993
10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887 10.9434 10.0051 11.898 9.3631C12.8525 8.72113 14.1468 8.97454 14.7887
9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331 9.9291ZM14.4606 14.3901L16.6181 17.5982C16.8492 17.9419 17.3153 18.0331
@@ -26,24 +21,23 @@
13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002 13.5279C16.7949 12.3365 16.9801 10.4996 16.0334 9.092C14.9292 7.45002
12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873 12.7029 7.01412 11.0609 8.1184C9.41891 9.22268 8.98302 11.449 10.0873
13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z" 13.0909C11.062 14.5403 12.9109 15.05 14.4606 14.3901Z"
fill="#A2B1C6" fill="#A2B1C6"
/> />
</g> </g>
<defs> <defs>
<clipPath id="clip0_2131_6054"> <clipPath id="clip0_2131_6054">
<rect <rect
width="18" width="18"
height="18" height="18"
fill="white" fill="white"
transform="translate(0.5 18.5916) rotate(-90)" transform="translate(0.5 18.5916) rotate(-90)"
/> />
</clipPath> </clipPath>
</defs> </defs>
</svg> </svg>
</template> </template>
<script> <script>
export default { export default {
name: 'ViewCellValueIcon' name: 'ViewCellValueIcon'
} }

View File

@@ -0,0 +1,65 @@
import ReactPlotlyEditor from 'react-chart-editor'
import React, { createRef } from 'react'
import EditorControls from 'react-chart-editor/lib/EditorControls'
/**
* This extended ReactPlotlyEditor has a reference to PlotComponent.
* The reference makes it possible to call updatePlotly method of PlotComponent.
* updatePlotly method allows smoothly resize the plot
* when resize chart editor container.
*/
export default class ReactPlotlyEditorWithPlotRef extends ReactPlotlyEditor {
constructor(props) {
super(props)
this.plotComponentRef = createRef()
}
render() {
return (
<div className="plotly_editor">
{!this.props.hideControls && (
<EditorControls
graphDiv={this.state.graphDiv}
dataSources={this.props.dataSources}
dataSourceOptions={this.props.dataSourceOptions}
plotly={this.props.plotly}
onUpdate={this.props.onUpdate}
advancedTraceTypeSelector={this.props.advancedTraceTypeSelector}
locale={this.props.locale}
traceTypesConfig={this.props.traceTypesConfig}
dictionaries={this.props.dictionaries}
showFieldTooltips={this.props.showFieldTooltips}
srcConverters={this.props.srcConverters}
makeDefaultTrace={this.props.makeDefaultTrace}
glByDefault={this.props.glByDefault}
mapBoxAccess={Boolean(
this.props.config && this.props.config.mapboxAccessToken
)}
fontOptions={this.props.fontOptions}
chartHelp={this.props.chartHelp}
customConfig={this.props.customConfig}
>
{this.props.children}
</EditorControls>
)}
<div
className="plotly_editor_plot"
style={{ width: '100%', height: '100%' }}
>
<this.PlotComponent
ref={this.plotComponentRef}
data={this.props.data}
layout={this.props.layout}
frames={this.props.frames}
config={this.props.config}
useResizeHandler={this.props.useResizeHandler}
debug={this.props.debug}
onInitialized={this.handleRender}
onUpdate={this.handleRender}
style={{ width: '100%', height: '100%' }}
divId={this.props.divId}
/>
</div>
</div>
)
}
}

View File

@@ -1,8 +1,8 @@
import dereference from 'react-chart-editor/lib/lib/dereference' import * as dereference from 'react-chart-editor/lib/lib/dereference'
import plotly from 'plotly.js' import plotly from 'plotly.js'
import { nanoid } from 'nanoid' import { nanoid } from 'nanoid'
export function getOptionsFromDataSources (dataSources) { export function getOptionsFromDataSources(dataSources) {
if (!dataSources) { if (!dataSources) {
return [] return []
} }
@@ -13,7 +13,7 @@ export function getOptionsFromDataSources (dataSources) {
})) }))
} }
export function getOptionsForSave (state, dataSources) { export function getOptionsForSave(state, dataSources) {
// we don't need to save the data, only settings // we don't need to save the data, only settings
// so we modify state.data using dereference // so we modify state.data using dereference
const stateCopy = JSON.parse(JSON.stringify(state)) const stateCopy = JSON.parse(JSON.stringify(state))
@@ -21,11 +21,11 @@ export function getOptionsForSave (state, dataSources) {
for (const key in dataSources) { for (const key in dataSources) {
emptySources[key] = [] emptySources[key] = []
} }
dereference(stateCopy.data, emptySources) dereference.default(stateCopy.data, emptySources)
return stateCopy return stateCopy
} }
export async function getImageDataUrl (element, type) { export async function getImageDataUrl(element, type) {
const chartElement = element.querySelector('.js-plotly-plot') const chartElement = element.querySelector('.js-plotly-plot')
return await plotly.toImage(chartElement, { return await plotly.toImage(chartElement, {
format: type, format: type,
@@ -34,7 +34,7 @@ export async function getImageDataUrl (element, type) {
}) })
} }
export function getChartData (element) { export function getChartData(element) {
const chartElement = element.querySelector('.js-plotly-plot') const chartElement = element.querySelector('.js-plotly-plot')
return { return {
data: chartElement.data, data: chartElement.data,
@@ -42,7 +42,7 @@ export function getChartData (element) {
} }
} }
export function getHtml (options) { export function getHtml(options) {
const chartId = nanoid() const chartId = nanoid()
return ` return `
<script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script> <script src="https://cdn.plot.ly/plotly-latest.js" charset="UTF-8"></script>

View File

@@ -7,7 +7,7 @@ const hintsByCode = {
} }
export default { export default {
getResult (source, columns) { getResult(source, columns) {
const result = { const result = {
columns: columns || [] columns: columns || []
} }
@@ -52,7 +52,7 @@ export default {
return result return result
}, },
prepareForExport (resultSet) { prepareForExport(resultSet) {
const columns = resultSet.columns const columns = resultSet.columns
const rowCount = resultSet.values[columns[0]].length const rowCount = resultSet.values[columns[0]].length
const result = { const result = {
@@ -61,13 +61,15 @@ export default {
} }
for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) { for (let rowNumber = 0; rowNumber < rowCount; rowNumber++) {
result.data.push(columns.map(column => resultSet.values[column][rowNumber])) result.data.push(
columns.map(column => resultSet.values[column][rowNumber])
)
} }
return result return result
}, },
parse (file, config = {}) { parse(file, config = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const defaultConfig = { const defaultConfig = {
delimiter: '', // auto-detect delimiter: '', // auto-detect
@@ -122,7 +124,7 @@ export default {
}) })
}, },
serialize (resultSet) { serialize(resultSet) {
return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' }) return Papa.unparse(this.prepareForExport(resultSet), { delimiter: '\t' })
} }
} }

View File

@@ -1,10 +1,15 @@
import initSqlJs from 'sql.js/dist/sql-wasm.js' import initSqlJs from 'sql.js'
import dbUtils from './_statements' import dbUtils from './_statements'
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
let SQL = null let SQL = null
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule }) const sqlModuleReady = initSqlJs({
locateFile: () => wasmUrl
}).then(sqlModule => {
SQL = sqlModule
})
function _getDataSourcesFromSqlResult (sqlResult) { function _getDataSourcesFromSqlResult(sqlResult) {
if (!sqlResult) { if (!sqlResult) {
return {} return {}
} }
@@ -16,31 +21,30 @@ function _getDataSourcesFromSqlResult (sqlResult) {
} }
export default class Sql { export default class Sql {
constructor () { constructor() {
this.db = null this.db = null
} }
static build () { static build() {
return sqlModuleReady return sqlModuleReady.then(() => {
.then(() => { return new Sql()
return new Sql() })
})
} }
createDb (buffer) { createDb(buffer) {
if (this.db != null) this.db.close() if (this.db != null) this.db.close()
this.db = new SQL.Database(buffer) this.db = new SQL.Database(buffer)
return this.db return this.db
} }
open (buffer) { open(buffer) {
this.createDb(buffer && new Uint8Array(buffer)) this.createDb(buffer && new Uint8Array(buffer))
return { return {
ready: true ready: true
} }
} }
exec (sql, params) { exec(sql, params) {
if (this.db === null) { if (this.db === null) {
this.createDb() this.createDb()
} }
@@ -56,7 +60,7 @@ export default class Sql {
}) })
} }
import (tabName, data, progressCounterId, progressCallback, chunkSize = 1500) { import(tabName, data, progressCounterId, progressCallback, chunkSize = 1500) {
if (this.db === null) { if (this.db === null) {
this.createDb() this.createDb()
} }
@@ -77,7 +81,10 @@ export default class Sql {
} }
this.db.exec('COMMIT') this.db.exec('COMMIT')
count++ count++
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId }) progressCallback({
progress: 100 * (count / chunksAmount),
id: progressCounterId
})
} }
return { return {
@@ -85,11 +92,11 @@ export default class Sql {
} }
} }
export () { export() {
return this.db.export() return this.db.export()
} }
close () { close() {
if (this.db) { if (this.db) {
this.db.close() this.db.close()
} }

View File

@@ -1,8 +1,10 @@
export default { export default {
* generateChunks (data, size) { *generateChunks(data, size) {
const matrix = Object.keys(data).map(col => data[col]) const matrix = Object.keys(data).map(col => data[col])
const [row] = matrix const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column])) const transposedMatrix = row.map((value, column) =>
matrix.map(row => row[column])
)
const count = Math.ceil(transposedMatrix.length / size) const count = Math.ceil(transposedMatrix.length / size)
@@ -13,13 +15,13 @@ export default {
} }
}, },
getInsertStmt (tabName, columns) { getInsertStmt(tabName, columns) {
const colList = `"${columns.join('", "')}"` const colList = `"${columns.join('", "')}"`
const params = columns.map(() => '?').join(', ') const params = columns.map(() => '?').join(', ')
return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});` return `INSERT INTO "${tabName}" (${colList}) VALUES (${params});`
}, },
getCreateStatement (tabName, data) { getCreateStatement(tabName, data) {
let result = `CREATE table "${tabName}"(` let result = `CREATE table "${tabName}"(`
for (const col in data) { for (const col in data) {
// Get the first row of values to determine types // Get the first row of values to determine types
@@ -38,7 +40,8 @@ export default {
type = 'TEXT' type = 'TEXT'
break break
} }
default: type = 'TEXT' default:
type = 'TEXT'
} }
result += `"${col}" ${type}, ` result += `"${col}" ${type}, `
} }

View File

@@ -3,7 +3,7 @@ import Sql from './_sql'
const sqlReady = Sql.build() const sqlReady = Sql.build()
function processMsg (sql) { function processMsg(sql) {
const data = this const data = this
switch (data && data.action) { switch (data && data.action) {
case 'open': case 'open':
@@ -28,14 +28,12 @@ function processMsg (sql) {
} }
} }
function onError (error) { function onError(error) {
return { return {
error: error.message error: error.message
} }
} }
registerPromiseWorker(data => { registerPromiseWorker(data => {
return sqlReady return sqlReady.then(processMsg.bind(data)).catch(onError)
.then(processMsg.bind(data))
.catch(onError)
}) })

View File

@@ -1,7 +1,4 @@
import fu from '@/lib/utils/fileIo' 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: // Use promise-worker in order to turn worker into the promise based one:
// https://github.com/nolanlawson/promise-worker // https://github.com/nolanlawson/promise-worker
@@ -9,8 +6,10 @@ import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
function getNewDatabase () { function getNewDatabase() {
const worker = new Worker() const worker = new Worker(new URL('./_worker.js', import.meta.url), {
type: 'module'
})
return new Database(worker) return new Database(worker)
} }
@@ -20,7 +19,7 @@ export default {
let progressCounterIds = 0 let progressCounterIds = 0
class Database { class Database {
constructor (worker) { constructor(worker) {
this.dbName = null this.dbName = null
this.schema = null this.schema = null
this.worker = worker this.worker = worker
@@ -31,29 +30,33 @@ class Database {
const progress = e.data.progress const progress = e.data.progress
if (progress !== undefined) { if (progress !== undefined) {
const id = e.data.id const id = e.data.id
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', { this.importProgresses[id].dispatchEvent(
detail: progress new CustomEvent('progress', {
})) detail: progress
})
)
} }
}) })
} }
shutDown () { shutDown() {
this.worker.terminate() this.worker.terminate()
} }
createProgressCounter (callback) { createProgressCounter(callback) {
const id = progressCounterIds++ const id = progressCounterIds++
this.importProgresses[id] = new EventTarget() this.importProgresses[id] = new EventTarget()
this.importProgresses[id].addEventListener('progress', e => { callback(e.detail) }) this.importProgresses[id].addEventListener('progress', e => {
callback(e.detail)
})
return id return id
} }
deleteProgressCounter (id) { deleteProgressCounter(id) {
delete this.importProgresses[id] delete this.importProgresses[id]
} }
async addTableFromCsv (tabName, data, progressCounterId) { async addTableFromCsv(tabName, data, progressCounterId) {
const result = await this.pw.postMessage({ const result = await this.pw.postMessage({
action: 'import', action: 'import',
data, data,
@@ -68,9 +71,12 @@ class Database {
this.refreshSchema() this.refreshSchema()
} }
async loadDb (file) { async loadDb(file) {
const fileContent = file ? await fu.readAsArrayBuffer(file) : null const fileContent = file ? await fu.readAsArrayBuffer(file) : null
const res = await this.pw.postMessage({ action: 'open', buffer: fileContent }) const res = await this.pw.postMessage({
action: 'open',
buffer: fileContent
})
if (res.error) { if (res.error) {
throw new Error(res.error) throw new Error(res.error)
@@ -85,7 +91,7 @@ class Database {
}) })
} }
async refreshSchema () { async refreshSchema() {
const getSchemaSql = ` const getSchemaSql = `
WITH columns as ( WITH columns as (
SELECT SELECT
@@ -103,7 +109,7 @@ class Database {
this.schema = JSON.parse(result.values.objects[0]) this.schema = JSON.parse(result.values.objects[0])
} }
async execute (commands) { async execute(commands) {
await this.pw.postMessage({ action: 'reopen' }) await this.pw.postMessage({ action: 'reopen' })
const results = await this.pw.postMessage({ action: 'exec', sql: commands }) const results = await this.pw.postMessage({ action: 'exec', sql: commands })
@@ -114,7 +120,7 @@ class Database {
return results[results.length - 1] return results[results.length - 1]
} }
async export (fileName) { async export(fileName) {
const data = await this.pw.postMessage({ action: 'export' }) const data = await this.pw.postMessage({ action: 'export' })
if (data.error) { if (data.error) {
@@ -124,13 +130,15 @@ class Database {
events.send('database.export', data.byteLength, { to: 'sqlite' }) events.send('database.export', data.byteLength, { to: 'sqlite' })
} }
async validateTableName (name) { async validateTableName(name) {
if (name.startsWith('sqlite_')) { if (name.startsWith('sqlite_')) {
throw new Error("Table name can't start with sqlite_") throw new Error("Table name can't start with sqlite_")
} }
if (/[^\w]/.test(name)) { if (/[^\w]/.test(name)) {
throw new Error('Table name can contain only letters, digits and underscores') throw new Error(
'Table name can contain only letters, digits and underscores'
)
} }
if (/^(\d)/.test(name)) { if (/^(\d)/.test(name)) {
@@ -140,7 +148,7 @@ class Database {
await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`) await this.execute(`BEGIN; CREATE TABLE "${name}"(id); ROLLBACK;`)
} }
sanitizeTableName (tabName) { sanitizeTableName(tabName) {
return tabName return tabName
.replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _ .replace(/[^\w]/g, '_') // replace everything that is not letter, digit or _ with _
.replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit .replace(/^(\d)/, '_$1') // add _ at beginning if starts with digit

8
src/lib/eventBus.js Normal file
View File

@@ -0,0 +1,8 @@
import emitter from 'tiny-emitter/instance'
export default {
$on: (...args) => emitter.on(...args),
$once: (...args) => emitter.once(...args),
$off: (...args) => emitter.off(...args),
$emit: (...args) => emitter.emit(...args)
}

View File

@@ -1,5 +1,5 @@
export default { export default {
_migrate (installedVersion, inquiries) { _migrate(installedVersion, inquiries) {
if (installedVersion === 1) { if (installedVersion === 1) {
inquiries.forEach(inquire => { inquiries.forEach(inquire => {
inquire.viewType = 'chart' inquire.viewType = 'chart'

View File

@@ -7,7 +7,7 @@ const migrate = migration._migrate
export default { export default {
version: 2, version: 2,
getStoredInquiries () { getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem('myInquiries')) let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
if (!myInquiries) { if (!myInquiries) {
const oldInquiries = localStorage.getItem('myQueries') const oldInquiries = localStorage.getItem('myQueries')
@@ -22,7 +22,7 @@ export default {
return (myInquiries && myInquiries.inquiries) || [] return (myInquiries && myInquiries.inquiries) || []
}, },
duplicateInquiry (baseInquiry) { duplicateInquiry(baseInquiry) {
const newInquiry = JSON.parse(JSON.stringify(baseInquiry)) const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
newInquiry.name = newInquiry.name + ' Copy' newInquiry.name = newInquiry.name + ' Copy'
newInquiry.id = nanoid() newInquiry.id = nanoid()
@@ -32,53 +32,28 @@ export default {
return newInquiry return newInquiry
}, },
isTabNeedName (inquiryTab) { isTabNeedName(inquiryTab) {
return inquiryTab.isPredefined || !inquiryTab.name return inquiryTab.isPredefined || !inquiryTab.name
}, },
save (inquiryTab, newName) { updateStorage(inquiries) {
const value = { localStorage.setItem(
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id, 'myInquiries',
query: inquiryTab.query, JSON.stringify({ version: this.version, inquiries })
viewType: inquiryTab.dataView.mode, )
viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.name
}
// 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) { serialiseInquiries(inquiryList) {
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
},
serialiseInquiries (inquiryList) {
const preparedData = JSON.parse(JSON.stringify(inquiryList)) const preparedData = JSON.parse(JSON.stringify(inquiryList))
preparedData.forEach(inquiry => delete inquiry.isPredefined) preparedData.forEach(inquiry => delete inquiry.isPredefined)
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4) return JSON.stringify(
{ version: this.version, inquiries: preparedData },
null,
4
)
}, },
deserialiseInquiries (str) { deserialiseInquiries(str) {
const inquiries = JSON.parse(str) const inquiries = JSON.parse(str)
let inquiryList = [] let inquiryList = []
if (!inquiries.version) { if (!inquiries.version) {
@@ -91,7 +66,9 @@ export default {
// Generate new ids if they are the same as existing inquiries // Generate new ids if they are the same as existing inquiries
inquiryList.forEach(inquiry => { inquiryList.forEach(inquiry => {
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id) const allInquiriesIds = this.getStoredInquiries().map(
inquiry => inquiry.id
)
if (allInquiriesIds.includes(inquiry.id)) { if (allInquiriesIds.includes(inquiry.id)) {
inquiry.id = nanoid() inquiry.id = nanoid()
} }
@@ -100,24 +77,23 @@ export default {
return inquiryList return inquiryList
}, },
importInquiries () { importInquiries() {
return fu.importFile() return fu.importFile().then(str => {
.then(str => { const inquires = this.deserialiseInquiries(str)
const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length) events.send('inquiry.import', inquires.length)
return inquires return inquires
}) })
}, },
export (inquiryList, fileName) { export(inquiryList, fileName) {
const jsonStr = this.serialiseInquiries(inquiryList) const jsonStr = this.serialiseInquiries(inquiryList)
fu.exportToFile(jsonStr, fileName) fu.exportToFile(jsonStr, fileName)
events.send('inquiry.export', inquiryList.length) events.send('inquiry.export', inquiryList.length)
}, },
async readPredefinedInquiries () { async readPredefinedInquiries() {
const res = await fu.readFile('./inquiries.json') const res = await fu.readFile('./inquiries.json')
const data = await res.json() const data = await res.json()

View File

@@ -3,12 +3,14 @@ import time from '@/lib/utils/time'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
export default class Tab { export default class Tab {
constructor (state, inquiry = {}) { constructor(state, inquiry = {}) {
this.id = inquiry.id || nanoid() this.id = inquiry.id || nanoid()
this.name = inquiry.id ? inquiry.name : null this.name = inquiry.id ? inquiry.name : null
this.tempName = inquiry.name || (state.untitledLastIndex this.tempName =
? `Untitled ${state.untitledLastIndex}` inquiry.name ||
: 'Untitled') (state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.query = inquiry.query this.query = inquiry.query
this.viewOptions = inquiry.viewOptions || undefined this.viewOptions = inquiry.viewOptions || undefined
this.isPredefined = inquiry.isPredefined this.isPredefined = inquiry.isPredefined
@@ -28,7 +30,7 @@ export default class Tab {
this.state = state this.state = state
} }
async execute () { async execute() {
this.isGettingResults = true this.isGettingResults = true
this.result = null this.result = null
this.error = null this.error = null
@@ -39,7 +41,8 @@ export default class Tab {
this.time = time.getPeriod(start, new Date()) this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) { if (this.result && this.result.values) {
events.send('resultset.create', events.send(
'resultset.create',
this.result.values[this.result.columns[0]].length this.result.values[this.result.columns[0]].length
) )
} }

View File

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

View File

@@ -1,5 +1,5 @@
export default { export default {
send (name, value, labels) { send(name, value, labels) {
const event = new CustomEvent('sqliteviz-app-event', { const event = new CustomEvent('sqliteviz-app-event', {
detail: { detail: {
name, name,

View File

@@ -1,22 +1,22 @@
export default { export default {
isJSON (file) { isJSON(file) {
return file && file.type === 'application/json' return file && file.type === 'application/json'
}, },
isNDJSON (file) { isNDJSON(file) {
return file && file.name.endsWith('.ndjson') return file && file.name.endsWith('.ndjson')
}, },
isDatabase (file) { isDatabase(file) {
const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3'] const dbTypes = ['application/vnd.sqlite3', 'application/x-sqlite3']
return file.type return file.type
? dbTypes.includes(file.type) ? dbTypes.includes(file.type)
: /\.(db|sqlite(3)?)+$/.test(file.name) : /\.(db|sqlite(3)?)+$/.test(file.name)
}, },
getFileName (file) { getFileName(file) {
return file.name.replace(/\.[^.]+$/, '') return file.name.replace(/\.[^.]+$/, '')
}, },
downloadFromUrl (url, fileName) { downloadFromUrl(url, fileName) {
// Create downloader // Create downloader
const downloader = document.createElement('a') const downloader = document.createElement('a')
downloader.href = url downloader.href = url
@@ -29,7 +29,7 @@ export default {
URL.revokeObjectURL(url) URL.revokeObjectURL(url)
}, },
async exportToFile (str, fileName, type = 'octet/stream') { async exportToFile(str, fileName, type = 'octet/stream') {
const blob = new Blob([str], { type }) const blob = new Blob([str], { type })
const url = URL.createObjectURL(blob) const url = URL.createObjectURL(blob)
this.downloadFromUrl(url, fileName) this.downloadFromUrl(url, fileName)
@@ -40,7 +40,7 @@ export default {
* it will be an unsettled promise. But it's grabbed by * it will be an unsettled promise. But it's grabbed by
* the garbage collector (tested with FinalizationRegistry). * the garbage collector (tested with FinalizationRegistry).
*/ */
getFileFromUser (type) { getFileFromUser(type) {
return new Promise(resolve => { return new Promise(resolve => {
const uploader = document.createElement('input') const uploader = document.createElement('input')
@@ -56,14 +56,13 @@ export default {
}) })
}, },
importFile () { importFile() {
return this.getFileFromUser('.json') return this.getFileFromUser('.json').then(file => {
.then(file => { return this.getFileContent(file)
return this.getFileContent(file) })
})
}, },
getFileContent (file) { getFileContent(file) {
const reader = new FileReader() const reader = new FileReader()
return new Promise(resolve => { return new Promise(resolve => {
reader.onload = e => resolve(e.target.result) reader.onload = e => resolve(e.target.result)
@@ -71,11 +70,11 @@ export default {
}) })
}, },
readFile (path) { readFile(path) {
return fetch(path) return fetch(path)
}, },
readAsArrayBuffer (file) { readAsArrayBuffer(file) {
const fileReader = new FileReader() const fileReader = new FileReader()
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {

View File

@@ -1,11 +1,11 @@
export default { export default {
getPeriod (start, end) { getPeriod(start, end) {
const diff = end.getTime() - start.getTime() const diff = end.getTime() - start.getTime()
const seconds = diff / 1000 const seconds = diff / 1000
return seconds.toFixed(3) + 's' return seconds.toFixed(3) + 's'
}, },
debounce (func, ms) { debounce(func, ms) {
let timeout let timeout
return function () { return function () {
clearTimeout(timeout) clearTimeout(timeout)
@@ -13,9 +13,11 @@ export default {
} }
}, },
sleep (ms) { sleep(ms) {
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { resolve() }, ms) setTimeout(() => {
resolve()
}, ms)
}) })
} }
} }

View File

@@ -1,9 +1,8 @@
import Vue from 'vue' import { createApp } from 'vue'
import App from '@/App.vue' import App from '@/App.vue'
import router from '@/router' import router from '@/router'
import store from '@/store' import store from '@/store'
import { VuePlugin } from 'vuera' import { createVfm, VueFinalModal, useVfm } from 'vue-final-modal'
import VModal from 'vue-js-modal'
import '@/assets/styles/variables.css' import '@/assets/styles/variables.css'
import '@/assets/styles/buttons.css' import '@/assets/styles/buttons.css'
@@ -11,20 +10,23 @@ import '@/assets/styles/tables.css'
import '@/assets/styles/dialogs.css' import '@/assets/styles/dialogs.css'
import '@/assets/styles/tooltips.css' import '@/assets/styles/tooltips.css'
import '@/assets/styles/messages.css' import '@/assets/styles/messages.css'
import 'vue-multiselect/dist/vue-multiselect.min.css' import 'vue-multiselect/dist/vue-multiselect.css'
import '@/assets/styles/multiselect.css' import '@/assets/styles/multiselect.css'
import 'vue-final-modal/style.css'
if (!['localhost', '127.0.0.1'].includes(location.hostname)) { if (!['localhost', '127.0.0.1'].includes(location.hostname)) {
import('./registerServiceWorker') // eslint-disable-line no-unused-expressions import('./registerServiceWorker') // eslint-disable-line no-unused-expressions
} }
Vue.use(VuePlugin) const app = createApp(App)
Vue.use(VModal) .use(router)
.use(store)
.use(createVfm())
.component('modal', VueFinalModal)
Vue.config.productionTip = false const vfm = useVfm()
app.config.globalProperties.$modal = {
new Vue({ show: modalId => vfm.open(modalId),
router, hide: modalId => vfm.close(modalId)
store, }
render: h => h(App) app.mount('#app')
}).$mount('#app')

View File

@@ -1,7 +1,7 @@
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
let refresh = false let refresh = false
function invokeServiceWorkerUpdateFlow (registration) { function invokeServiceWorkerUpdateFlow(registration) {
const agree = confirm('New version of the app is available. Refresh now?') const agree = confirm('New version of the app is available. Refresh now?')
if (agree) { if (agree) {
if (registration.waiting) { if (registration.waiting) {
@@ -14,7 +14,8 @@ function invokeServiceWorkerUpdateFlow (registration) {
if ('serviceWorker' in navigator) { if ('serviceWorker' in navigator) {
window.addEventListener('load', async () => { window.addEventListener('load', async () => {
const registration = await navigator.serviceWorker.register('service-worker.js') const registration =
await navigator.serviceWorker.register('service-worker.js')
// ensure the case when the updatefound event was missed is also handled // ensure the case when the updatefound event was missed is also handled
// by re-invoking the prompt when there's a waiting Service Worker // by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) { if (registration.waiting) {

View File

@@ -1,15 +1,12 @@
import Vue from 'vue' import { createRouter, createWebHashHistory } from 'vue-router'
import VueRouter from 'vue-router' import Workspace from '@/views/MainView/Workspace'
import Workspace from '@/views/Main/Workspace' import Inquiries from '@/views/MainView/Inquiries'
import Inquiries from '@/views/Main/Inquiries'
import Welcome from '@/views/Welcome' import Welcome from '@/views/Welcome'
import Main from '@/views/Main' import MainView from '@/views/MainView'
import LoadView from '@/views/LoadView' import LoadView from '@/views/LoadView'
import store from '@/store' import store from '@/store'
import database from '@/lib/database' import database from '@/lib/database'
Vue.use(VueRouter)
const routes = [ const routes = [
{ {
path: '/', path: '/',
@@ -18,8 +15,8 @@ const routes = [
}, },
{ {
path: '/', path: '/',
name: 'Main', name: 'MainView',
component: Main, component: MainView,
children: [ children: [
{ {
path: '/workspace', path: '/workspace',
@@ -40,7 +37,8 @@ const routes = [
} }
] ]
const router = new VueRouter({ const router = createRouter({
history: createWebHashHistory(),
routes routes
}) })

View File

@@ -1,7 +1,8 @@
import Tab from '@/lib/tab' import Tab from '@/lib/tab'
import { nanoid } from 'nanoid'
export default { export default {
async addTab ({ state }, inquiry = {}) { async addTab({ state }, inquiry = {}) {
// add new tab only if it was not already opened // add new tab only if it was not already opened
if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) { if (!state.tabs.some(openedTab => openedTab.id === inquiry.id)) {
const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry))) const tab = new Tab(state, JSON.parse(JSON.stringify(inquiry)))
@@ -13,5 +14,72 @@ export default {
} }
return inquiry.id return inquiry.id
},
async saveInquiry({ state }, { inquiryTab, newName }) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
viewType: inquiryTab.dataView.mode,
viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.name
}
// Get inquiries from local storage
const myInquiries = state.inquiries
// Set createdAt
if (newName) {
value.createdAt = new Date()
} else {
var inquiryIndex = myInquiries.findIndex(
oldInquiry => oldInquiry.id === inquiryTab.id
)
value.createdAt = myInquiries[inquiryIndex].createdAt
}
// Insert in inquiries list
if (newName) {
myInquiries.push(value)
} else {
myInquiries.splice(inquiryIndex, 1, value)
}
return value
},
addInquiry({ state }, newInquiry) {
state.inquiries.push(newInquiry)
},
deleteInquiries({ state, commit }, inquiryIdSet) {
state.inquiries = state.inquiries.filter(
inquiry => !inquiryIdSet.has(inquiry.id)
)
// Close deleted inquiries if it was opened
const tabs = state.tabs
let i = tabs.length - 1
while (i > -1) {
if (inquiryIdSet.has(tabs[i].id)) {
commit('deleteTab', tabs[i])
}
i--
}
},
renameInquiry({ state, commit }, { inquiryId, newName }) {
const renamingInquiry = state.inquiries.find(
inquiry => inquiry.id === inquiryId
)
renamingInquiry.name = newName
// update tab, if renamed inquiry is opened
const tab = state.tabs.find(tab => tab.id === renamingInquiry.id)
if (tab) {
commit('updateTab', {
tab,
newValues: {
name: newName
}
})
}
} }
} }

View File

@@ -1,12 +1,9 @@
import Vue from 'vue' import { createStore } from 'vuex'
import Vuex from 'vuex'
import state from '@/store/state' import state from '@/store/state'
import mutations from '@/store/mutations' import mutations from '@/store/mutations'
import actions from '@/store/actions' import actions from '@/store/actions'
Vue.use(Vuex) export default createStore({
export default new Vuex.Store({
state, state,
mutations, mutations,
actions actions

View File

@@ -1,12 +1,12 @@
export default { export default {
setDb (state, db) { setDb(state, db) {
if (state.db) { if (state.db) {
state.db.shutDown() state.db.shutDown()
} }
state.db = db state.db = db
}, },
updateTab (state, { tab, newValues }) { updateTab(state, { tab, newValues }) {
const { name, id, query, viewType, viewOptions, isSaved } = newValues const { name, id, query, viewType, viewOptions, isSaved } = newValues
const oldId = tab.id const oldId = tab.id
@@ -14,19 +14,31 @@ export default {
state.currentTabId = id state.currentTabId = id
} }
if (id) { tab.id = id } if (id) {
if (name) { tab.name = name } tab.id = id
if (query) { tab.query = query } }
if (viewType) { tab.viewType = viewType } if (name) {
if (viewOptions) { tab.viewOptions = viewOptions } tab.name = name
if (isSaved !== undefined) { tab.isSaved = isSaved } }
if (query) {
tab.query = query
}
if (viewType) {
tab.viewType = viewType
}
if (viewOptions) {
tab.viewOptions = viewOptions
}
if (isSaved !== undefined) {
tab.isSaved = isSaved
}
if (isSaved) { if (isSaved) {
// Saved inquiry is not predefined // Saved inquiry is not predefined
delete tab.isPredefined delete tab.isPredefined
} }
}, },
deleteTab (state, tab) { deleteTab(state, tab) {
const index = state.tabs.indexOf(tab) const index = state.tabs.indexOf(tab)
// If closing tab is the current opened // If closing tab is the current opened
if (tab.id === state.currentTabId) { if (tab.id === state.currentTabId) {
@@ -44,21 +56,29 @@ export default {
} }
state.tabs.splice(index, 1) state.tabs.splice(index, 1)
}, },
setCurrentTabId (state, id) { setCurrentTabId(state, id) {
try { try {
state.currentTabId = id state.currentTabId = id
state.currentTab = state.tabs.find(tab => tab.id === id) state.currentTab = state.tabs.find(tab => tab.id === id)
} catch (e) { } catch (e) {
console.error('Can\'t open a tab id:' + id) console.error("Can't open a tab id:" + id)
} }
}, },
updatePredefinedInquiries (state, inquiries) { updatePredefinedInquiries(state, inquiries) {
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries] state.predefinedInquiries = Array.isArray(inquiries)
? inquiries
: [inquiries]
}, },
setLoadingPredefinedInquiries (state, value) { setLoadingPredefinedInquiries(state, value) {
state.loadingPredefinedInquiries = value state.loadingPredefinedInquiries = value
}, },
setPredefinedInquiriesLoaded (state, value) { setPredefinedInquiriesLoaded(state, value) {
state.predefinedInquiriesLoaded = value state.predefinedInquiriesLoaded = value
},
setInquiries(state, value) {
state.inquiries = value
},
setIsWorkspaceVisible(state, value) {
state.isWorkspaceVisible = value
} }
} }

View File

@@ -3,8 +3,10 @@ export default {
currentTab: null, currentTab: null,
currentTabId: null, currentTabId: null,
untitledLastIndex: 0, untitledLastIndex: 0,
inquiries: [],
predefinedInquiries: [], predefinedInquiries: [],
loadingPredefinedInquiries: false, loadingPredefinedInquiries: false,
predefinedInquiriesLoaded: false, predefinedInquiriesLoaded: false,
db: null db: null,
isWorkspaceVisible: false
} }

View File

@@ -1,5 +1,5 @@
export default { export default {
data () { data() {
return { return {
tooltipStyle: { tooltipStyle: {
visibility: 'hidden' visibility: 'hidden'
@@ -7,13 +7,15 @@ export default {
} }
}, },
computed: { computed: {
tooltipElement () { tooltipElement() {
return this.$refs.tooltip return this.$refs.tooltip
} }
}, },
methods: { methods: {
showTooltip (e, tooltipPosition) { showTooltip(e, tooltipPosition) {
const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right'] const position = tooltipPosition
? tooltipPosition.split('-')
: ['top', 'right']
const offset = 12 const offset = 12
if (position[0] === 'top') { if (position[0] === 'top') {
@@ -25,12 +27,13 @@ export default {
if (position[1] === 'right') { if (position[1] === 'right') {
this.tooltipStyle.left = e.clientX + offset + 'px' this.tooltipStyle.left = e.clientX + offset + 'px'
} else { } else {
this.tooltipStyle.left = e.clientX - offset - this.tooltipElement.offsetWidth + 'px' this.tooltipStyle.left =
e.clientX - offset - this.tooltipElement.offsetWidth + 'px'
} }
this.tooltipStyle.visibility = 'visible' this.tooltipStyle.visibility = 'visible'
}, },
hideTooltip () { hideTooltip() {
this.tooltipStyle.visibility = 'hidden' this.tooltipStyle.visibility = 'hidden'
} }
} }

View File

@@ -1,17 +1,15 @@
<template> <template>
<div> <div>
<logs <logs id="logs" :messages="messages" />
id="logs" <button
:messages="messages" v-if="hasErrors"
/> id="open-workspace-btn"
<button class="secondary"
v-if="hasErrors" @click="$router.push('/workspace?hide_schema=1')"
id="open-workspace-btn" >
class="secondary" Open workspace
@click="$router.push('/workspace?hide_schema=1')"> </button>
Open workspace </div>
</button>
</div>
</template> </template>
<script> <script>
@@ -25,7 +23,7 @@ export default {
components: { components: {
Logs Logs
}, },
data () { data() {
return { return {
newDb: null, newDb: null,
messages: [], messages: [],
@@ -34,11 +32,11 @@ export default {
} }
}, },
computed: { computed: {
hasErrors () { hasErrors() {
return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error' return this.dataMsg.type === 'error' || this.inquiryMsg.type === 'error'
} }
}, },
async created () { async created() {
const { const {
data_url: dataUrl, data_url: dataUrl,
data_format: dataFormat, data_format: dataFormat,
@@ -65,7 +63,7 @@ export default {
} }
}, },
methods: { methods: {
async loadData (dataUrl, dataFormat) { async loadData(dataUrl, dataFormat) {
this.newDb = database.getNewDatabase() this.newDb = database.getNewDatabase()
if (dataUrl) { if (dataUrl) {
this.dataMsg = { this.dataMsg = {
@@ -95,7 +93,7 @@ export default {
} }
this.$store.commit('setDb', this.newDb) this.$store.commit('setDb', this.newDb)
}, },
async getSqliteDb (dataUrl) { async getSqliteDb(dataUrl) {
try { try {
const filename = new URL(dataUrl).pathname.split('/').pop() const filename = new URL(dataUrl).pathname.split('/').pop()
const res = await fu.readFile(dataUrl) const res = await fu.readFile(dataUrl)
@@ -114,7 +112,7 @@ export default {
this.dataMsg.type = 'error' this.dataMsg.type = 'error'
} }
}, },
async loadInquiries (inquiryUrl, inquiryIds = []) { async loadInquiries(inquiryUrl, inquiryIds = []) {
if (!inquiryUrl) { if (!inquiryUrl) {
return [] return []
} }
@@ -148,7 +146,7 @@ export default {
// Loading indicator is not needed anymore // Loading indicator is not needed anymore
clearTimeout(loadingInquiriesIndicator) clearTimeout(loadingInquiriesIndicator)
}, },
async openInquiries (inquiries, maximize) { async openInquiries(inquiries, maximize) {
let tabToOpen = null let tabToOpen = null
const layout = maximize ? this.getLayout(maximize) : undefined const layout = maximize ? this.getLayout(maximize) : undefined
for (const inquiry of inquiries) { for (const inquiry of inquiries) {
@@ -167,7 +165,7 @@ export default {
this.$store.state.currentTab.execute() this.$store.state.currentTab.execute()
}, },
getLayout (panelToMaximize) { getLayout(panelToMaximize) {
if (panelToMaximize === 'dataView') { if (panelToMaximize === 'dataView') {
return { return {
sqlEditor: 'hidden', sqlEditor: 'hidden',
@@ -190,7 +188,6 @@ export default {
#logs { #logs {
margin: 8px auto; margin: 8px auto;
max-width: 800px; max-width: 800px;
} }
#open-workspace-btn { #open-workspace-btn {

View File

@@ -1,159 +0,0 @@
<template>
<div v-show="visible" class="chart-container" ref="chartContainer">
<div class="warning chart-warning" v-show="!dataSources && 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, modeBarButtonsToRemove: ['toImage'] }"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
@onUpdate="update"
@onRender="onRender"
:useResizeHandler="true"
:debug="true"
:advancedTraceTypeSelector="true"
class="chart"
ref="plotlyEditor"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
/>
</div>
</template>
<script>
import plotly from 'plotly.js'
import 'react-chart-editor/lib/react-chart-editor.min.css'
import PlotlyEditor from 'react-chart-editor'
import chartHelper from '@/lib/chartHelper'
import dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'Chart',
props: [
'dataSources', 'initOptions',
'importToPngEnabled', 'importToSvgEnabled',
'forPivot'
],
components: {
PlotlyEditor
},
data () {
return {
plotly: plotly,
state: this.initOptions || {
data: [],
layout: {},
frames: []
},
visible: true,
resizeObserver: null
}
},
computed: {
dataSourceOptions () {
return chartHelper.getOptionsFromDataSources(this.dataSources)
}
},
created () {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() => this.state && this.state.data && this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
(value) => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
})
},
{ deep: true }
)
this.$emit('update:importToSvgEnabled', true)
},
mounted () {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer)
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.chartContainer)
},
watch: {
dataSources () {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) {
dereference(this.state.data, this.dataSources)
}
}
},
methods: {
handleResize () {
this.visible = false
this.$nextTick(() => {
this.visible = true
})
},
onRender (data, layout, frames) {
// TODO: check changes and enable Save button if needed
},
update (data, layout, frames) {
this.state = { data, layout, frames }
this.$emit('update')
},
getOptionsForSave () {
return chartHelper.getOptionsForSave(this.state, this.dataSources)
},
async saveAsPng () {
const url = await this.prepareCopy()
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'chart')
},
async saveAsSvg () {
const url = await this.prepareCopy('svg')
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml () {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy (type = 'png') {
return await chartHelper.getImageDataUrl(this.$refs.plotlyEditor.$el, type)
}
}
}
</script>
<style scoped>
.chart-container {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px;
}
>>> .editor_controls .sidebar__item:before {
width: 0;
}
</style>

View File

@@ -1,69 +0,0 @@
<template>
<div class="record-navigator">
<icon-button
:disabled="value === 0"
tooltip="First row"
tooltip-position="top-left"
class="first"
@click="$emit('input', 0)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="value === 0"
tooltip="Previous row"
tooltip-position="top-left"
class="prev"
@click="$emit('input', value - 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="value === total - 1"
tooltip="Next row"
tooltip-position="top-left"
class="next"
@click="$emit('input', value + 1)"
>
<arrow-icon :disabled="false" />
</icon-button>
<icon-button
:disabled="value === total - 1"
tooltip="Last row"
tooltip-position="top-left"
class="last"
@click="$emit('input', total - 1)"
>
<edge-arrow-icon :disabled="false" />
</icon-button>
</div>
</template>
<script>
import IconButton from '@/components/IconButton'
import ArrowIcon from '@/components/svg/arrow'
import EdgeArrowIcon from '@/components/svg/edgeArrow'
export default {
components: {
IconButton,
ArrowIcon,
EdgeArrowIcon
},
props: {
value: Number,
total: Number
}
}
</script>
<style scoped>
.record-navigator {
display: flex;
}
.record-navigator .next,
.record-navigator .last {
transform: rotate(180deg);
}
</style>

View File

@@ -2,21 +2,21 @@
<div id="app-info-container"> <div id="app-info-container">
<img <img
id="app-info-icon" id="app-info-icon"
:src="require('@/assets/images/info.svg')" src="~@/assets/images/info.svg"
@click="$modal.show('app-info')" @click="$modal.show('app-info')"
/> />
<modal name="app-info" classes="dialog" height="auto" width="400px"> <modal modalId="app-info" class="dialog" contentClass="app-info-modal">
<div class="dialog-header"> <div class="dialog-header">
App info App info
<close-icon @click="$modal.hide('app-info')"/> <close-icon @click="$modal.hide('app-info')" />
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<div v-for="(item, index) in info" :key="index" class="info-item"> <div v-for="(item, index) in info" :key="index" class="info-item">
{{item.name}} {{ item.name }}
<div class="divider"/> <div class="divider" />
<div class="options"> <div class="options">
<div v-for="(opt, index) in item.info" :key="index"> <div v-for="(opt, optIndex) in item.info" :key="optIndex">
{{opt}} {{ opt }}
</div> </div>
</div> </div>
</div> </div>
@@ -27,22 +27,23 @@
<script> <script>
import CloseIcon from '@/components/svg/close' import CloseIcon from '@/components/svg/close'
import { version } from '../../../package.json'
export default { export default {
name: 'AppDiagnosticInfo', name: 'AppDiagnosticInfo',
components: { CloseIcon }, components: { CloseIcon },
data () { data() {
return { return {
info: [ info: [
{ {
name: 'sqliteviz version', name: 'sqliteviz version',
info: [require('../../../package.json').version] info: [version]
} }
] ]
} }
}, },
async created () { async created() {
const state = this.$store.state const state = this.$store.state
let result = (await state.db.execute('select sqlite_version()')).values let result = (await state.db.execute('select sqlite_version()')).values
this.info.push({ this.info.push({
@@ -59,6 +60,12 @@ export default {
} }
</script> </script>
<style>
.app-info-modal {
width: 400px;
}
</style>
<style scoped> <style scoped>
#app-info-icon { #app-info-icon {
cursor: pointer; cursor: pointer;
@@ -83,7 +90,7 @@ export default {
} }
.info-item { .info-item {
margin-bottom: 32px; margin-bottom: 32px;
font-size: 14px; font-size: 14px;
} }
.info-item:last-child { .info-item:last-child {
margin-bottom: 0; margin-bottom: 0;

View File

@@ -1,43 +1,55 @@
<template> <template>
<div id="my-inquiries-container"> <div id="my-inquiries-container">
<div id="start-guide" v-if="allInquiries.length === 0"> <div v-if="allInquiries.length === 0" id="start-guide">
You don't have saved inquiries so far. You don't have saved inquiries so far.
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span> <span class="link" @click="emitCreateTabEvent">Create</span>
the one from scratch or the one from scratch or
<span @click="importInquiries" class="link">import</span> from a file. <span class="link" @click="importInquiries">import</span> from a file.
</div> </div>
<div <div
id="loading-predefined-status"
v-if="$store.state.loadingPredefinedInquiries" v-if="$store.state.loadingPredefinedInquiries"
id="loading-predefined-status"
> >
<loading-indicator/> <loading-indicator />
Loading predefined inquiries... Loading predefined inquiries...
</div> </div>
<div id="my-inquiries-content" ref="my-inquiries-content" v-show="allInquiries.length > 0"> <div
v-show="allInquiries.length > 0"
id="my-inquiries-content"
ref="my-inquiries-content"
>
<div id="my-inquiries-toolbar"> <div id="my-inquiries-toolbar">
<div id="toolbar-buttons"> <div id="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries"> <button
id="toolbar-btns-import"
class="toolbar"
@click="importInquiries"
>
Import Import
</button> </button>
<button <button
v-show="selectedInquiriesCount > 0"
id="toolbar-btns-export" id="toolbar-btns-export"
class="toolbar" class="toolbar"
v-show="selectedInquiriesCount > 0"
@click="exportSelectedInquiries()" @click="exportSelectedInquiries()"
> >
Export Export
</button> </button>
<button <button
v-show="selectedNotPredefinedCount > 0"
id="toolbar-btns-delete" id="toolbar-btns-delete"
class="toolbar" class="toolbar"
v-show="selectedNotPredefinedCount > 0"
@click="showDeleteDialog(selectedInquiriesIds)" @click="showDeleteDialog(selectedInquiriesIds)"
> >
Delete Delete
</button> </button>
</div> </div>
<div id="toolbar-search"> <div id="toolbar-search">
<text-field placeholder="Search inquiry by name" width="300px" v-model="filter"/> <text-field
v-model="filter"
placeholder="Search inquiry by name"
width="300px"
/>
</div> </div>
</div> </div>
@@ -46,30 +58,36 @@
</div> </div>
<div v-show="showedInquiries.length > 0" class="rounded-bg"> <div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container"> <div class="header-container">
<div> <div>
<div class="fixed-header" ref="name-th"> <div ref="name-th" class="fixed-header">
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/> <check-box
<div class="name-th">Name</div> ref="mainCheckBox"
</div> theme="light"
<div class="fixed-header"> @click="toggleSelectAll"
Created at />
<div class="name-th">Name</div>
</div>
<div class="fixed-header">Created at</div>
</div> </div>
</div> </div>
</div> <div
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }"> class="table-container"
<table ref="table" class="sqliteviz-table"> :style="{ 'max-height': `${maxTableHeight}px` }"
<tbody> >
<tr <table ref="table" class="sqliteviz-table">
v-for="(inquiry, index) in showedInquiries" <tbody>
:key="inquiry.id" <tr
@click="openInquiry(index)" v-for="(inquiry, index) in showedInquiries"
> :key="inquiry.id"
<td ref="name-td"> @click="openInquiry(index)"
<div class="cell-data"> >
<td ref="name-td">
<div class="cell-data">
<check-box <check-box
ref="rowCheckBox" ref="rowCheckBox"
:init="selectAll || selectedInquiriesIds.has(inquiry.id)" :init="selectAll || selectedInquiriesIds.has(inquiry.id)"
data-test="rowCheckBox"
@click="toggleRow($event, inquiry.id)" @click="toggleRow($event, inquiry.id)"
/> />
<div class="name">{{ inquiry.name }}</div> <div class="name">{{ inquiry.name }}</div>
@@ -80,80 +98,89 @@
@mouseleave="hideTooltip" @mouseleave="hideTooltip"
> >
Predefined Predefined
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span
Predefined inquiries come from the server. ref="tooltip"
These inquiries cant be deleted or renamed. class="icon-tooltip"
:style="tooltipStyle"
>
Predefined inquiries come from the server. These
inquiries cant be deleted or renamed.
</span> </span>
</div> </div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">{{ inquiry.createdAt | date }}</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)"/>
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog((new Set()).add(inquiry.id))"
/>
</div> </div>
</div> </td>
</td> <td>
</tr> <div class="second-column">
</tbody> <div class="date-container">
</table> {{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@click="showRenameDialog(inquiry.id)"
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
tooltip="Export inquiry to file"
tooltipPosition="top-left"
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div> </div>
</div> </div>
<!--Rename Inquiry dialog -->
<modal modalId="rename" class="dialog" contentStyle="width: 560px;">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')" />
</div>
<div class="dialog-body">
<text-field
v-model="newName"
label="New inquiry name"
:errorMsg="errorMsg"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal modalId="delete" class="dialog" contentStyle="width: 480px;">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')" />
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div
v-show="selectedInquiriesCount > selectedNotPredefinedCount"
id="note"
>
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteInquiry">Delete</button>
</div>
</modal>
</div> </div>
<!--Rename Inquiry dialog -->
<modal name="rename" classes="dialog" height="auto">
<div class="dialog-header">
Rename inquiry
<close-icon @click="$modal.hide('rename')"/>
</div>
<div class="dialog-body">
<text-field
label="New inquiry name"
:error-msg="errorMsg"
v-model="newName"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('rename')">Cancel</button>
<button class="primary" @click="renameInquiry">Rename</button>
</div>
</modal>
<!--Delete Inquiry dialog -->
<modal name="delete" classes="dialog" height="auto">
<div class="dialog-header">
Delete {{ deleteGroup ? 'inquiries' : 'inquiry' }}
<close-icon @click="$modal.hide('delete')"/>
</div>
<div class="dialog-body">
{{ deleteDialogMsg }}
<div v-show="selectedInquiriesCount > selectedNotPredefinedCount" id="note">
<img :src="require('@/assets/images/info.svg')">
Note: Predefined inquiries you've selected won't be deleted
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="$modal.hide('delete')">Cancel</button>
<button class="primary" @click="deleteInquiry">Delete</button>
</div>
</modal>
</div>
</template> </template>
<script> <script>
@@ -167,6 +194,7 @@ import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator' import LoadingIndicator from '@/components/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin' import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus'
export default { export default {
name: 'Inquiries', name: 'Inquiries',
@@ -181,9 +209,8 @@ export default {
LoadingIndicator LoadingIndicator
}, },
mixins: [tooltipMixin], mixins: [tooltipMixin],
data () { data() {
return { return {
inquiries: [],
filter: null, filter: null,
newName: null, newName: null,
processedInquiryId: null, processedInquiryId: null,
@@ -198,69 +225,84 @@ export default {
} }
}, },
computed: { computed: {
predefinedInquiries () { inquiries() {
return this.$store.state.inquiries
},
predefinedInquiries() {
return this.$store.state.predefinedInquiries.map(inquiry => { return this.$store.state.predefinedInquiries.map(inquiry => {
inquiry.isPredefined = true inquiry.isPredefined = true
return inquiry return inquiry
}) })
}, },
predefinedInquiriesIds () { predefinedInquiriesIds() {
return new Set(this.predefinedInquiries.map(inquiry => inquiry.id)) return new Set(this.predefinedInquiries.map(inquiry => inquiry.id))
}, },
showedInquiries () { showedInquiries() {
let showedInquiries = this.allInquiries let showedInquiries = this.allInquiries
if (this.filter) { if (this.filter) {
showedInquiries = showedInquiries.filter( showedInquiries = showedInquiries.filter(
inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0 inquiry =>
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
) )
} }
return showedInquiries return showedInquiries
}, },
allInquiries () { allInquiries() {
return this.predefinedInquiries.concat(this.inquiries) return this.predefinedInquiries.concat(this.inquiries)
}, },
processedInquiryIndex () { processedInquiryIndex() {
return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId) return this.inquiries.findIndex(
inquiry => inquiry.id === this.processedInquiryId
)
}, },
deleteDialogMsg () { deleteDialogMsg() {
if (!this.deleteGroup && ( if (
this.processedInquiryIndex === null || !this.deleteGroup &&
(this.processedInquiryIndex === null ||
this.processedInquiryIndex < 0 || this.processedInquiryIndex < 0 ||
this.processedInquiryIndex > this.inquiries.length this.processedInquiryIndex > this.inquiries.length)
)) { ) {
return '' return ''
} }
const deleteItem = this.deleteGroup const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1 ? `${this.selectedNotPredefinedCount} ${
? 'inquiries' this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
: 'inquiry'}` }`
: `"${this.inquiries[this.processedInquiryIndex].name}"` : `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?` return `Are you sure you want to delete ${deleteItem}?`
} }
}, },
watch: { watch: {
showedInquiries () { showedInquiries: {
this.selectedInquiriesIds = new Set(this.showedInquiries handler() {
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id)) this.selectedInquiriesIds = new Set(
.map(inquiry => inquiry.id) this.showedInquiries
) .filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
this.selectedInquiriesCount = this.selectedInquiriesIds.size .map(inquiry => inquiry.id)
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds] )
.filter(id => !this.predefinedInquiriesIds.has(id))).length this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) { if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
this.$refs.mainCheckBox.checked = false if (this.$refs.mainCheckBox) {
this.selectAll = false this.$refs.mainCheckBox.checked = false
} }
this.selectAll = false
}
},
deep: true
} }
}, },
async created () { async created() {
this.inquiries = storedInquiries.getStoredInquiries() const loadingPredefinedInquiries =
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded const predefinedInquiriesLoaded =
this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) { if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try { try {
this.$store.commit('setLoadingPredefinedInquiries', true) this.$store.commit('setLoadingPredefinedInquiries', true)
@@ -273,7 +315,7 @@ export default {
this.$store.commit('setLoadingPredefinedInquiries', false) this.$store.commit('setLoadingPredefinedInquiries', false)
} }
}, },
mounted () { mounted() {
this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight) this.resizeObserver = new ResizeObserver(this.calcMaxTableHeight)
this.resizeObserver.observe(this.$refs['my-inquiries-content']) this.resizeObserver.observe(this.$refs['my-inquiries-content'])
@@ -282,12 +324,15 @@ export default {
this.calcNameWidth() this.calcNameWidth()
this.calcMaxTableHeight() this.calcMaxTableHeight()
}, },
beforeDestroy () { beforeUnmount() {
this.resizeObserver.unobserve(this.$refs['my-inquiries-content']) this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table) this.tableResizeObserver.unobserve(this.$refs.table)
}, },
filters: { methods: {
date (value) { emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
createdAtFormatted(value) {
if (!value) { if (!value) {
return '' return ''
} }
@@ -297,131 +342,109 @@ export default {
hour: '2-digit', hour: '2-digit',
minute: '2-digit' minute: '2-digit'
} }
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' + return (
new Date(value).toLocaleTimeString('en-GB', timeOptions) new Date(value).toLocaleDateString('en-GB', dateOptions) +
} ' ' +
}, new Date(value).toLocaleTimeString('en-GB', timeOptions)
methods: { )
calcNameWidth () { },
const nameWidth = this.$refs['name-td'] && this.$refs['name-td'][0] calcNameWidth() {
? this.$refs['name-td'][0].getBoundingClientRect().width const nameWidth =
: 0 this.$refs['name-td'] && this.$refs['name-td'][0]
? this.$refs['name-td'][0].getBoundingClientRect().width
: 0
this.$refs['name-th'].style = `width: ${nameWidth}px` this.$refs['name-th'].style = `width: ${nameWidth}px`
}, },
calcMaxTableHeight () { calcMaxTableHeight() {
const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200 const freeSpace = this.$refs['my-inquiries-content'].offsetHeight - 200
this.maxTableHeight = freeSpace - (freeSpace % 40) + 1 this.maxTableHeight = freeSpace - (freeSpace % 40) + 1
}, },
openInquiry (index) { openInquiry(index) {
const tab = this.showedInquiries[index] const tab = this.showedInquiries[index]
this.$store.dispatch('addTab', tab).then(id => { setTimeout(() => {
this.$store.commit('setCurrentTabId', id) this.$store.dispatch('addTab', tab).then(id => {
this.$router.push('/workspace') this.$store.commit('setCurrentTabId', id)
this.$router.push('/workspace')
})
}) })
}, },
showRenameDialog (id) { showRenameDialog(id) {
this.errorMsg = null this.errorMsg = null
this.processedInquiryId = id this.processedInquiryId = id
this.newName = this.inquiries[this.processedInquiryIndex].name this.newName = this.inquiries[this.processedInquiryIndex].name
this.$modal.show('rename') this.$modal.show('rename')
}, },
renameInquiry () { renameInquiry() {
if (!this.newName) { if (!this.newName) {
this.errorMsg = "Inquiry name can't be empty" this.errorMsg = "Inquiry name can't be empty"
return return
} }
const processedInquiry = this.inquiries[this.processedInquiryIndex] this.$store.dispatch('renameInquiry', {
processedInquiry.name = this.newName inquiryId: this.processedInquiryId,
this.$set(this.inquiries, this.processedInquiryIndex, processedInquiry) newName: this.newName
})
// update inquiries in local storage
storedInquiries.updateStorage(this.inquiries)
// update tab, if renamed inquiry is opened
const tab = this.$store.state.tabs
.find(tab => tab.id === processedInquiry.id)
if (tab) {
this.$store.commit('updateTab', {
tab,
newValues: {
name: this.newName
}
})
}
// hide dialog // hide dialog
this.$modal.hide('rename') this.$modal.hide('rename')
}, },
duplicateInquiry (index) { duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index]) const newInquiry = storedInquiries.duplicateInquiry(
this.inquiries.push(newInquiry) this.showedInquiries[index]
storedInquiries.updateStorage(this.inquiries) )
this.$store.dispatch('addInquiry', newInquiry)
}, },
showDeleteDialog (idsSet) { showDeleteDialog(idsSet) {
this.deleteGroup = idsSet.size > 1 this.deleteGroup = idsSet.size > 1
if (!this.deleteGroup) { if (!this.deleteGroup) {
this.processedInquiryId = idsSet.values().next().value this.processedInquiryId = idsSet.values().next().value
} }
this.$modal.show('delete') this.$modal.show('delete')
}, },
deleteInquiry () { deleteInquiry() {
this.$modal.hide('delete') this.$modal.hide('delete')
if (!this.deleteGroup) { if (!this.deleteGroup) {
this.inquiries.splice(this.processedInquiryIndex, 1) this.$store.dispatch(
'deleteInquiries',
// Close deleted inquiry tab if it was opened new Set().add(this.processedInquiryId)
const tab = this.$store.state.tabs )
.find(tab => tab.id === this.processedInquiryId)
if (tab) {
this.$store.commit('deleteTab', tab)
}
// Clear checkbox // Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) { if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
this.selectedInquiriesIds.delete(this.processedInquiryId) this.selectedInquiriesIds.delete(this.processedInquiryId)
} }
} else { } else {
this.inquiries = this.inquiries.filter( this.$store.dispatch('deleteInquiries', this.selectedInquiriesIds)
inquiry => !this.selectedInquiriesIds.has(inquiry.id)
)
// Close deleted inquiries if it was opened
const tabs = this.$store.state.tabs
let i = tabs.length - 1
while (i > -1) {
if (this.selectedInquiriesIds.has(tabs[i].id)) {
this.$store.commit('deleteTab', tabs[i])
}
i--
}
// Clear checkboxes // Clear checkboxes
this.selectedInquiriesIds.clear() this.selectedInquiriesIds.clear()
} }
this.selectedInquiriesCount = this.selectedInquiriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
storedInquiries.updateStorage(this.inquiries)
}, },
exportToFile (inquiryList, fileName) { exportToFile(inquiryList, fileName) {
storedInquiries.export(inquiryList, fileName) storedInquiries.export(inquiryList, fileName)
}, },
exportSelectedInquiries () { exportSelectedInquiries() {
const inquiryList = this.allInquiries.filter( const inquiryList = this.allInquiries.filter(inquiry =>
inquiry => this.selectedInquiriesIds.has(inquiry.id) this.selectedInquiriesIds.has(inquiry.id)
) )
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json') this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
}, },
importInquiries () { importInquiries() {
storedInquiries.importInquiries() storedInquiries.importInquiries().then(importedInquiries => {
.then(importedInquiries => { this.$store.commit(
this.inquiries = this.inquiries.concat(importedInquiries) 'setInquiries',
storedInquiries.updateStorage(this.inquiries) this.inquiries.concat(importedInquiries)
}) )
})
}, },
toggleSelectAll (checked) { toggleSelectAll(checked) {
this.selectAll = checked this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked }) this.$refs.rowCheckBox.forEach(item => {
item.checked = checked
})
this.selectedInquiriesIds = checked this.selectedInquiriesIds = checked
? new Set(this.showedInquiries.map(inquiry => inquiry.id)) ? new Set(this.showedInquiries.map(inquiry => inquiry.id))
@@ -429,12 +452,13 @@ export default {
this.selectedInquiriesCount = this.selectedInquiriesIds.size this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id))) ? [...this.selectedInquiriesIds].filter(
.length id => !this.predefinedInquiriesIds.has(id)
).length
: 0 : 0
}, },
toggleRow (checked, id) { toggleRow(checked, id) {
const isPredefined = this.predefinedInquiriesIds.has(id) const isPredefined = this.predefinedInquiriesIds.has(id)
if (checked) { if (checked) {
this.selectedInquiriesIds.add(id) this.selectedInquiriesIds.add(id)

View File

@@ -20,7 +20,7 @@
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Duplicate inquiry Duplicate inquiry
</span> </span>
</span> </span>
@@ -32,8 +32,9 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'CopyIcon', name: 'CopyIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -18,7 +18,7 @@
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Delete inquiry Delete inquiry
</span> </span>
</span> </span>
@@ -30,8 +30,9 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'DeleteIcon', name: 'DeleteIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -18,7 +18,7 @@
fill="#A2B1C6" fill="#A2B1C6"
/> />
</svg> </svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip"> <span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Rename inquiry Rename inquiry
</span> </span>
</span> </span>
@@ -30,8 +30,9 @@ import tooltipMixin from '@/tooltipMixin'
export default { export default {
name: 'RenameIcon', name: 'RenameIcon',
mixins: [tooltipMixin], mixins: [tooltipMixin],
emits: ['click'],
methods: { methods: {
onClick () { onClick() {
this.hideTooltip() this.hideTooltip()
this.$emit('click') this.$emit('click')
} }

View File

@@ -2,7 +2,7 @@
<nav> <nav>
<div id="nav-links"> <div id="nav-links">
<a href="https://sqliteviz.com"> <a href="https://sqliteviz.com">
<img :src="require('@/assets/images/logo_simple.svg')"> <img src="~@/assets/images/logo_simple.svg" />
</a> </a>
<router-link to="/workspace">Workspace</router-link> <router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link> <router-link to="/inquiries">Inquiries</router-link>
@@ -10,40 +10,36 @@
</div> </div>
<div id="nav-buttons"> <div id="nav-buttons">
<button <button
id="save-btn"
v-show="currentInquiry && $route.path === '/workspace'" v-show="currentInquiry && $route.path === '/workspace'"
id="save-btn"
class="primary" class="primary"
:disabled="isSaved" :disabled="isSaved"
@click="checkInquiryBeforeSave" @click="checkInquiryBeforeSave"
> >
Save Save
</button> </button>
<button <button id="create-btn" class="primary" @click="createNewInquiry">
id="create-btn"
class="primary"
@click="createNewInquiry"
>
Create Create
</button> </button>
<app-diagnostic-info /> <app-diagnostic-info />
</div> </div>
<!--Save Inquiry dialog --> <!--Save Inquiry dialog -->
<modal name="save" classes="dialog" height="auto"> <modal modalId="save" class="dialog" contentStyle="width: 560px;">
<div class="dialog-header"> <div class="dialog-header">
Save inquiry Save inquiry
<close-icon @click="cancelSave"/> <close-icon @click="cancelSave" />
</div> </div>
<div class="dialog-body"> <div class="dialog-body">
<div v-show="isPredefined" id="save-note"> <div v-show="isPredefined" id="save-note">
<img :src="require('@/assets/images/info.svg')"> <img src="~@/assets/images/info.svg" />
Note: Predefined inquiries can't be edited. Note: Predefined inquiries can't be edited. That's why your
That's why your modifications will be saved as a new inquiry. Enter the name for it. modifications will be saved as a new inquiry. Enter the name for it.
</div> </div>
<text-field <text-field
label="Inquiry name"
:error-msg="errorMsg"
v-model="name" v-model="name"
label="Inquiry name"
:errorMsg="errorMsg"
width="100%" width="100%"
/> />
</div> </div>
@@ -61,6 +57,7 @@ import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries' import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo' import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events' import events from '@/lib/utils/events'
import eventBus from '@/lib/eventBus'
export default { export default {
name: 'MainMenu', name: 'MainMenu',
@@ -69,36 +66,39 @@ export default {
CloseIcon, CloseIcon,
AppDiagnosticInfo AppDiagnosticInfo
}, },
data () { data() {
return { return {
name: '', name: '',
errorMsg: null errorMsg: null
} }
}, },
computed: { computed: {
currentInquiry () { currentInquiry() {
return this.$store.state.currentTab return this.$store.state.currentTab
}, },
isSaved () { isSaved() {
return this.currentInquiry && this.currentInquiry.isSaved return this.currentInquiry && this.currentInquiry.isSaved
}, },
isPredefined () { isPredefined() {
return this.currentInquiry && this.currentInquiry.isPredefined return this.currentInquiry && this.currentInquiry.isPredefined
}, },
runDisabled () { runDisabled() {
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query) return (
this.currentInquiry &&
(!this.$store.state.db || !this.currentInquiry.query)
)
} }
}, },
created () { created() {
this.$root.$on('createNewInquiry', this.createNewInquiry) eventBus.$on('createNewInquiry', this.createNewInquiry)
this.$root.$on('saveInquiry', this.checkInquiryBeforeSave) eventBus.$on('saveInquiry', this.checkInquiryBeforeSave)
document.addEventListener('keydown', this._keyListener) document.addEventListener('keydown', this._keyListener)
}, },
beforeDestroy () { beforeUnmount() {
document.removeEventListener('keydown', this._keyListener) document.removeEventListener('keydown', this._keyListener)
}, },
methods: { methods: {
createNewInquiry () { createNewInquiry() {
this.$store.dispatch('addTab').then(id => { this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id) this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/workspace') { if (this.$route.path !== '/workspace') {
@@ -108,11 +108,11 @@ export default {
events.send('inquiry.create', null, { auto: false }) events.send('inquiry.create', null, { auto: false })
}, },
cancelSave () { cancelSave() {
this.$modal.hide('save') this.$modal.hide('save')
this.$root.$off('inquirySaved') eventBus.$off('inquirySaved')
}, },
checkInquiryBeforeSave () { checkInquiryBeforeSave() {
this.errorMsg = null this.errorMsg = null
this.name = '' this.name = ''
@@ -122,17 +122,20 @@ export default {
this.saveInquiry() this.saveInquiry()
} }
}, },
saveInquiry () { async saveInquiry() {
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry) const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) { if (isNeedName && !this.name) {
this.errorMsg = 'Inquiry name can\'t be empty' this.errorMsg = "Inquiry name can't be empty"
return return
} }
const dataSet = this.currentInquiry.result const dataSet = this.currentInquiry.result
const tabView = this.currentInquiry.view const tabView = this.currentInquiry.view
// Save inquiry // Save inquiry
const value = storedInquiries.save(this.currentInquiry, this.name) const value = await this.$store.dispatch('saveInquiry', {
inquiryTab: this.currentInquiry,
newName: this.name
})
// Update tab in store // Update tab in store
this.$store.commit('updateTab', { this.$store.commit('updateTab', {
@@ -161,10 +164,10 @@ export default {
this.$modal.hide('save') this.$modal.hide('save')
// Signal about saving // Signal about saving
this.$root.$emit('inquirySaved') eventBus.$emit('inquirySaved')
events.send('inquiry.save') events.send('inquiry.save')
}, },
_keyListener (e) { _keyListener(e) {
if (this.$route.path === '/workspace') { if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter // Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) { if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {

View File

@@ -1,7 +1,7 @@
<template> <template>
<div> <div>
<div @click="colVisible = !colVisible" class="table-name"> <div class="table-name" @click="colVisible = !colVisible">
<tree-chevron :expanded="colVisible"/> <tree-chevron :expanded="colVisible" />
{{ name }} {{ name }}
</div> </div>
<div v-show="colVisible" class="columns"> <div v-show="colVisible" class="columns">
@@ -19,8 +19,11 @@ import TreeChevron from '@/components/svg/treeChevron'
export default { export default {
name: 'TableDescription', name: 'TableDescription',
components: { TreeChevron }, components: { TreeChevron },
props: ['name', 'columns'], props: {
data () { name: String,
columns: Array
},
data() {
return { return {
colVisible: false colVisible: false
} }
@@ -29,7 +32,8 @@ export default {
</script> </script>
<style scoped> <style scoped>
.table-name, .column { .table-name,
.column {
margin-top: 11px; margin-top: 11px;
} }

View File

@@ -1,16 +1,16 @@
<template> <template>
<div id="schema-container"> <div id="schema-container">
<div id="schema-filter"> <div id="schema-filter">
<text-field placeholder="Search table" width="100%" v-model="filter"/> <text-field v-model="filter" placeholder="Search table" width="100%" />
</div> </div>
<div id="db"> <div id="db">
<div @click="schemaVisible = !schemaVisible" class="db-name"> <div class="db-name" @click="schemaVisible = !schemaVisible">
<tree-chevron v-show="schema.length > 0" :expanded="schemaVisible"/> <tree-chevron v-show="schema.length > 0" :expanded="schemaVisible" />
{{ dbName }} {{ dbName }}
</div> </div>
<db-uploader id="db-edit" type="small" /> <db-uploader id="db-edit" type="small" />
<export-icon tooltip="Export database" @click="exportToFile"/> <export-icon tooltip="Export database" @click="exportToFile" />
<add-table-icon @click="addCsvJson"/> <add-table-icon @click="addCsvJson" />
</div> </div>
<div v-show="schemaVisible" class="schema"> <div v-show="schemaVisible" class="schema">
<table-description <table-description
@@ -26,7 +26,7 @@
ref="addCsvJson" ref="addCsvJson"
:file="file" :file="file"
:db="$store.state.db" :db="$store.state.db"
dialog-name="addCsvJson" dialogName="addCsvJson"
/> />
</div> </div>
</template> </template>
@@ -53,7 +53,7 @@ export default {
AddTableIcon, AddTableIcon,
CsvJsonImport CsvJsonImport
}, },
data () { data() {
return { return {
schemaVisible: true, schemaVisible: true,
filter: null, filter: null,
@@ -61,7 +61,7 @@ export default {
} }
}, },
computed: { computed: {
schema () { schema() {
if (!this.$store.state.db.schema) { if (!this.$store.state.db.schema) {
return [] return []
} }
@@ -69,18 +69,19 @@ export default {
return !this.filter return !this.filter
? this.$store.state.db.schema ? this.$store.state.db.schema
: this.$store.state.db.schema.filter( : this.$store.state.db.schema.filter(
table => table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1 table =>
) table.name.toUpperCase().indexOf(this.filter.toUpperCase()) !== -1
)
}, },
dbName () { dbName() {
return this.$store.state.db.dbName return this.$store.state.db.dbName
} }
}, },
methods: { methods: {
exportToFile () { exportToFile() {
this.$store.state.db.export(`${this.dbName}.sqlite`) this.$store.state.db.export(`${this.dbName}.sqlite`)
}, },
async addCsvJson () { async addCsvJson() {
this.file = await fIo.getFileFromUser('.csv,.json,.ndjson') this.file = await fIo.getFileFromUser('.csv,.json,.ndjson')
await this.$nextTick() await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson const csvJsonImportModal = this.$refs.addCsvJson
@@ -119,7 +120,8 @@ export default {
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0)); background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
z-index: 2; z-index: 2;
} }
.schema, .db-name { .schema,
.db-name {
color: var(--color-text-base); color: var(--color-text-base);
font-size: 13px; font-size: 13px;
white-space: nowrap; white-space: nowrap;
@@ -141,7 +143,7 @@ export default {
} }
.db-name:hover .chevron-icon path, .db-name:hover .chevron-icon path,
>>> .table-name:hover .chevron-icon path { :deep(.table-name:hover .chevron-icon path) {
fill: var(--color-gray-dark); fill: var(--color-gray-dark);
} }
</style> </style>

View File

@@ -0,0 +1,193 @@
<template>
<div ref="chartContainer" class="chart-container">
<div v-show="!dataSources" class="warning chart-warning">
There is no data to build a chart. Run your SQL query and make sure the
result is not empty.
</div>
<div
class="chart"
:style="{ height: !dataSources ? 'calc(100% - 40px)' : '100%' }"
>
<PlotlyEditor
ref="plotlyEditor"
:data="state.data"
:layout="state.layout"
:frames="state.frames"
:config="config"
:dataSources="dataSources"
:dataSourceOptions="dataSourceOptions"
:plotly="plotly"
:useResizeHandler="useResizeHandler"
:debug="true"
:advancedTraceTypeSelector="true"
@update="update"
@render="onRender"
/>
</div>
</div>
</template>
<script>
import { applyPureReactInVue } from 'veaury'
import plotly from 'plotly.js'
import 'react-chart-editor/lib/react-chart-editor.css'
import ReactPlotlyEditorWithPlotRef from '@/lib/ReactPlotlyEditorWithPlotRef.jsx'
import chartHelper from '@/lib/chartHelper'
import * as dereference from 'react-chart-editor/lib/lib/dereference'
import fIo from '@/lib/utils/fileIo'
import events from '@/lib/utils/events'
export default {
name: 'Chart',
components: {
PlotlyEditor: applyPureReactInVue(ReactPlotlyEditorWithPlotRef)
},
props: {
dataSources: Object,
initOptions: Object,
importToPngEnabled: Boolean,
importToSvgEnabled: Boolean,
forPivot: Boolean
},
emits: ['update:importToSvgEnabled', 'update', 'loadingImageCompleted'],
data() {
return {
plotly,
state: this.initOptions || {
data: [],
layout: { autosize: true },
frames: []
},
config: {
editable: true,
displaylogo: false,
modeBarButtonsToRemove: ['toImage']
},
resizeObserver: null,
useResizeHandler: this.$store.state.isWorkspaceVisible
}
},
computed: {
dataSourceOptions() {
return chartHelper.getOptionsFromDataSources(this.dataSources)
}
},
watch: {
dataSources() {
// we need to update state.data in order to update the graph
// https://github.com/plotly/react-chart-editor/issues/948
if (this.dataSources) {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
}
},
created() {
// https://github.com/plotly/plotly.js/issues/4555
plotly.setPlotConfig({
notifyOnLogging: 1
})
this.$watch(
() =>
this.state &&
this.state.data &&
this.state.data
.map(trace => `${trace.type}${trace.mode ? '-' + trace.mode : ''}`)
.join(','),
value => {
events.send('viz_plotly.render', null, {
type: value,
pivot: !!this.forPivot
})
},
{ deep: true }
)
this.$emit('update:importToSvgEnabled', true)
},
mounted() {
this.resizeObserver = new ResizeObserver(this.handleResize)
this.resizeObserver.observe(this.$refs.chartContainer)
if (this.dataSources) {
dereference.default(this.state.data, this.dataSources)
this.updatePlotly()
}
},
activated() {
this.useResizeHandler = true
},
deactivated() {
this.useResizeHandler = false
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.chartContainer)
},
methods: {
async handleResize() {
this.updatePlotly()
},
onRender() {
// TODO: check changes and enable Save button if needed
},
update(data, layout, frames) {
this.state = { data, layout, frames }
this.$emit('update')
},
updatePlotly() {
const plotComponent = this.$refs.plotlyEditor.plotComponentRef.current
plotComponent.updatePlotly(
true, // shouldInvokeResizeHandler
plotComponent.props.onUpdate, // figureCallbackFunction
false // shouldAttachUpdateEvents
)
},
getOptionsForSave() {
return chartHelper.getOptionsForSave(this.state, this.dataSources)
},
async saveAsPng() {
const url = await this.prepareCopy()
this.$emit('loadingImageCompleted')
fIo.downloadFromUrl(url, 'chart')
},
async saveAsSvg() {
const url = await this.prepareCopy('svg')
fIo.downloadFromUrl(url, 'chart')
},
saveAsHtml() {
fIo.exportToFile(
chartHelper.getHtml(this.state),
'chart.html',
'text/html'
)
},
async prepareCopy(type = 'png') {
return await chartHelper.getImageDataUrl(
this.$refs.plotlyEditor.$el,
type
)
}
}
}
</script>
<style scoped>
.chart-container {
height: 100%;
}
.chart-warning {
height: 40px;
line-height: 40px;
border-bottom: 1px solid var(--color-border);
box-sizing: border-box;
}
.chart {
min-height: 242px;
}
:deep(.editor_controls .sidebar__item:before) {
width: 0;
}
</style>

View File

@@ -1,11 +1,11 @@
<template> <template>
<div :class="['pivot-sort-btn', direction] " @click="changeSorting"> <div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ value.includes('key') ? 'key' : 'value' }} {{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon <sort-icon
class="sort-icon" class="sort-icon"
:horizontal="direction === 'col'" :horizontal="direction === 'col'"
:asc="value.includes('a_to_z')" :asc="modelValue.includes('a_to_z')"
/> />
</div> </div>
</template> </template>
@@ -14,18 +14,22 @@ import SortIcon from '@/components/svg/sort'
export default { export default {
name: 'PivotSortBtn', name: 'PivotSortBtn',
props: ['direction', 'value'],
components: { components: {
SortIcon SortIcon
}, },
props: {
direction: String,
modelValue: String
},
emits: ['update:modelValue'],
methods: { methods: {
changeSorting () { changeSorting() {
if (this.value === 'key_a_to_z') { if (this.modelValue === 'key_a_to_z') {
this.$emit('input', 'value_a_to_z') this.$emit('update:modelValue', 'value_a_to_z')
} else if (this.value === 'value_a_to_z') { } else if (this.modelValue === 'value_a_to_z') {
this.$emit('input', 'value_z_to_a') this.$emit('update:modelValue', 'value_z_to_a')
} else { } else {
this.$emit('input', 'key_a_to_z') this.$emit('update:modelValue', 'key_a_to_z')
} }
} }
} }
@@ -52,7 +56,7 @@ export default {
color: var(--color-text-active); color: var(--color-text-active);
border-color: var(--color-border-dark); border-color: var(--color-border-dark);
} }
.pivot-sort-btn:hover >>> .sort-icon path { .pivot-sort-btn:hover :deep(.sort-icon path) {
fill: var(--color-text-active); fill: var(--color-text-active);
} }

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