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

44 Commits

Author SHA1 Message Date
lana-k
9cf7d0e5dc #128 fix save and close 2025-10-09 22:49:54 +02:00
lana-k
0a8c09b58d #127 fix for new inquiry 2025-10-08 21:04:17 +02:00
lana-k
931cf380bc #127 tests 2025-10-08 19:39:56 +02:00
lana-k
f0f96ac663 tests 2025-10-05 20:59:34 +02:00
lana-k
45530cc9d6 add save as event 2025-10-05 14:27:50 +02:00
lana-k
6fbf75b601 fix tests 2025-10-03 22:13:33 +02:00
lana-k
d3fbf08569 #31 fix deleting inquiry 2025-09-29 21:17:36 +02:00
lana-k
be6a19a30f #127 fix copy to clipboard 2025-09-28 22:11:18 +02:00
lana-k
07d7a9d54b #31 handle concurrent saving 2025-09-27 21:59:32 +02:00
lana-k
cdd925b8af #16 save as 2025-09-27 17:01:50 +02:00
lana-k
12fa0749b1 Update package.json 2025-07-30 23:27:35 +02:00
saaj
75bf849823 Build SQLite 3.50.3 (#124)
* Build SQLite 3.50.3

* Update pivot_vtab, base in Dockerfile.test, fix test after SQLite 3.47

* Update CI image for tests
2025-07-30 23:26:22 +02:00
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
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
177 changed files with 18531 additions and 39089 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

@@ -14,10 +14,10 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
node-version: 18.x
- name: Update npm
run: npm install -g npm@8
run: npm install -g npm@10
- name: npm install and build
run: |
@@ -27,19 +27,19 @@ jobs:
- name: Create archives
run: |
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 .
- name: Create Release Notes
run: |
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:
GREN_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create release
uses: ncipollo/release-action@v1
with:
artifacts: "dist.zip,dist_map.zip"
artifacts: 'dist.zip,dist_map.zip'
token: ${{ secrets.GITHUB_TOKEN }}
bodyFile: "CHANGELOG.md"
bodyFile: 'CHANGELOG.md'

View File

@@ -11,21 +11,22 @@ on:
jobs:
test:
name: Run tests
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 16.x
node-version: 18.x
- name: Install browsers
run: |
export DEBIAN_FRONTEND=noninteractive
sudo add-apt-repository -y ppa:mozillateam/ppa
sudo apt-get update
sudo apt-get install -y chromium-browser firefox
sudo apt-get install -y chromium-browser firefox-esr
- name: Update npm
run: npm install -g npm@8
run: npm install -g npm@10
- name: Install the project
run: npm install

7
.prettierrc Normal file
View File

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

View File

@@ -3,7 +3,7 @@
# docker build -t sqliteviz/test -f Dockerfile.test .
#
FROM node:12.22-buster
FROM node:12.22-bullseye
RUN set -ex; \
apt update; \

View File

@@ -8,6 +8,7 @@ Sqliteviz is a single-page offline-first PWA for fully client-side visualisation
of SQLite databases, CSV, JSON or NDJSON files.
With sqliteviz you can:
- 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
- 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
## Quickstart
The latest release of sqliteviz is deployed on [sqliteviz.com/app][6].
## Wiki
For user documentation, check out sqliteviz [documentation][7].
## Motivation
It's a kind of middleground between [Plotly Falcon][1] and [Redash][2].
## Components
It is built on top of [react-chart-editor][3], [PivotTable.js][12], [sql.js][4] and [Vue-Codemirror][8] in [Vue.js][5]. CSV parsing is performed with [Papa Parse][9].
[1]: https://github.com/plotly/falcon

View File

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

View File

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

@@ -1,12 +1,15 @@
module.exports = function (config) {
const timeout = 15 * 60 * 1000
config.set({
frameworks: ['mocha'],
files: [
'suite.js',
{ pattern: 'node_modules/sql.js/dist/sql-wasm.wasm', served: true, included: false },
{
pattern: 'node_modules/sql.js/dist/sql-wasm.wasm',
served: true,
included: false
},
{ pattern: 'sample.csv', served: true, included: false }
],
@@ -15,7 +18,10 @@ module.exports = function (config) {
singleRun: true,
customLaunchers: {
ChromiumHeadlessNoSandbox: { base: 'ChromiumHeadless', flags: ['--no-sandbox'] }
ChromiumHeadlessNoSandbox: {
base: 'ChromiumHeadless',
flags: ['--no-sandbox']
}
},
browsers: ['ChromiumHeadlessNoSandbox', 'FirefoxHeadless'],
concurrency: 1,
@@ -47,6 +53,5 @@ module.exports = function (config) {
},
jsonToFileReporter: { outputPath: '.', fileName: 'suite-result.json' }
})
}

View File

@@ -4,7 +4,6 @@ import lodash from 'lodash'
import Papa from 'papaparse'
import useragent from 'ua-parser-js'
describe('SQLite build benchmark', function () {
let parsedCsv
let sqlModule
@@ -50,10 +49,8 @@ describe('SQLite build benchmark', function () {
suite.add('select', { initCount: 3, minSamples: 50, fn: benchmarkSelect })
await run(suite)
})
})
function importToTable(db, parsedCsv, chunkSize = 1024) {
const columnListString = parsedCsv.meta.fields.join(', ')
db.exec(`CREATE TABLE csv_import(${columnListString})`)
@@ -67,7 +64,6 @@ function importToTable (db, parsedCsv, chunkSize = 1024) {
})
}
class PromiseWrapper {
constructor() {
this.promise = new Promise((resolve, reject) => {
@@ -102,7 +98,6 @@ function chunkArray (arr, size) {
}, [])
}
function createSuite() {
// Combined workaround from:
// - https://github.com/bestiejs/benchmark.js/issues/106
@@ -124,10 +119,12 @@ function run (suite) {
console.info(String(event.target))
})
.on('complete', function () {
console.log(JSON.stringify({
console.log(
JSON.stringify({
browser: useragent(navigator.userAgent).browser,
result: this.filter('successful')
}))
})
)
suiteResult.resolve()
})
.on('error', function (event) {

View File

@@ -10,7 +10,7 @@ from pathlib import Path
from urllib import request
amalgamation_url = 'https://sqlite.org/2023/sqlite-amalgamation-3410000.zip'
amalgamation_url = 'https://sqlite.org/2025/sqlite-amalgamation-3500300.zip'
# Extension-functions
# ===================
@@ -22,15 +22,15 @@ contrib_functions_url = 'https://sqlite.org/contrib/download/extension-functions
extension_urls = (
# Miscellaneous extensions
# ========================
('https://sqlite.org/src/raw/8d79354f?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/dbfd8543?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/e212edb2?at=series.c', 'sqlite3_series_init'),
('https://sqlite.org/src/raw/5559daf1?at=closure.c', 'sqlite3_closure_init'),
('https://sqlite.org/src/raw/5bb2264c?at=uuid.c', 'sqlite3_uuid_init'),
('https://sqlite.org/src/raw/5853b0e5?at=regexp.c', 'sqlite3_regexp_init'),
('https://sqlite.org/src/raw/b9086e22?at=percentile.c', 'sqlite3_percentile_init'),
('https://sqlite.org/src/raw/09f967dc?at=decimal.c', 'sqlite3_decimal_init'),
('https://sqlite.org/src/raw/388e7f23?at=regexp.c', 'sqlite3_regexp_init'),
('https://sqlite.org/src/raw/72e05a21?at=percentile.c', 'sqlite3_percentile_init'),
('https://sqlite.org/src/raw/228d47e9?at=decimal.c', 'sqlite3_decimal_init'),
# Third-party extension
# =====================
('https://github.com/jakethaw/pivot_vtab/raw/9323ef93/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/jakethaw/pivot_vtab/raw/e7705f34/pivot_vtab.c', 'sqlite3_pivotvtab_init'),
('https://github.com/nalgeon/sqlean/raw/95e8d21a/src/pearson.c', 'sqlite3_pearson_init'),
# Third-party extension with own dependencies
# ===========================================

File diff suppressed because one or more lines are too long

Binary file not shown.

43499
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +1,21 @@
{
"name": "sqliteviz",
"version": "0.25.1",
"version": "0.27.0",
"license": "Apache-2.0",
"private": true,
"type": "module",
"scripts": {
"serve": "vue-cli-service serve",
"build": "NODE_OPTIONS=--max_old_space_size=4096 vue-cli-service build",
"test": "vue-cli-service karma",
"lint": "vue-cli-service lint"
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"test": "karma start karma.conf.cjs",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
"format": "prettier . --write"
},
"dependencies": {
"buffer": "^6.0.3",
"codemirror": "^5.65.18",
"codemirror-editor-vue3": "^2.8.0",
"core-js": "^3.6.5",
"dataurl-to-blob": "^0.0.1",
"html2canvas": "^1.1.4",
@@ -24,40 +29,51 @@
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"sql.js": "file:./lib/sql-js",
"vue": "^2.6.11",
"vue-codemirror": "^4.0.6",
"vue-js-modal": "^2.0.0-rc.6",
"vue-multiselect": "^2.1.6",
"vue-router": "^3.2.0",
"vue2-teleport": "^1.0.1",
"vuejs-paginate": "^2.1.0",
"vuera": "^0.2.7",
"vuex": "^3.4.0"
"tiny-emitter": "^2.1.0",
"veaury": "^2.5.1",
"vue": "^3.5.11",
"vue-final-modal": "^4.5.5",
"vue-multiselect": "^3.0.0-beta.3",
"vue-router": "^4.4.5",
"vuejs-paginate-next": "^1.0.2",
"vuex": "^4.1.0"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-plugin-router": "^4.4.0",
"@vue/cli-plugin-vuex": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vue/eslint-config-standard": "^5.1.2",
"@vue/test-utils": "^1.1.2",
"babel-eslint": "^10.1.0",
"@babel/core": "^7.25.7",
"@vitejs/plugin-vue": "^5.2.1",
"@vue/eslint-config-standard": "^8.0.1",
"@vue/test-utils": "^2.4.6",
"chai": "^4.1.2",
"chai-as-promised": "^7.1.1",
"eslint": "^6.7.2",
"chai-as-promised": "^8.0.1",
"eslint": "^8.57.1",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0",
"eslint-plugin-vue": "^6.2.2",
"eslint-plugin-vue": "^9.28.0",
"flush-promises": "^1.0.2",
"karma": "^3.1.4",
"karma-firefox-launcher": "^2.1.0",
"karma-webpack": "^4.0.2",
"vue-cli-plugin-ui-karma": "^0.2.5",
"vue-template-compiler": "^2.6.11",
"workbox-webpack-plugin": "^6.1.5",
"worker-loader": "^3.0.8"
"karma": "^6.4.4",
"karma-coverage": "^2.2.1",
"karma-coverage-istanbul-reporter": "^3.0.3",
"karma-firefox-launcher": "^2.1.3",
"karma-mocha": "^1.3.0",
"karma-spec-reporter": "^0.0.36",
"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

@@ -8,9 +8,6 @@
import storedInquiries from '@/lib/storedInquiries'
export default {
created () {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
},
computed: {
inquiries() {
return this.$store.state.inquiries
@@ -23,59 +20,68 @@ export default {
storedInquiries.updateStorage(this.inquiries)
}
}
},
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
addEventListener('storage', event => {
if (event.key === storedInquiries.myInquiriesKey) {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
}
})
}
}
</script>
<style>
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-Regular.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Regular.woff2');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-SemiBold.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBold.woff2');
font-weight: 600;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-Bold.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Bold.woff2');
font-weight: 700;
font-style: normal;
}
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-Italic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-Italic.woff2');
font-weight: 400;
font-style: italic;
}
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-SemiBoldItalic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-SemiBoldItalic.woff2');
font-weight: 600;
font-style: italic;
}
@font-face {
font-family: "Open Sans";
src: url("~@/assets/fonts/OpenSans-BoldItalic.woff2");
font-family: 'Open Sans';
src: url('@/assets/fonts/OpenSans-BoldItalic.woff2');
font-weight: 700;
font-style: italic;
}
#app,
.dialog,
input,
label,
button,
.plotly_editor * {
font-family: "Open Sans", Helvetica, Arial, sans-serif;
font-family: 'Open Sans', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

View File

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

View File

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

View File

@@ -62,14 +62,14 @@
margin: 2px;
}
.sqliteviz-select .multiselect__tag-icon:after {
content: url('~@/assets/images/delete-tag.svg');
content: url('@/assets/images/delete-tag.svg');
height: 14px;
width: 14px;
}
.sqliteviz-select .multiselect__tag-icon:focus:after,
.sqliteviz-select .multiselect__tag-icon:hover:after {
content: url('~@/assets/images/delete-tag-hover.svg');
content: url('@/assets/images/delete-tag-hover.svg');
}
.sqliteviz-select .multiselect__tag-icon:focus,
@@ -102,7 +102,7 @@
}
.sqliteviz-select .multiselect__select:before {
content: url('~@/assets/images/arrow.svg');
content: url('@/assets/images/arrow.svg');
border: none;
top: 0;
}
@@ -116,7 +116,7 @@
}
.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 {

View File

@@ -59,7 +59,8 @@ table.sqliteviz-table {
margin-top: -35px;
border-collapse: collapse;
}
.sqliteviz-table thead th, .fixed-header {
.sqliteviz-table thead th,
.fixed-header {
font-size: 14px;
font-weight: 600;
box-sizing: border-box;
@@ -108,8 +109,8 @@ table.sqliteviz-table {
color: var(--color-text-base);
}
.sqliteviz-table tbody td[data-isNull="true"],
.sqliteviz-table tbody td[data-isBlob="true"] {
.sqliteviz-table tbody td[data-isNull='true'],
.sqliteviz-table tbody td[data-isBlob='true'] {
color: var(--color-text-light-2);
font-style: italic;
}

View File

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

View File

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

View File

@@ -1,18 +1,27 @@
<template>
<div
:class="['checkbox-container', { 'checked': checked }, {'disabled': disabled}]"
:class="[
'checkbox-container',
{ checked: checked },
{ disabled: disabled }
]"
@click.stop="onClick"
>
<div v-show="!checked" class="unchecked" />
<img
v-show="checked && !disabled"
:src="theme === 'light'
? require('@/assets/images/checkbox_checked_light.svg')
: require('@/assets/images/checkbox_checked.svg')"
v-show="checked && !disabled && theme === 'light'"
class="checked-light"
src="~@/assets/images/checkbox_checked_light.svg"
/>
<img
v-show="checked && !disabled && theme !== 'light'"
class="checked"
src="~@/assets/images/checkbox_checked.svg"
/>
<img
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>
</div>
@@ -26,7 +35,7 @@ export default {
type: String,
required: false,
default: 'accent',
validator: (value) => {
validator: value => {
return ['accent', 'light'].includes(value)
}
},
@@ -46,6 +55,7 @@ export default {
default: false
}
},
emits: ['click'],
data() {
return {
checked: this.init

View File

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

View File

@@ -1,24 +1,23 @@
<template>
<modal
:name="dialogName"
classes="dialog"
height="auto"
width="80%"
:modalId="dialogName"
class="dialog"
contentClass="import-modal"
scrollable
:clickToClose="false"
>
<div class="dialog-header">
{{ typeName }} import
<close-icon @click="cancelImport" :disabled="disableDialog"/>
<close-icon :disabled="disableDialog" @click="cancelImport" />
</div>
<div class="dialog-body">
<text-field
label="Table name"
id="csv-json-table-name"
v-model="tableName"
label="Table name"
width="484px"
:disabled="disableDialog"
:error-msg="tableNameError"
id="csv-json-table-name"
:errorMsg="tableNameError"
/>
<div v-if="!isJson && !isNdJson" class="chars">
<delimiter-selector
@@ -29,27 +28,27 @@
@input="preview"
/>
<text-field
id="quote-char"
v-model="quoteChar"
label="Quote char"
hint="The character used to quote fields."
v-model="quoteChar"
width="93px"
:disabled="disableDialog"
class="char-input"
id="quote-char"
@input="preview"
/>
<text-field
id="escape-char"
v-model="escapeChar"
label="Escape char"
hint='
The character used to escape the quote character within a field
(e.g. "column with ""quotes"" in text").
'
max-hint-width="242px"
v-model="escapeChar"
maxHintWidth="242px"
width="93px"
:disabled="disableDialog"
class="char-input"
id="escape-char"
@input="preview"
/>
</div>
@@ -67,35 +66,32 @@
class="preview-table"
/>
<div v-else class="no-data">No data</div>
<logs
class="import-errors"
:messages="importMessages"
/>
<logs class="import-errors" :messages="importMessages" />
</div>
<div class="dialog-buttons-container">
<button
id="import-cancel"
class="secondary"
:disabled="disableDialog"
@click="cancelImport"
id="import-cancel"
>
Cancel
</button>
<button
v-show="!importCompleted"
id="import-start"
class="primary"
:disabled="disableDialog || disableImport"
@click="loadToDb(file)"
id="import-start"
>
Import
</button>
<button
v-show="importCompleted"
id="import-finish"
class="primary"
:disabled="disableDialog"
@click="finish"
id="import-finish"
>
Finish
</button>
@@ -130,6 +126,7 @@ export default {
db: Object,
dialogName: String
},
emits: ['cancel', 'finish'],
data() {
return {
disableDialog: false,
@@ -175,8 +172,7 @@ export default {
if (!this.tableName) {
return
}
this.db.validateTableName(this.tableName)
.catch(err => {
this.db.validateTableName(this.tableName).catch(err => {
this.tableNameError = err.message + '. Try another table name.'
})
}, 400)
@@ -257,10 +253,12 @@ export default {
}
} catch (err) {
console.error(err)
this.importMessages = [{
this.importMessages = [
{
message: err,
type: 'error'
}]
}
]
}
},
async getJsonParseResult(file) {
@@ -273,7 +271,7 @@ export default {
},
hasErrors: false,
messages: [],
rowCount: +(!isEmpty)
rowCount: +!isEmpty
}
},
async loadToDb(file) {
@@ -290,21 +288,22 @@ export default {
delimiter: this.delimiter,
columns: !this.isJson && !this.isNdJson ? null : ['doc']
}
const parsingMsg = {
let parsingMsg = {}
this.importMessages.push({
message: `Parsing ${this.typeName}...`,
type: 'info'
}
this.importMessages.push(parsingMsg)
const parsingLoadingIndicator = setTimeout(() => { parsingMsg.type = 'loading' }, 1000)
})
// Get *reactive* link to parsing message for later updates
parsingMsg = this.importMessages[this.importMessages.length - 1]
const parsingLoadingIndicator = setTimeout(() => {
parsingMsg.type = 'loading'
}, 1000)
const importMsg = {
message: `Importing ${this.typeName} into a SQLite database...`,
type: 'info'
}
let importMsg = {}
let importLoadingIndicator = null
const updateProgress = progress => {
this.$set(importMsg, 'progress', progress)
importMsg.progress = progress
}
const progressCounterId = this.db.createProgressCounter(updateProgress)
@@ -322,7 +321,9 @@ export default {
parsingMsg.type = 'success'
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}.`
} else {
// Inform about parsing success
@@ -333,7 +334,11 @@ export default {
clearTimeout(parsingLoadingIndicator)
// 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
importLoadingIndicator = setTimeout(() => {
@@ -342,13 +347,18 @@ export default {
// Add table
start = new Date()
await this.db.addTableFromCsv(this.tableName, parseResult.data, progressCounterId)
await this.db.addTableFromCsv(
this.tableName,
parseResult.data,
progressCounterId
)
end = new Date()
this.addedTable = this.tableName
// Inform about import success
period = time.getPeriod(start, end)
importMsg.message = `Importing ${this.typeName} ` +
importMsg.message =
`Importing ${this.typeName} ` +
`into a SQLite database is completed in ${period}.`
importMsg.type = 'success'
@@ -392,8 +402,10 @@ export default {
events.send('inquiry.create', null, { auto: true })
},
getQueryExample() {
return this.isNdJson ? this.getNdJsonQueryExample()
: this.isJson ? this.getJsonQueryExample()
return this.isNdJson
? this.getNdJsonQueryExample()
: this.isJson
? this.getJsonQueryExample()
: [
'/*',
` * Your CSV file has been imported into ${this.addedTable} table.`,
@@ -455,6 +467,15 @@ export default {
}
</script>
<style>
.import-modal {
width: 80%;
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>
<style scoped>
.dialog-body {
padding-bottom: 0;
@@ -494,11 +515,4 @@ margin-bottom: 24px;
justify-content: center;
align-items: center;
}
/* https://github.com/euvl/vue-js-modal/issues/623 */
>>> .vm--modal {
max-width: 1152px;
margin: auto;
left: 0 !important;
}
</style>

View File

@@ -10,34 +10,34 @@
@click="browse"
>
<div class="text">
Drop the database, CSV, JSON or NDJSON file here
or click to choose a file from your computer.
Drop the database, CSV, JSON or NDJSON file here or click to choose a
file from your computer.
</div>
</div>
</div>
<div 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
id="left-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/leftArm.svg')"
:class="{ swing: state === 'dragover' }"
src="~@/assets/images/leftArm.svg"
/>
<img
id="file-img"
ref="fileImg"
:class="{
'swing': state === 'dragover',
'fly': state === 'dropping',
'hidden': state === 'dropped'
swing: state === 'dragover',
fly: state === 'dropping',
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="body-img" :src="require('@/assets/images/body.svg')" />
<img id="drop-file-bottom-img" src="~@/assets/images/bottom.svg" />
<img id="body-img" src="~@/assets/images/body.svg" />
<img
id="right-arm-img"
:class="{'swing': state === 'dragover'}"
:src="require('@/assets/images/rightArm.svg')"
:class="{ swing: state === 'dragover' }"
src="~@/assets/images/rightArm.svg"
/>
</div>
<div id="error" class="error"></div>
@@ -47,7 +47,7 @@
ref="addCsvJson"
:file="file"
:db="newDb"
dialog-name="importFromCsvJson"
dialogName="importFromCsvJson"
@cancel="cancelImport"
@finish="finish"
/>
@@ -63,12 +63,16 @@ import events from '@/lib/utils/events'
export default {
name: 'DbUploader',
components: {
ChangeDbIcon,
CsvJsonImport
},
props: {
type: {
type: String,
required: false,
default: 'small',
validator: (value) => {
validator: value => {
return ['illustrated', 'small'].includes(value)
}
},
@@ -78,10 +82,7 @@ export default {
default: 'unset'
}
},
components: {
ChangeDbIcon,
CsvJsonImport
},
emits: [],
data() {
return {
state: '',
@@ -92,7 +93,7 @@ export default {
},
mounted() {
if (this.type === 'illustrated') {
this.animationPromise = new Promise((resolve) => {
this.animationPromise = new Promise(resolve => {
this.$refs.fileImg.addEventListener('animationend', event => {
if (event.animationName.startsWith('fly')) {
this.state = 'dropped'
@@ -118,8 +119,9 @@ export default {
},
loadDb(file) {
return Promise.all([this.newDb.loadDb(file), this.animationPromise])
.then(this.finish)
return Promise.all([this.newDb.loadDb(file), this.animationPromise]).then(
this.finish
)
},
async checkFile(file) {
@@ -139,12 +141,15 @@ export default {
await this.$nextTick()
const csvJsonImportModal = this.$refs.addCsvJson
csvJsonImportModal.reset()
return Promise.all([csvJsonImportModal.preview(), this.animationPromise])
.then(csvJsonImportModal.open)
return Promise.all([
csvJsonImportModal.preview(),
this.animationPromise
]).then(csvJsonImportModal.open)
}
},
browse() {
fIo.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
fIo
.getFileFromUser('.db,.sqlite,.sqlite3,.csv,.json,.ndjson')
.then(this.checkFile)
},
@@ -245,8 +250,12 @@ export default {
transform-origin: -74px 139px;
}
@keyframes swing {
0% { transform: rotate(0deg); }
100% { transform: rotate(-7deg); }
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(-7deg);
}
}
#file-img.fly {

View File

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

View File

@@ -1,13 +1,15 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
v-model="show"
class="dialog"
:clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
@update:modelValue="$emit('update:modelValue', $event)"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
<close-icon :disabled="loading" @click="cancel" />
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
@@ -15,20 +17,25 @@
{{ loadingMsg }}
</div>
<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 }}
</div>
</div>
<div class="dialog-buttons-container">
<button
class="secondary"
type="button"
:disabled="loading"
@click="$emit('cancel')"
@click="cancel"
>
Cancel
</button>
<button
class="primary"
type="button"
:disabled="loading"
@click="$emit('action')"
>
@@ -43,31 +50,66 @@ import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: {
modelValue: Boolean,
loadingMsg: String,
successMsg: String,
actionBtnName: String,
name: String,
title: String,
loading: Boolean
},
data() {
return {
show: this.modelValue
}
},
emits: ['cancel', 'action', 'update:modelValue'],
watch: {
modelValue() {
this.show = this.modelValue
},
loading() {
if (this.loading) {
this.$modal.show(this.name)
this.$emit('update:modelValue', true)
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel() {
this.$emit('cancel')
this.$emit('update:modelValue', false)
}
}
}
</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>
.loading-dialog-body {
display: flex;

View File

@@ -1,5 +1,10 @@
<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
class="loader-svg bg"
:style="{ strokeWidth }"
@@ -9,7 +14,11 @@
/>
<circle
class="loader-svg front"
:style="{ strokeDasharray: circleProgress, strokeDashoffset: offset, strokeWidth }"
:style="{
strokeDasharray: circleProgress,
strokeDashoffset: offset,
strokeWidth
}"
:cx="size / 2"
:cy="size / 2"
:r="radius"
@@ -31,10 +40,13 @@ export default {
default: 20
}
},
emits: [],
computed: {
circleProgress() {
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
return `${dash}px, ${space}px`
},
@@ -45,7 +57,7 @@ export default {
return this.size / 2 - this.strokeWidth
},
offset() {
return this.radius * 3.14 / 2
return (this.radius * 3.14) / 2
},
strokeWidth() {
return this.size / 10
@@ -57,7 +69,10 @@ export default {
<style scoped>
.loader-svg {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
left: 0;
right: 0;
top: 0;
bottom: 0;
fill: none;
stroke-linecap: round;
stroke: var(--color-accent);
@@ -111,5 +126,4 @@ export default {
r: 8;
}
}
</style>

View File

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

View File

@@ -7,10 +7,14 @@
{ 'splitpanes-dragging': dragging }
]"
>
<div class="movable-splitter" ref="movableSplitter" :style="movableSplitterStyle" />
<div
class="splitpanes-pane"
ref="movableSplitter"
class="movable-splitter"
:style="movableSplitterStyle"
/>
<div
ref="left"
class="splitpanes-pane"
:size="paneBefore.size"
max-size="30"
:style="styles.before"
@@ -27,8 +31,11 @@
:class="[
'toggle-btns',
{
'both': after.max === 100 && before.max === 100 &&
paneAfter.size > 0 && paneBefore.size > 0
both:
after.max === 100 &&
before.max === 100 &&
paneAfter.size > 0 &&
paneBefore.size > 0
}
]"
>
@@ -39,9 +46,9 @@
>
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
src="~@/assets/images/chevron.svg"
:style="directionBeforeIconStyle"
>
/>
</div>
<div
v-if="before.max === 100 && paneBefore.size > 0"
@@ -50,18 +57,14 @@
>
<img
class="direction-icon"
:src="require('@/assets/images/chevron.svg')"
src="~@/assets/images/chevron.svg"
:style="directionAfterIconStyle"
>
/>
</div>
</div>
</div>
<!-- splitter end -->
<div
class="splitpanes-pane"
ref="right"
:style="styles.after"
>
<div ref="right" class="splitpanes-pane" :style="styles.after">
<slot name="right-pane" />
</div>
</div>
@@ -86,12 +89,16 @@ export default {
}
}
},
emits: [],
data() {
return {
container: null,
paneBefore: this.before,
paneAfter: this.after,
beforeMinimising: !this.after.size || !this.before.size ? this.default : {
beforeMinimising:
!this.after.size || !this.before.size
? this.default
: {
before: this.before.size,
after: this.after.size
},
@@ -106,8 +113,12 @@ export default {
computed: {
styles() {
return {
before: { [this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%` },
after: { [this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%` }
before: {
[this.horizontal ? 'height' : 'width']: `${this.paneBefore.size}%`
},
after: {
[this.horizontal ? 'height' : 'width']: `${this.paneAfter.size}%`
}
}
},
movableSplitterStyle() {
@@ -147,25 +158,36 @@ export default {
}
}
},
mounted() {
this.container = this.$refs.container
},
methods: {
bindEvents() {
// 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)
if ('ontouchstart' in window) {
document.addEventListener('touchmove', this.onMouseMove, { passive: false })
document.addEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.addEventListener('touchend', this.onMouseUp)
}
},
unbindEvents() {
document.removeEventListener('mousemove', this.onMouseMove, { passive: false })
document.removeEventListener('mousemove', this.onMouseMove, {
passive: false
})
document.removeEventListener('mouseup', this.onMouseUp)
if ('ontouchstart' in window) {
document.removeEventListener('touchmove', this.onMouseMove, { passive: false })
document.removeEventListener('touchmove', this.onMouseMove, {
passive: false
})
document.removeEventListener('touchend', this.onMouseUp)
}
},
@@ -215,16 +237,14 @@ export default {
this.beforeMinimising.before = this.paneBefore.size
this.beforeMinimising.after = this.paneAfter.size
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
} else {
this.paneBefore.size = this.beforeMinimising.before
this.paneAfter.size = this.beforeMinimising.after
}
}
},
mounted () {
this.container = this.$refs.container
}
}
</script>
@@ -236,9 +256,15 @@ export default {
position: relative;
}
.splitpanes-vertical {flex-direction: row;}
.splitpanes-horizontal {flex-direction: column;}
.splitpanes-dragging * {user-select: none;}
.splitpanes-vertical {
flex-direction: row;
}
.splitpanes-horizontal {
flex-direction: column;
}
.splitpanes-dragging * {
user-select: none;
}
.splitpanes-pane {
width: 100%;
@@ -285,7 +311,7 @@ export default {
.splitpanes-vertical > .movable-splitter {
width: 8px;
z-index: 5;
height: 100%
height: 100%;
}
.splitpanes-horizontal > .splitpanes-splitter,
@@ -336,20 +362,32 @@ export default {
left: 50%;
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) 0 0 var(--border-radius-small);
}
.splitpanes-horizontal > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-horizontal
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 var(--border-radius-small) var(--border-radius-small) 0;
margin-left: -1px;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:first-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:first-child {
border-radius: var(--border-radius-small) var(--border-radius-small) 0 0;
}
.splitpanes-vertical > .splitpanes-splitter .toggle-btns.both .toggle-btn:last-child {
.splitpanes-vertical
> .splitpanes-splitter
.toggle-btns.both
.toggle-btn:last-child {
border-radius: 0 0 var(--border-radius-small) var(--border-radius-small);
margin-top: -1px;
}

View File

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

View File

@@ -1,32 +1,36 @@
<template>
<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"
: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>
<script>
import Paginate from 'vuejs-paginate'
import Paginate from 'vuejs-paginate-next'
export default {
name: 'Pager',
components: { Paginate },
props: ['pageCount', 'value'],
props: {
pageCount: Number,
modelValue: Number
},
emits: ['update:modelValue'],
data() {
return {
page: this.value,
page: this.modelValue,
chevron: `
<svg width="9" height="9" viewBox="0 0 8 12" fill="none">
<path
@@ -39,10 +43,10 @@ export default {
},
watch: {
page() {
this.$emit('input', this.page)
this.$emit('update:modelValue', this.page)
},
value () {
this.page = this.value
modelValue() {
this.page = this.modelValue
}
}
}
@@ -54,48 +58,52 @@ export default {
align-items: center;
line-height: 10px;
}
>>> .paginator-page-link {
:deep(a) {
cursor: pointer;
}
:deep(.paginator-page-link) {
padding: 2px 3px;
margin: 0 5px;
display: block;
color: var(--color-text-base);
font-size: 11px;
}
>>> .paginator-page-link:hover {
:deep(.paginator-page-link:hover) {
color: var(--color-text-active);
}
>>> .paginator-page-link:active,
>>> .paginator-page-link:visited,
>>> .paginator-page-link:focus,
>>> .paginator-next:active,
>>> .paginator-next:visited,
>>> .paginator-next:focus,
>>> .paginator-prev:active,
>>> .paginator-prev:visited,
>>> .paginator-prev:focus {
:deep(.paginator-page-link:active),
:deep(.paginator-page-link:visited),
:deep(.paginator-page-link:focus),
:deep(.paginator-next:active),
:deep(.paginator-next:visited),
:deep(.paginator-next:focus),
:deep(.paginator-prev:active),
:deep(.paginator-prev:visited),
:deep(.paginator-prev:focus) {
outline: none;
}
>>> .paginator-active-page,
>>> .paginator-active-page:hover {
:deep(.paginator-active-page),
:deep(.paginator-active-page:hover) {
color: var(--color-accent);
}
>>> .paginator-break:hover,
>>> .paginator-disabled:hover {
:deep(.paginator-break:hover),
:deep(.paginator-disabled:hover) {
cursor: default;
}
>>> .paginator-prev svg {
:deep(.paginator-prev svg) {
transform: rotate(180deg);
}
>>> .paginator-next:hover path,
>>> .paginator-prev:hover path {
:deep(.paginator-next:hover path),
:deep(.paginator-prev:hover path) {
fill: var(--color-text-active);
}
>>> .paginator-disabled path,
>>> .paginator-disabled:hover path {
:deep(.paginator-disabled path),
:deep(.paginator-disabled:hover path) {
fill: var(--color-text-light-2);
}
</style>

View File

@@ -1,21 +1,22 @@
<template>
<div>
<div class="rounded-bg">
<div class="header-container" ref="header-container">
<div ref="header-container" class="header-container">
<div>
<div
v-for="(th, index) in header"
:key="index"
class="fixed-header"
:style="{ width: `${th.width}px` }"
:key="index"
:title="th.name"
>
{{ th.name }}
</div>
</div>
</div>
<div
class="table-container"
ref="table-container"
class="table-container"
@scroll="onScrollTable"
>
<table
@@ -35,11 +36,11 @@
<tr v-for="rowIndex in currentPageData.count" :key="rowIndex">
<td
v-for="(col, colIndex) in columns"
:key="colIndex"
:data-col="colIndex"
:data-row="pageSize * (currentPage - 1) + rowIndex - 1"
:data-isNull="isNull(getCellValue(col, rowIndex))"
:data-isBlob="isBlob(getCellValue(col, rowIndex))"
:key="colIndex"
:aria-selected="false"
@click="onCellClick"
>
@@ -60,15 +61,15 @@
</div>
<pager
v-show="pageCount > 1"
:page-count="pageCount"
v-model="currentPage"
:pageCount="pageCount"
/>
</div>
</div>
</template>
<script>
import Pager from './Pager'
import Pager from './Pager.vue'
export default {
name: 'SqlTable',
@@ -87,6 +88,7 @@ export default {
preview: Boolean,
selectedCellCoordinates: Object
},
emits: ['updateSelectedCell'],
data() {
return {
header: null,
@@ -123,6 +125,15 @@ export default {
}
}
},
watch: {
currentPageData() {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet() {
this.currentPage = 1
}
},
mounted() {
this.resizeObserver = new ResizeObserver(this.calculateHeadersWidth)
this.resizeObserver.observe(this.$refs.table)
@@ -130,13 +141,17 @@ export default {
if (this.selectedCellCoordinates) {
const { row, col } = this.selectedCellCoordinates
const cell = this.$refs.table
.querySelector(`td[data-col="${col}"][data-row="${row}"]`)
const cell = this.$refs.table.querySelector(
`td[data-col="${col}"][data-row="${row}"]`
)
if (cell) {
this.selectCell(cell)
}
}
},
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs.table)
},
methods: {
isBlob(value) {
return value && ArrayBuffer.isView(value)
@@ -166,7 +181,8 @@ export default {
})
},
onScrollTable() {
this.$refs['header-container'].scrollLeft = this.$refs['table-container'].scrollLeft
this.$refs['header-container'].scrollLeft =
this.$refs['table-container'].scrollLeft
},
onTableKeydown(e) {
const keyCodeMap = {
@@ -241,24 +257,13 @@ export default {
newColIndex = currentColIndex
}
const newCell = this.$refs.table
.querySelector(`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`)
const newCell = this.$refs.table.querySelector(
`td[data-col="${newColIndex}"][data-row="${newRowIndex}"]`
)
if (newCell) {
this.selectCell(newCell)
}
}
},
beforeDestroy () {
this.resizeObserver.unobserve(this.$refs.table)
},
watch: {
currentPageData () {
this.calculateHeadersWidth()
this.selectCell(null)
},
dataSet () {
this.currentPage = 1
}
}
}
</script>
@@ -270,7 +275,7 @@ table.sqliteviz-table:focus {
.sqliteviz-table tbody td:hover {
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);
}
</style>

View File

@@ -1,17 +1,25 @@
<template>
<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 }}
<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>
<input
type="text"
:placeholder="placeholder"
:class="{ error: errorMsg }"
:style="{ width: width }"
:value="value"
:value="modelValue"
: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>
@@ -20,9 +28,19 @@
<script>
import HintIcon from '@/components/svg/hint'
export default {
name: 'textField',
props: ['placeholder', 'label', 'errorMsg', 'value', 'width', 'hint', 'maxHintWidth', 'disabled'],
components: { HintIcon }
name: 'TextField',
components: { HintIcon },
props: {
placeholder: String,
label: String,
errorMsg: String,
modelValue: String,
width: String,
hint: String,
maxHintWidth: String,
disabled: Boolean
},
emits: ['update:modelValue']
}
</script>

View File

@@ -33,7 +33,7 @@
</clipPath>
</defs>
</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
</span>
</span>
@@ -45,7 +45,8 @@ import tooltipMixin from '@/tooltipMixin'
export default {
name: 'AddTableIcon',
mixins: [tooltipMixin],
props: ['tooltip'],
props: { tooltip: String },
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()

View File

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

View File

@@ -21,7 +21,7 @@
fill="#A2B1C6"
/>
</svg>
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
<span ref="tooltip" class="icon-tooltip" :style="tooltipStyle">
Load another database, CSV, JSON or NDJSON
</span>
</div>
@@ -31,8 +31,9 @@
import tooltipMixin from '@/tooltipMixin'
export default {
name: 'changeDbIcon',
name: 'ChangeDbIcon',
mixins: [tooltipMixin],
emits: ['click'],
methods: {
onClick() {
this.hideTooltip()

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<template>
<svg
@click.stop="$emit('click')"
:class="['icon', {'disabled': disabled }]"
:class="['icon', { disabled: disabled }]"
:width="size"
:height="size"
viewBox="0 0 14 14"
fill="none"
xmlns="http://www.w3.org/2000/svg"
@click.stop="$emit('click')"
>
<path
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,
default: false
}
}
},
emits: ['click']
}
</script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="18"
viewBox="0 0 19 18"
fill="none"
>
<svg width="19" height="18" viewBox="0 0 19 18" fill="none">
<path
d="M6.07959 13.5756C6.05908 14.0209 5.93896 14.415 5.71924 14.7578C5.49951 15.0976 5.19043
15.3613 4.79199 15.5488C4.39648 15.7363 3.94385 15.83 3.43408 15.83C2.59326 15.83
@@ -55,7 +50,6 @@
</template>
<script>
export default {
name: 'ExportToCsvIcon'
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2130_5292)">
<path
fill-rule="evenodd"
@@ -46,7 +41,6 @@
</template>
<script>
export default {
name: 'RowIcon'
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,5 @@
<template>
<svg
width="19"
height="19"
viewBox="0 0 19 19"
fill="none"
>
<svg width="19" height="19" viewBox="0 0 19 19" fill="none">
<g clip-path="url(#clip0_2131_6054)">
<path
d="M3.53784 11.5846L3.53784 3.14734L11.9751 3.14734V7.676C12.4655 7.51991
@@ -43,7 +38,6 @@
</template>
<script>
export default {
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,4 +1,4 @@
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 { nanoid } from 'nanoid'
@@ -21,7 +21,7 @@ export function getOptionsForSave (state, dataSources) {
for (const key in dataSources) {
emptySources[key] = []
}
dereference(stateCopy.data, emptySources)
dereference.default(stateCopy.data, emptySources)
return stateCopy
}

View File

@@ -61,7 +61,9 @@ export default {
}
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

View File

@@ -1,8 +1,13 @@
import initSqlJs from 'sql.js/dist/sql-wasm.js'
import initSqlJs from 'sql.js'
import dbUtils from './_statements'
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
let SQL = null
const sqlModuleReady = initSqlJs().then(sqlModule => { SQL = sqlModule })
const sqlModuleReady = initSqlJs({
locateFile: () => wasmUrl
}).then(sqlModule => {
SQL = sqlModule
})
function _getDataSourcesFromSqlResult(sqlResult) {
if (!sqlResult) {
@@ -21,8 +26,7 @@ export default class Sql {
}
static build() {
return sqlModuleReady
.then(() => {
return sqlModuleReady.then(() => {
return new Sql()
})
}
@@ -77,7 +81,10 @@ export default class Sql {
}
this.db.exec('COMMIT')
count++
progressCallback({ progress: 100 * (count / chunksAmount), id: progressCounterId })
progressCallback({
progress: 100 * (count / chunksAmount),
id: progressCounterId
})
}
return {

View File

@@ -2,7 +2,9 @@ export default {
*generateChunks(data, size) {
const matrix = Object.keys(data).map(col => data[col])
const [row] = matrix
const transposedMatrix = row.map((value, column) => matrix.map(row => row[column]))
const transposedMatrix = row.map((value, column) =>
matrix.map(row => row[column])
)
const count = Math.ceil(transposedMatrix.length / size)
@@ -38,7 +40,8 @@ export default {
type = 'TEXT'
break
}
default: type = 'TEXT'
default:
type = 'TEXT'
}
result += `"${col}" ${type}, `
}

View File

@@ -35,7 +35,5 @@ function onError (error) {
}
registerPromiseWorker(data => {
return sqlReady
.then(processMsg.bind(data))
.catch(onError)
return sqlReady.then(processMsg.bind(data)).catch(onError)
})

View File

@@ -1,7 +1,4 @@
import fu from '@/lib/utils/fileIo'
// We can import workers like so because of worker-loader:
// https://webpack.js.org/loaders/worker-loader/
import Worker from './_worker.js'
// Use promise-worker in order to turn worker into the promise based one:
// https://github.com/nolanlawson/promise-worker
@@ -10,7 +7,9 @@ import PromiseWorker from 'promise-worker'
import events from '@/lib/utils/events'
function getNewDatabase() {
const worker = new Worker()
const worker = new Worker(new URL('./_worker.js', import.meta.url), {
type: 'module'
})
return new Database(worker)
}
@@ -31,9 +30,11 @@ class Database {
const progress = e.data.progress
if (progress !== undefined) {
const id = e.data.id
this.importProgresses[id].dispatchEvent(new CustomEvent('progress', {
this.importProgresses[id].dispatchEvent(
new CustomEvent('progress', {
detail: progress
}))
})
)
}
})
}
@@ -45,7 +46,9 @@ class Database {
createProgressCounter(callback) {
const id = progressCounterIds++
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
}
@@ -70,7 +73,10 @@ class Database {
async loadDb(file) {
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) {
throw new Error(res.error)
@@ -130,7 +136,9 @@ class Database {
}
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)) {

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

@@ -4,11 +4,13 @@ import events from '@/lib/utils/events'
import migration from './_migrations'
const migrate = migration._migrate
const myInquiriesKey = 'myInquiries'
export default {
version: 2,
myInquiriesKey,
getStoredInquiries() {
let myInquiries = JSON.parse(localStorage.getItem('myInquiries'))
let myInquiries = JSON.parse(localStorage.getItem(myInquiriesKey))
if (!myInquiries) {
const oldInquiries = localStorage.getItem('myQueries')
if (oldInquiries) {
@@ -26,7 +28,8 @@ export default {
const newInquiry = JSON.parse(JSON.stringify(baseInquiry))
newInquiry.name = newInquiry.name + ' Copy'
newInquiry.id = nanoid()
newInquiry.createdAt = new Date()
newInquiry.createdAt = new Date().toJSON()
newInquiry.updatedAt = new Date().toJSON()
delete newInquiry.isPredefined
return newInquiry
@@ -37,13 +40,20 @@ export default {
},
updateStorage(inquiries) {
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
localStorage.setItem(
myInquiriesKey,
JSON.stringify({ version: this.version, inquiries })
)
},
serialiseInquiries(inquiryList) {
const preparedData = JSON.parse(JSON.stringify(inquiryList))
preparedData.forEach(inquiry => delete inquiry.isPredefined)
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
return JSON.stringify(
{ version: this.version, inquiries: preparedData },
null,
4
)
},
deserialiseInquiries(str) {
@@ -59,7 +69,9 @@ export default {
// Generate new ids if they are the same as existing inquiries
inquiryList.forEach(inquiry => {
const allInquiriesIds = this.getStoredInquiries().map(inquiry => inquiry.id)
const allInquiriesIds = this.getStoredInquiries().map(
inquiry => inquiry.id
)
if (allInquiriesIds.includes(inquiry.id)) {
inquiry.id = nanoid()
}
@@ -69,8 +81,7 @@ export default {
},
importInquiries() {
return fu.importFile()
.then(str => {
return fu.importFile().then(str => {
const inquires = this.deserialiseInquiries(str)
events.send('inquiry.import', inquires.length)

View File

@@ -6,7 +6,9 @@ export default class Tab {
constructor(state, inquiry = {}) {
this.id = inquiry.id || nanoid()
this.name = inquiry.id ? inquiry.name : null
this.tempName = inquiry.name || (state.untitledLastIndex
this.tempName =
inquiry.name ||
(state.untitledLastIndex
? `Untitled ${state.untitledLastIndex}`
: 'Untitled')
this.query = inquiry.query
@@ -26,6 +28,7 @@ export default class Tab {
this.isSaved = !!inquiry.id
this.state = state
this.updatedAt = inquiry.updatedAt
}
async execute() {
@@ -39,7 +42,8 @@ export default class Tab {
this.time = time.getPeriod(start, new Date())
if (this.result && this.result.values) {
events.send('resultset.create',
events.send(
'resultset.create',
this.result.values[this.result.columns[0]].length
)
}

View File

@@ -19,7 +19,8 @@ export default {
async _copyBlob(blob) {
await navigator.clipboard.write([
new ClipboardItem({ // eslint-disable-line no-undef
new ClipboardItem({
// eslint-disable-line no-undef
[blob.type]: blob
})
])
@@ -32,9 +33,13 @@ export default {
},
async _copyCanvas(canvas) {
canvas.toBlob(async (blob) => {
canvas.toBlob(
async blob => {
await this._copyBlob(blob)
Lib.notifier('Image copied to clipboard successfully', 'long')
}, 'image/png', 1)
},
'image/png',
1
)
}
}

View File

@@ -57,8 +57,7 @@ export default {
},
importFile() {
return this.getFileFromUser('.json')
.then(file => {
return this.getFileFromUser('.json').then(file => {
return this.getFileContent(file)
})
},

View File

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

View File

@@ -14,7 +14,8 @@ function invokeServiceWorkerUpdateFlow (registration) {
if ('serviceWorker' in navigator) {
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
// by re-invoking the prompt when there's a waiting Service Worker
if (registration.waiting) {

View File

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

View File

@@ -17,26 +17,33 @@ export default {
},
async saveInquiry({ state }, { inquiryTab, newName }) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
id: inquiryTab.isPredefined || newName ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
viewType: inquiryTab.dataView.mode,
viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.name
name: newName || inquiryTab.name,
updatedAt: new Date().toJSON()
}
// Get inquiries from local storage
const myInquiries = state.inquiries
let inquiryIndex
// Set createdAt
if (newName) {
value.createdAt = new Date()
value.createdAt = new Date().toJSON()
} else {
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
value.createdAt = myInquiries[inquiryIndex].createdAt
inquiryIndex = myInquiries.findIndex(
oldInquiry => oldInquiry.id === inquiryTab.id
)
value.createdAt =
inquiryIndex !== -1
? myInquiries[inquiryIndex].createdAt
: new Date().toJSON()
}
// Insert in inquiries list
if (newName) {
if (newName || inquiryIndex === -1) {
myInquiries.push(value)
} else {
myInquiries.splice(inquiryIndex, 1, value)
@@ -63,8 +70,9 @@ export default {
}
},
renameInquiry({ state, commit }, { inquiryId, newName }) {
const renamingInquiry = state.inquiries
.find(inquiry => inquiry.id === inquiryId)
const renamingInquiry = state.inquiries.find(
inquiry => inquiry.id === inquiryId
)
renamingInquiry.name = newName

View File

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

View File

@@ -7,23 +7,39 @@ export default {
},
updateTab(state, { tab, newValues }) {
const { name, id, query, viewType, viewOptions, isSaved } = newValues
const { name, id, query, viewType, viewOptions, isSaved, updatedAt } =
newValues
const oldId = tab.id
if (id && state.currentTabId === oldId) {
state.currentTabId = id
}
if (id) { tab.id = id }
if (name) { tab.name = name }
if (query) { tab.query = query }
if (viewType) { tab.viewType = viewType }
if (viewOptions) { tab.viewOptions = viewOptions }
if (isSaved !== undefined) { tab.isSaved = isSaved }
if (id) {
tab.id = id
}
if (name) {
tab.name = name
}
if (query) {
tab.query = query
}
if (viewType) {
tab.viewType = viewType
}
if (viewOptions) {
tab.viewOptions = viewOptions
}
if (isSaved !== undefined) {
tab.isSaved = isSaved
}
if (isSaved) {
// Saved inquiry is not predefined
delete tab.isPredefined
}
if (updatedAt) {
tab.updatedAt = updatedAt
}
},
deleteTab(state, tab) {
@@ -49,11 +65,13 @@ export default {
state.currentTabId = id
state.currentTab = state.tabs.find(tab => tab.id === id)
} catch (e) {
console.error('Can\'t open a tab id:' + id)
console.error("Can't open a tab id:" + id)
}
},
updatePredefinedInquiries(state, inquiries) {
state.predefinedInquiries = Array.isArray(inquiries) ? inquiries : [inquiries]
state.predefinedInquiries = Array.isArray(inquiries)
? inquiries
: [inquiries]
},
setLoadingPredefinedInquiries(state, value) {
state.loadingPredefinedInquiries = value
@@ -63,5 +81,8 @@ export default {
},
setInquiries(state, value) {
state.inquiries = value
},
setIsWorkspaceVisible(state, value) {
state.isWorkspaceVisible = value
}
}

View File

@@ -7,5 +7,6 @@ export default {
predefinedInquiries: [],
loadingPredefinedInquiries: false,
predefinedInquiriesLoaded: false,
db: null
db: null,
isWorkspaceVisible: false
}

View File

@@ -13,7 +13,9 @@ export default {
},
methods: {
showTooltip(e, tooltipPosition) {
const position = tooltipPosition ? tooltipPosition.split('-') : ['top', 'right']
const position = tooltipPosition
? tooltipPosition.split('-')
: ['top', 'right']
const offset = 12
if (position[0] === 'top') {
@@ -25,7 +27,8 @@ export default {
if (position[1] === 'right') {
this.tooltipStyle.left = e.clientX + offset + 'px'
} 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'

View File

@@ -1,14 +1,12 @@
<template>
<div>
<logs
id="logs"
:messages="messages"
/>
<logs id="logs" :messages="messages" />
<button
v-if="hasErrors"
id="open-workspace-btn"
class="secondary"
@click="$router.push('/workspace?hide_schema=1')">
@click="$router.push('/workspace?hide_schema=1')"
>
Open workspace
</button>
</div>
@@ -190,7 +188,6 @@ export default {
#logs {
margin: 8px auto;
max-width: 800px;
}
#open-workspace-btn {

View File

@@ -1,251 +0,0 @@
<template>
<nav>
<div id="nav-links">
<a href="https://sqliteviz.com">
<img :src="require('@/assets/images/logo_simple.svg')">
</a>
<router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link>
<a href="https://sqliteviz.com/docs" target="_blank">Help</a>
</div>
<div id="nav-buttons">
<button
id="save-btn"
v-show="currentInquiry && $route.path === '/workspace'"
class="primary"
:disabled="isSaved"
@click="checkInquiryBeforeSave"
>
Save
</button>
<button
id="create-btn"
class="primary"
@click="createNewInquiry"
>
Create
</button>
<app-diagnostic-info />
</div>
<!--Save Inquiry dialog -->
<modal name="save" classes="dialog" height="auto">
<div class="dialog-header">
Save inquiry
<close-icon @click="cancelSave"/>
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
<img :src="require('@/assets/images/info.svg')">
Note: Predefined inquiries can't be edited.
That's why your modifications will be saved as a new inquiry. Enter the name for it.
</div>
<text-field
label="Inquiry name"
:error-msg="errorMsg"
v-model="name"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="saveInquiry">Save</button>
</div>
</modal>
</nav>
</template>
<script>
import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events'
export default {
name: 'MainMenu',
components: {
TextField,
CloseIcon,
AppDiagnosticInfo
},
data () {
return {
name: '',
errorMsg: null
}
},
computed: {
currentInquiry () {
return this.$store.state.currentTab
},
isSaved () {
return this.currentInquiry && this.currentInquiry.isSaved
},
isPredefined () {
return this.currentInquiry && this.currentInquiry.isPredefined
},
runDisabled () {
return this.currentInquiry && (!this.$store.state.db || !this.currentInquiry.query)
}
},
created () {
this.$root.$on('createNewInquiry', this.createNewInquiry)
this.$root.$on('saveInquiry', this.checkInquiryBeforeSave)
document.addEventListener('keydown', this._keyListener)
},
beforeDestroy () {
document.removeEventListener('keydown', this._keyListener)
},
methods: {
createNewInquiry () {
this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/workspace') {
this.$router.push('/workspace')
}
})
events.send('inquiry.create', null, { auto: false })
},
cancelSave () {
this.$modal.hide('save')
this.$root.$off('inquirySaved')
},
checkInquiryBeforeSave () {
this.errorMsg = null
this.name = ''
if (storedInquiries.isTabNeedName(this.currentInquiry)) {
this.$modal.show('save')
} else {
this.saveInquiry()
}
},
async saveInquiry () {
const isNeedName = storedInquiries.isTabNeedName(this.currentInquiry)
if (isNeedName && !this.name) {
this.errorMsg = 'Inquiry name can\'t be empty'
return
}
const dataSet = this.currentInquiry.result
const tabView = this.currentInquiry.view
// Save inquiry
const value = await this.$store.dispatch('saveInquiry', {
inquiryTab: this.currentInquiry,
newName: this.name
})
// Update tab in store
this.$store.commit('updateTab', {
tab: this.currentInquiry,
newValues: {
name: value.name,
id: value.id,
query: value.query,
viewType: value.viewType,
viewOptions: value.viewOptions,
isSaved: true
}
})
// Restore data:
// e.g. if we save predefined inquiry the tab will be created again
// (because of new id) and
// it will be without sql result and has default view - table.
// That's why we need to restore data and view
this.$nextTick(() => {
this.currentInquiry.result = dataSet
this.currentInquiry.view = tabView
})
// Hide dialog
this.$modal.hide('save')
// Signal about saving
this.$root.$emit('inquirySaved')
events.send('inquiry.save')
},
_keyListener (e) {
if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (!this.runDisabled) {
this.currentInquiry.execute()
}
return
}
// Save inquiry Ctrl+S
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (!this.isSaved) {
this.checkInquiryBeforeSave()
}
return
}
}
// New (blank) inquiry Ctrl+B
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
this.createNewInquiry()
}
}
}
}
</script>
<style scoped>
nav {
height: 68px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg-light);
border-bottom: 1px solid var(--color-border-light);
box-shadow: var(--shadow-1);
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
width: 100vw;
padding: 0 16px 0 52px;
z-index: 999;
}
a {
font-size: 18px;
color: var(--color-text-base);
text-transform: none;
text-decoration: none;
margin-right: 28px;
}
a.router-link-active {
color: var(--color-accent);
}
button {
margin-left: 16px;
}
#save-note {
margin-bottom: 24px;
display: flex;
align-items: flex-start;
}
#save-note img {
margin: -3px 6px 0 0;
}
#nav-buttons {
display: flex;
}
#nav-links {
display: flex;
align-items: center;
}
#nav-links img {
width: 32px;
}
</style>

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.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,10 +2,10 @@
<div id="app-info-container">
<img
id="app-info-icon"
:src="require('@/assets/images/info.svg')"
src="~@/assets/images/info.svg"
@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">
App info
<close-icon @click="$modal.hide('app-info')" />
@@ -15,7 +15,7 @@
{{ item.name }}
<div class="divider" />
<div class="options">
<div v-for="(opt, index) in item.info" :key="index">
<div v-for="(opt, optIndex) in item.info" :key="optIndex">
{{ opt }}
</div>
</div>
@@ -27,6 +27,7 @@
<script>
import CloseIcon from '@/components/svg/close'
import { version } from '../../../package.json'
export default {
name: 'AppDiagnosticInfo',
@@ -36,7 +37,7 @@ export default {
info: [
{
name: 'sqliteviz version',
info: [require('../../../package.json').version]
info: [version]
}
]
}
@@ -59,6 +60,12 @@ export default {
}
</script>
<style>
.app-info-modal {
width: 400px;
}
</style>
<style scoped>
#app-info-icon {
cursor: pointer;

View File

@@ -1,43 +1,55 @@
<template>
<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.
<span class="link" @click="$root.$emit('createNewInquiry')">Create</span>
<span class="link" @click="emitCreateTabEvent">Create</span>
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
id="loading-predefined-status"
v-if="$store.state.loadingPredefinedInquiries"
id="loading-predefined-status"
>
<loading-indicator />
Loading predefined inquiries...
</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="toolbar-buttons">
<button id="toolbar-btns-import" class="toolbar" @click="importInquiries">
<button
id="toolbar-btns-import"
class="toolbar"
@click="importInquiries"
>
Import
</button>
<button
v-show="selectedInquiriesCount > 0"
id="toolbar-btns-export"
class="toolbar"
v-show="selectedInquiriesCount > 0"
@click="exportSelectedInquiries()"
>
Export
</button>
<button
v-show="selectedNotPredefinedCount > 0"
id="toolbar-btns-delete"
class="toolbar"
v-show="selectedNotPredefinedCount > 0"
@click="showDeleteDialog(selectedInquiriesIds)"
>
Delete
</button>
</div>
<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>
@@ -48,16 +60,21 @@
<div v-show="showedInquiries.length > 0" class="rounded-bg">
<div class="header-container">
<div>
<div class="fixed-header" ref="name-th">
<check-box ref="mainCheckBox" theme="light" @click="toggleSelectAll"/>
<div ref="name-th" class="fixed-header">
<check-box
ref="mainCheckBox"
theme="light"
@click="toggleSelectAll"
/>
<div class="name-th">Name</div>
</div>
<div class="fixed-header">
Created at
<div class="fixed-header">Created at</div>
</div>
</div>
</div>
<div class="table-container" :style="{ 'max-height': `${maxTableHeight}px` }">
<div
class="table-container"
:style="{ 'max-height': `${maxTableHeight}px` }"
>
<table ref="table" class="sqliteviz-table">
<tbody>
<tr
@@ -70,6 +87,7 @@
<check-box
ref="rowCheckBox"
:init="selectAll || selectedInquiriesIds.has(inquiry.id)"
data-test="rowCheckBox"
@click="toggleRow($event, inquiry.id)"
/>
<div class="name">{{ inquiry.name }}</div>
@@ -80,16 +98,22 @@
@mouseleave="hideTooltip"
>
Predefined
<span class="icon-tooltip" :style="tooltipStyle" ref="tooltip">
Predefined inquiries come from the server.
These inquiries cant be deleted or renamed.
<span
ref="tooltip"
class="icon-tooltip"
:style="tooltipStyle"
>
Predefined inquiries come from the server. These
inquiries cant be deleted or renamed.
</span>
</div>
</div>
</td>
<td>
<div class="second-column">
<div class="date-container">{{ inquiry.createdAt | date }}</div>
<div class="date-container">
{{ createdAtFormatted(inquiry.createdAt) }}
</div>
<div class="icons-container">
<rename-icon
v-if="!inquiry.isPredefined"
@@ -97,13 +121,13 @@
/>
<copy-icon @click="duplicateInquiry(index)" />
<export-icon
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
tooltip="Export inquiry to file"
tooltip-position="top-left"
tooltipPosition="top-left"
@click="exportToFile([inquiry], `${inquiry.name}.json`)"
/>
<delete-icon
v-if="!inquiry.isPredefined"
@click="showDeleteDialog((new Set()).add(inquiry.id))"
@click="showDeleteDialog(new Set().add(inquiry.id))"
/>
</div>
</div>
@@ -116,16 +140,16 @@
</div>
<!--Rename Inquiry dialog -->
<modal name="rename" classes="dialog" height="auto">
<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
label="New inquiry name"
:error-msg="errorMsg"
v-model="newName"
label="New inquiry name"
:errorMsg="errorMsg"
width="100%"
/>
</div>
@@ -136,15 +160,18 @@
</modal>
<!--Delete Inquiry dialog -->
<modal name="delete" classes="dialog" height="auto">
<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="require('@/assets/images/info.svg')">
<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>
@@ -167,6 +194,7 @@ import CheckBox from '@/components/CheckBox'
import LoadingIndicator from '@/components/LoadingIndicator'
import tooltipMixin from '@/tooltipMixin'
import storedInquiries from '@/lib/storedInquiries'
import eventBus from '@/lib/eventBus'
export default {
name: 'Inquiries',
@@ -213,7 +241,8 @@ export default {
let showedInquiries = this.allInquiries
if (this.filter) {
showedInquiries = showedInquiries.filter(
inquiry => inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
inquiry =>
inquiry.name.toUpperCase().indexOf(this.filter.toUpperCase()) >= 0
)
}
return showedInquiries
@@ -223,45 +252,57 @@ export default {
return this.predefinedInquiries.concat(this.inquiries)
},
processedInquiryIndex() {
return this.inquiries.findIndex(inquiry => inquiry.id === this.processedInquiryId)
return this.inquiries.findIndex(
inquiry => inquiry.id === this.processedInquiryId
)
},
deleteDialogMsg() {
if (!this.deleteGroup && (
this.processedInquiryIndex === null ||
if (
!this.deleteGroup &&
(this.processedInquiryIndex === null ||
this.processedInquiryIndex < 0 ||
this.processedInquiryIndex > this.inquiries.length
)) {
this.processedInquiryIndex > this.inquiries.length)
) {
return ''
}
const deleteItem = this.deleteGroup
? `${this.selectedNotPredefinedCount} ${this.selectedNotPredefinedCount > 1
? 'inquiries'
: 'inquiry'}`
? `${this.selectedNotPredefinedCount} ${
this.selectedNotPredefinedCount > 1 ? 'inquiries' : 'inquiry'
}`
: `"${this.inquiries[this.processedInquiryIndex].name}"`
return `Are you sure you want to delete ${deleteItem}?`
}
},
watch: {
showedInquiries () {
this.selectedInquiriesIds = new Set(this.showedInquiries
showedInquiries: {
handler() {
this.selectedInquiriesIds = new Set(
this.showedInquiries
.filter(inquiry => this.selectedInquiriesIds.has(inquiry.id))
.map(inquiry => inquiry.id)
)
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = ([...this.selectedInquiriesIds]
.filter(id => !this.predefinedInquiriesIds.has(id))).length
this.selectedNotPredefinedCount = [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
if (this.selectedInquiriesIds.size < this.showedInquiries.length) {
if (this.$refs.mainCheckBox) {
this.$refs.mainCheckBox.checked = false
}
this.selectAll = false
}
},
deep: true
}
},
async created() {
const loadingPredefinedInquiries = this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded = this.$store.state.predefinedInquiriesLoaded
const loadingPredefinedInquiries =
this.$store.state.loadingPredefinedInquiries
const predefinedInquiriesLoaded =
this.$store.state.predefinedInquiriesLoaded
if (!predefinedInquiriesLoaded && !loadingPredefinedInquiries) {
try {
this.$store.commit('setLoadingPredefinedInquiries', true)
@@ -283,12 +324,15 @@ export default {
this.calcNameWidth()
this.calcMaxTableHeight()
},
beforeDestroy () {
beforeUnmount() {
this.resizeObserver.unobserve(this.$refs['my-inquiries-content'])
this.tableResizeObserver.unobserve(this.$refs.table)
},
filters: {
date (value) {
methods: {
emitCreateTabEvent() {
eventBus.$emit('createNewInquiry')
},
createdAtFormatted(value) {
if (!value) {
return ''
}
@@ -298,13 +342,15 @@ export default {
hour: '2-digit',
minute: '2-digit'
}
return new Date(value).toLocaleDateString('en-GB', dateOptions) + ' ' +
return (
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]
const nameWidth =
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`
@@ -315,10 +361,12 @@ export default {
},
openInquiry(index) {
const tab = this.showedInquiries[index]
setTimeout(() => {
this.$store.dispatch('addTab', tab).then(id => {
this.$store.commit('setCurrentTabId', id)
this.$router.push('/workspace')
})
})
},
showRenameDialog(id) {
this.errorMsg = null
@@ -340,7 +388,9 @@ export default {
this.$modal.hide('rename')
},
duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
const newInquiry = storedInquiries.duplicateInquiry(
this.showedInquiries[index]
)
this.$store.dispatch('addInquiry', newInquiry)
},
showDeleteDialog(idsSet) {
@@ -353,7 +403,10 @@ export default {
deleteInquiry() {
this.$modal.hide('delete')
if (!this.deleteGroup) {
this.$store.dispatch('deleteInquiries', new Set().add(this.processedInquiryId))
this.$store.dispatch(
'deleteInquiries',
new Set().add(this.processedInquiryId)
)
// Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
@@ -371,23 +424,27 @@ export default {
storedInquiries.export(inquiryList, fileName)
},
exportSelectedInquiries() {
const inquiryList = this.allInquiries.filter(
inquiry => this.selectedInquiriesIds.has(inquiry.id)
const inquiryList = this.allInquiries.filter(inquiry =>
this.selectedInquiriesIds.has(inquiry.id)
)
this.exportToFile(inquiryList, 'My sqliteviz inquiries.json')
},
importInquiries() {
storedInquiries.importInquiries()
.then(importedInquiries => {
this.$store.commit('setInquiries', this.inquiries.concat(importedInquiries))
storedInquiries.importInquiries().then(importedInquiries => {
this.$store.commit(
'setInquiries',
this.inquiries.concat(importedInquiries)
)
})
},
toggleSelectAll(checked) {
this.selectAll = checked
this.$refs.rowCheckBox.forEach(item => { item.checked = checked })
this.$refs.rowCheckBox.forEach(item => {
item.checked = checked
})
this.selectedInquiriesIds = checked
? new Set(this.showedInquiries.map(inquiry => inquiry.id))
@@ -395,8 +452,9 @@ export default {
this.selectedInquiriesCount = this.selectedInquiriesIds.size
this.selectedNotPredefinedCount = checked
? ([...this.selectedInquiriesIds].filter(id => !this.predefinedInquiriesIds.has(id)))
.length
? [...this.selectedInquiriesIds].filter(
id => !this.predefinedInquiriesIds.has(id)
).length
: 0
},

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,317 @@
<template>
<nav>
<div id="nav-links">
<a href="https://sqliteviz.com">
<img src="~@/assets/images/logo_simple.svg" />
</a>
<router-link to="/workspace">Workspace</router-link>
<router-link to="/inquiries">Inquiries</router-link>
<a href="https://sqliteviz.com/docs" target="_blank">Help</a>
</div>
<div id="nav-buttons">
<button
v-show="currentInquiryTab && $route.path === '/workspace'"
id="save-btn"
class="primary"
:disabled="isSaved"
@click="onSave(false)"
>
Save
</button>
<button
v-show="currentInquiryTab && $route.path === '/workspace'"
id="save-as-btn"
class="primary"
@click="onSaveAs"
>
Save as
</button>
<button id="create-btn" class="primary" @click="createNewInquiry">
Create
</button>
<app-diagnostic-info />
</div>
<!--Save Inquiry dialog -->
<modal modalId="save" class="dialog" contentStyle="width: 560px;">
<div class="dialog-header">
Save inquiry
<close-icon @click="cancelSave" />
</div>
<div class="dialog-body">
<div v-show="isPredefined" id="save-note">
<img src="~@/assets/images/info.svg" />
Note: Predefined inquiries can't be edited. That's why your
modifications will be saved as a new inquiry. Enter the name for it.
</div>
<text-field
v-model="name"
label="Inquiry name"
:errorMsg="errorMsg"
width="100%"
/>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="validateSaveFormAndSaveInquiry">
Save
</button>
</div>
</modal>
<!-- Inquiery saving conflict dialog -->
<modal
modalId="inquiry-conflict"
class="dialog"
contentStyle="width: 560px;"
>
<div class="dialog-header">
Inquiry saving conflict
<close-icon @click="cancelSave" />
</div>
<div class="dialog-body">
<div id="save-note">
<img src="~@/assets/images/info.svg" />
This inquiry has been modified in the mean time. This can happen if an
inquiry is saved in another window or browser tab. Do you want to
overwrite that changes or save the current state as a new inquiry?
</div>
</div>
<div class="dialog-buttons-container">
<button class="secondary" @click="cancelSave">Cancel</button>
<button class="primary" @click="onSave(true)">Overwrite</button>
<button class="primary" @click="onSaveAs">Save as new</button>
</div>
</modal>
</nav>
</template>
<script>
import TextField from '@/components/TextField'
import CloseIcon from '@/components/svg/close'
import storedInquiries from '@/lib/storedInquiries'
import AppDiagnosticInfo from './AppDiagnosticInfo'
import events from '@/lib/utils/events'
import eventBus from '@/lib/eventBus'
export default {
name: 'MainMenu',
components: {
TextField,
CloseIcon,
AppDiagnosticInfo
},
data() {
return {
name: '',
errorMsg: null
}
},
computed: {
inquiries() {
return this.$store.state.inquiries
},
currentInquiryTab() {
return this.$store.state.currentTab
},
isSaved() {
return this.currentInquiryTab && this.currentInquiryTab.isSaved
},
isPredefined() {
return this.currentInquiryTab && this.currentInquiryTab.isPredefined
},
runDisabled() {
return (
this.currentInquiryTab &&
(!this.$store.state.db || !this.currentInquiryTab.query)
)
}
},
created() {
eventBus.$on('createNewInquiry', this.createNewInquiry)
eventBus.$on('saveInquiry', this.onSave)
document.addEventListener('keydown', this._keyListener)
},
beforeUnmount() {
document.removeEventListener('keydown', this._keyListener)
},
methods: {
createNewInquiry() {
this.$store.dispatch('addTab').then(id => {
this.$store.commit('setCurrentTabId', id)
if (this.$route.path !== '/workspace') {
this.$router.push('/workspace')
}
})
events.send('inquiry.create', null, { auto: false })
},
cancelSave() {
this.errorMsg = null
this.name = ''
this.$modal.hide('save')
this.$modal.hide('inquiry-conflict')
eventBus.$off('inquirySaved')
},
onSave(skipConcurrentEditingCheck = false) {
if (storedInquiries.isTabNeedName(this.currentInquiryTab)) {
this.openSaveModal()
return
}
if (!skipConcurrentEditingCheck) {
const inquiryInStore = this.inquiries.find(
inquiry => inquiry.id === this.currentInquiryTab.id
)
if (
inquiryInStore &&
inquiryInStore.updatedAt !== this.currentInquiryTab.updatedAt
) {
this.$modal.show('inquiry-conflict')
return
}
}
this.saveInquiry()
},
onSaveAs() {
this.openSaveModal()
},
openSaveModal() {
this.errorMsg = null
this.name = ''
this.$modal.show('save')
},
validateSaveFormAndSaveInquiry() {
if (!this.name) {
this.errorMsg = "Inquiry name can't be empty"
return
}
this.saveInquiry()
},
async saveInquiry() {
const dataSet = this.currentInquiryTab.result
const tabView = this.currentInquiryTab.view
const eventName =
this.currentInquiryTab.name && this.name
? 'inquiry.saveAs'
: 'inquiry.save'
// Save inquiry
const value = await this.$store.dispatch('saveInquiry', {
inquiryTab: this.currentInquiryTab,
newName: this.name
})
// Update tab in store
this.$store.commit('updateTab', {
tab: this.currentInquiryTab,
newValues: {
name: value.name,
id: value.id,
query: value.query,
viewType: value.viewType,
viewOptions: value.viewOptions,
isSaved: true,
updatedAt: value.updatedAt
}
})
// Hide dialogs
this.$modal.hide('save')
this.$modal.hide('inquiry-conflict')
this.errorMsg = null
this.name = ''
// Signal about saving
eventBus.$emit('inquirySaved')
events.send(eventName)
},
_keyListener(e) {
if (this.$route.path === '/workspace') {
// Run query Ctrl+R or Ctrl+Enter
if ((e.key === 'r' || e.key === 'Enter') && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (!this.runDisabled) {
this.currentInquiryTab.execute()
}
return
}
// Save inquiry Ctrl+S
if (e.key === 's' && (e.ctrlKey || e.metaKey) && !e.shiftKey) {
e.preventDefault()
if (!this.isSaved) {
this.onSave()
}
return
}
// Save inquiry as Ctrl+Shift+S
if (e.key === 'S' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
e.preventDefault()
this.onSaveAs()
return
}
}
// New (blank) inquiry Ctrl+B
if (e.key === 'b' && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
this.createNewInquiry()
}
}
}
}
</script>
<style scoped>
nav {
height: 68px;
display: flex;
justify-content: space-between;
align-items: center;
background-color: var(--color-bg-light);
border-bottom: 1px solid var(--color-border-light);
box-shadow: var(--shadow-1);
box-sizing: border-box;
position: fixed;
top: 0;
left: 0;
width: 100vw;
padding: 0 16px 0 52px;
z-index: 999;
}
a {
font-size: 18px;
color: var(--color-text-base);
text-transform: none;
text-decoration: none;
margin-right: 28px;
}
a.router-link-active {
color: var(--color-accent);
}
button {
margin-left: 16px;
}
#save-note {
margin-bottom: 24px;
display: flex;
align-items: flex-start;
}
#save-note img {
margin: -3px 6px 0 0;
}
#nav-buttons {
display: flex;
}
#nav-links {
display: flex;
align-items: center;
}
#nav-links img {
width: 32px;
}
</style>

View File

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

View File

@@ -1,10 +1,10 @@
<template>
<div id="schema-container">
<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 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" />
{{ dbName }}
</div>
@@ -26,7 +26,7 @@
ref="addCsvJson"
:file="file"
:db="$store.state.db"
dialog-name="addCsvJson"
dialogName="addCsvJson"
/>
</div>
</template>
@@ -69,7 +69,8 @@ export default {
return !this.filter
? this.$store.state.db.schema
: 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() {
@@ -119,7 +120,8 @@ export default {
background-image: linear-gradient(white 73%, rgba(255, 255, 255, 0));
z-index: 2;
}
.schema, .db-name {
.schema,
.db-name {
color: var(--color-text-base);
font-size: 13px;
white-space: nowrap;
@@ -141,7 +143,7 @@ export default {
}
.db-name:hover .chevron-icon path,
>>> .table-name:hover .chevron-icon path {
:deep(.table-name:hover .chevron-icon path) {
fill: var(--color-gray-dark);
}
</style>

View File

@@ -0,0 +1,190 @@
<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'
)
},
prepareCopy(type = 'png') {
return 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,10 +1,10 @@
<template>
<div :class="['pivot-sort-btn', direction]" @click="changeSorting">
{{ value.includes('key') ? 'key' : 'value' }}
{{ modelValue.includes('key') ? 'key' : 'value' }}
<sort-icon
class="sort-icon"
:horizontal="direction === 'col'"
:asc="value.includes('a_to_z')"
:asc="modelValue.includes('a_to_z')"
/>
</div>
</template>
@@ -14,18 +14,22 @@ import SortIcon from '@/components/svg/sort'
export default {
name: 'PivotSortBtn',
props: ['direction', 'value'],
components: {
SortIcon
},
props: {
direction: String,
modelValue: String
},
emits: ['update:modelValue'],
methods: {
changeSorting() {
if (this.value === 'key_a_to_z') {
this.$emit('input', 'value_a_to_z')
} else if (this.value === 'value_a_to_z') {
this.$emit('input', 'value_z_to_a')
if (this.modelValue === 'key_a_to_z') {
this.$emit('update:modelValue', 'value_a_to_z')
} else if (this.modelValue === 'value_a_to_z') {
this.$emit('update:modelValue', 'value_z_to_a')
} 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);
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);
}

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