1
0
mirror of https://github.com/lana-k/sqliteviz.git synced 2025-12-06 18:18:53 +08:00

42 Commits

Author SHA1 Message Date
lana-k
4232f15c04 autostart, reset and fixes 2025-08-17 21:21:21 +02:00
lana-k
9d562d11b8 export and background 2025-06-09 21:08:51 +02:00
lana-k
54cdbbc8b9 wip 2025-06-06 21:24:04 +02:00
lana-k
1601514cca wip 2025-06-01 19:16:26 +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
3a05b27400 #121 0.25.1 2025-01-12 22:03:06 +01:00
lana-k
108d96a753 #121 fix lint 2025-01-12 22:00:48 +01:00
lana-k
f55a8caa92 #121 tests 2025-01-12 21:42:17 +01:00
lana-k
87f9f9eb01 use actions, add store tests 2025-01-05 22:30:12 +01:00
lana-k
d6408bdd85 #121 save inquiries in store 2025-01-05 21:06:06 +01:00
lana-k
e14696b59e update tests 2025-01-05 17:13:55 +01:00
lana-k
eee67763b5 plotly in pivot 2024-12-10 21:41:07 +01:00
lana-k
637d8d26dd #63 migrate to Vue 3 2024-10-13 16:12:21 +02:00
lana-k
b30b2181e4 #63 update slot syntax 2024-10-05 15:43:22 +02:00
lana-k
378b9fb580 #113 upgrade plotly 2024-09-23 16:46:50 +02:00
184 changed files with 28346 additions and 31593 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: 10.x
node-version: 18.x
- name: Update npm
run: npm install -g npm@7
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

@@ -17,7 +17,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@v1
with:
node-version: 10.x
node-version: 18.x
- name: Install browsers
run: |
export DEBIAN_FRONTEND=noninteractive
@@ -25,7 +25,7 @@ jobs:
sudo apt-get install -y chromium-browser firefox
- name: Update npm
run: npm install -g npm@7
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

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

45793
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,63 +1,85 @@
{
"name": "sqliteviz",
"version": "0.25.0",
"version": "0.26.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": {
"codemirror": "^5.57.0",
"@sigma/export-image": "^3.0.0",
"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",
"graphology": "^0.26.0",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"html2canvas": "^1.1.4",
"jquery": "^3.6.0",
"nanoid": "^3.1.12",
"papaparse": "^5.4.1",
"pivottable": "^2.23.0",
"plotly.js": "^1.58.4",
"plotly.js": "^2.35.2",
"promise-worker": "^2.0.1",
"react": "^16.13.1",
"react-chart-editor": "^0.45.0",
"react-dom": "^16.13.1",
"react": "^16.14.0",
"react-chart-editor": "^0.46.1",
"react-dom": "^16.14.0",
"seedrandom": "^3.0.5",
"sigma": "^3.0.1",
"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

@@ -1,58 +1,85 @@
<template>
<div id="app">
<router-view />
<modals-container />
</div>
</template>
<script>
import storedInquiries from '@/lib/storedInquiries'
import { ModalsContainer } from 'vue-final-modal'
export default {
components: { ModalsContainer },
computed: {
inquiries() {
return this.$store.state.inquiries
}
},
watch: {
inquiries: {
deep: true,
handler() {
storedInquiries.updateStorage(this.inquiries)
}
}
},
created() {
this.$store.commit('setInquiries', storedInquiries.getStoredInquiries())
}
}
</script>
<style>
@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

@@ -0,0 +1,117 @@
<template>
<Field label="Adjust sizes">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.adjustSizes"
@option-change="update('adjustSizes', $event)"
/>
</Field>
<Field label="Barnes-Hut optimize">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.barnesHutOptimize"
@option-change="update('barnesHutOptimize', $event)"
/>
</Field>
<Field v-show="modelValue.barnesHutOptimize" label="Barnes-Hut Theta">
<NumericInput
:value="modelValue.barnesHutTheta"
@update="update('barnesHutTheta', $event)"
/>
</Field>
<Field label="Gravity">
<NumericInput
:value="modelValue.gravity"
@update="update('gravity', $event)"
/>
</Field>
<Field label="Strong gravity mode">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.strongGravityMode"
@option-change="update('strongGravityMode', $event)"
/>
</Field>
<Field label="Noack's LinLog model">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.linLogMode"
@option-change="update('linLogMode', $event)"
/>
</Field>
<Field label="Out bound attraction distribution">
<RadioBlocks
:options="booleanOptions"
:activeOption="modelValue.outboundAttractionDistribution"
@option-change="update('outboundAttractionDistribution', $event)"
/>
</Field>
<Field label="Slow down">
<NumericInput
:value="modelValue.slowDown"
:min="0"
@update="update('slowDown', $event)"
/>
</Field>
<Field label="Edge weight">
<Dropdown
:options="keyOptions"
:value="modelValue.weightSource"
@change="update('weightSource', $event)"
/>
</Field>
<Field v-show="modelValue.weightSource" label="Edge weight influence">
<NumericInput
:value="modelValue.edgeWeightInfluence"
@update="update('edgeWeightInfluence', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
booleanOptions: markRaw([
{ label: 'Yes', value: true },
{ label: 'No', value: false }
])
}
},
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -0,0 +1,77 @@
<template>
<Field
label="Hierarchy attributes"
fieldContainerClassName="multiselect-field"
>
<multiselect
:modelValue="modelValue.hierarchyAttributes"
class="sqliteviz-select"
:options="keyOptions"
:multiple="true"
:hideSelected="true"
:closeOnSelect="true"
:showLabels="false"
:max="keyOptions.length"
placeholder=""
openDirection="bottom"
@update:model-value="update('hierarchyAttributes', $event)"
>
<template #maxElements>
<span class="no-results">No Results</span>
</template>
<template #placeholder>Select an Option</template>
<template #noResult>
<span class="no-results">No Results</span>
</template>
</multiselect>
</Field>
<Field label="Seed value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
/>
</Field>
</template>
<script>
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import Multiselect from 'vue-multiselect'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput),
Dropdown: applyPureReactInVue(Dropdown),
Multiselect
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.sqliteviz-select.multiselect--active .multiselect__input) {
width: 100% !important;
}
:deep(.multiselect-field .field__widget > *) {
flex-grow: 1 !important;
}
</style>

View File

@@ -0,0 +1,137 @@
<template>
<Field label="Color">
<RadioBlocks
:options="edgeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
</Field>
<Field>
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@option-change="updateSettings('sourceUsage', $event)"
/>
</Field>
<Field v-if="modelValue.sourceUsage === 'map_to'">
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@colorscale-change="updateSettings('colorscale', $event)"
/>
</Field>
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"
@option-change="updateSettings('colorscaleDirection', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
ColorPicker: applyPureReactInVue(ColorPicker)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
edgeColorTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' }
]),
сolorAsOptions: markRaw([
{ label: 'Continious', value: 'continious' },
{ label: 'Categorical', value: 'categorical' }
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
}
}
}
},
methods: {
updateColorType(newColorType) {
const currentColorType = this.modelValue.type
this.defaultColorSettings[currentColorType] = this.modelValue
this.$emit('update:modelValue', {
type: newColorType,
...this.defaultColorSettings[newColorType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.customPickerContainer) {
float: right;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<Field label="Size">
<RadioBlocks
:options="edgeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
:min="1"
@update="updateSettings('value', $event)"
/>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Minimum size">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"
/>
</Field>
</template>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
edgeSizeTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' }
]),
defaultSizeSettings: {
constant: { value: 4 },
variable: { source: null, scale: 1, min: 1 }
}
}
},
methods: {
updateSizeType(newSizeType) {
const currentSizeType = this.modelValue.type
this.defaultSizeSettings[currentSizeType] = this.modelValue
this.$emit('update:modelValue', {
type: newSizeType,
...this.defaultSizeSettings[newSizeType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -0,0 +1,43 @@
<template>
<Field label="Initial iterations">
<NumericInput
:value="modelValue.initialIterationsAmount"
:min="1"
@update="update('initialIterationsAmount', $event)"
/>
</Field>
<Field label="Scaling ratio">
<NumericInput
:value="modelValue.scalingRatio"
@update="update('scalingRatio', $event)"
/>
</Field>
</template>
<script>
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -0,0 +1,155 @@
<template>
<Field label="Color">
<RadioBlocks
:options="nodeColorTypeOptions"
:activeOption="modelValue.type"
@option-change="updateColorType"
/>
<Field v-if="modelValue.type === 'constant'">
<ColorPicker
:selectedColor="modelValue.value"
@color-change="updateSettings('value', $event)"
/>
</Field>
<template v-else>
<Field>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
<Dropdown
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedColorMethodOptions"
:value="modelValue.method"
@change="updateSettings('method', $event)"
/>
</Field>
<Field>
<RadioBlocks
:options="colorSourceUsageOptions"
:activeOption="modelValue.sourceUsage"
@option-change="updateSettings('sourceUsage', $event)"
/>
</Field>
<Field v-if="modelValue.sourceUsage === 'map_to'">
<ColorscalePicker
:selected="modelValue.colorscale"
className="colorscale-picker"
@colorscale-change="updateSettings('colorscale', $event)"
/>
</Field>
</template>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Color as">
<RadioBlocks
:options="сolorAsOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field v-if="modelValue.type !== 'constant'" label="Colorscale direction">
<RadioBlocks
:options="сolorscaleDirections"
:activeOption="modelValue.colorscaleDirection"
@option-change="updateSettings('colorscaleDirection', $event)"
/>
</Field>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import ColorscalePicker from 'react-chart-editor/lib/components/widgets/ColorscalePicker'
import ColorPicker from 'react-chart-editor/lib/components/widgets/ColorPicker'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field),
ColorscalePicker: applyPureReactInVue(ColorscalePicker),
ColorPicker: applyPureReactInVue(ColorPicker)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
nodeColorTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' },
{ label: 'Calculated', value: 'calculated' }
]),
nodeCalculatedColorMethodOptions: markRaw([
{ label: 'Degree', value: 'degree' },
{ label: 'In degree', value: 'inDegree' },
{ label: 'Out degree', value: 'outDegree' }
]),
сolorAsOptions: markRaw([
{ label: 'Continious', value: 'continious' },
{ label: 'Categorical', value: 'categorical' }
]),
сolorscaleDirections: markRaw([
{ label: 'Normal', value: 'normal' },
{ label: 'Recersed', value: 'reversed' }
]),
colorSourceUsageOptions: markRaw([
{ label: 'Direct', value: 'direct' },
{ label: 'Map to', value: 'map_to' }
]),
defaultColorSettings: {
constant: { value: '#1F77B4' },
variable: {
source: null,
sourceUsage: 'map_to',
colorscale: null,
mode: 'categorical',
colorscaleDirection: 'normal'
},
calculated: {
method: 'degree',
sourceUsage: 'map_to',
colorscale: null,
mode: 'continious',
colorscaleDirection: 'normal'
}
}
}
},
methods: {
updateColorType(newColorType) {
const currentColorType = this.modelValue.type
this.defaultColorSettings[currentColorType] = this.modelValue
this.$emit('update:modelValue', {
type: newColorType,
...this.defaultColorSettings[newColorType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>
<style scoped>
:deep(.customPickerContainer) {
float: right;
}
</style>

View File

@@ -0,0 +1,118 @@
<template>
<Field label="Size">
<RadioBlocks
:options="nodeSizeTypeOptions"
:activeOption="modelValue.type"
@option-change="updateSizeType"
/>
<Field>
<NumericInput
v-if="modelValue.type === 'constant'"
:value="modelValue.value"
:min="1"
@update="updateSettings('value', $event)"
/>
<Dropdown
v-if="modelValue.type === 'variable'"
:options="keyOptions"
:value="modelValue.source"
@change="updateSettings('source', $event)"
/>
<Dropdown
v-if="modelValue.type === 'calculated'"
:options="nodeCalculatedSizeMethodOptions"
:value="modelValue.method"
@change="updateSettings('method', $event)"
/>
</Field>
</Field>
<template v-if="modelValue.type !== 'constant'">
<Field label="Size scale">
<NumericInput
:value="modelValue.scale"
@update="updateSettings('scale', $event)"
/>
</Field>
<Field label="Size mode">
<RadioBlocks
:options="nodeSizeModeOptions"
:activeOption="modelValue.mode"
@option-change="updateSettings('mode', $event)"
/>
</Field>
<Field label="Minimum size">
<NumericInput
:value="modelValue.min"
@update="updateSettings('min', $event)"
/>
</Field>
</template>
</template>
<script>
import { markRaw } from 'vue'
import { applyPureReactInVue } from 'veaury'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import Dropdown from 'react-chart-editor/lib/components/widgets/Dropdown'
import RadioBlocks from 'react-chart-editor/lib/components/widgets/RadioBlocks'
import Field from 'react-chart-editor/lib/components/fields/Field'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Dropdown: applyPureReactInVue(Dropdown),
NumericInput: applyPureReactInVue(NumericInput),
RadioBlocks: applyPureReactInVue(RadioBlocks),
Field: applyPureReactInVue(Field)
},
props: {
modelValue: Object,
keyOptions: Array
},
emits: ['update:modelValue'],
data() {
return {
nodeSizeTypeOptions: markRaw([
{ label: 'Constant', value: 'constant' },
{ label: 'Variable', value: 'variable' },
{ label: 'Calculated', value: 'calculated' }
]),
nodeCalculatedSizeMethodOptions: markRaw([
{ label: 'Degree', value: 'degree' },
{ label: 'In degree', value: 'inDegree' },
{ label: 'Out degree', value: 'outDegree' }
]),
nodeSizeModeOptions: markRaw([
{ label: 'Area', value: 'area' },
{ label: 'Diameter', value: 'diameter' }
]),
defaultSizeSettings: {
constant: { value: 4 },
variable: { source: null, scale: 1, mode: 'diameter', min: 1 },
calculated: { method: 'degree', scale: 1, mode: 'diameter', min: 1 }
}
}
},
methods: {
updateSizeType(newSizeType) {
const currentSizeType = this.modelValue.type
this.defaultSizeSettings[currentSizeType] = this.modelValue
this.$emit('update:modelValue', {
type: newSizeType,
...this.defaultSizeSettings[newSizeType]
})
},
updateSettings(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

View File

@@ -0,0 +1,34 @@
<template>
<Field label="Seed value">
<NumericInput
:value="modelValue.seedValue"
@update="update('seedValue', $event)"
/>
</Field>
</template>
<script>
import { applyPureReactInVue } from 'veaury'
import Field from 'react-chart-editor/lib/components/fields/Field'
import NumericInput from 'react-chart-editor/lib/components/widgets/NumericInput'
import 'react-chart-editor/lib/react-chart-editor.css'
export default {
components: {
Field: applyPureReactInVue(Field),
NumericInput: applyPureReactInVue(NumericInput)
},
props: {
modelValue: Object
},
emits: ['update:modelValue'],
methods: {
update(attributeName, value) {
this.$emit('update:modelValue', {
...this.modelValue,
[attributeName]: value
})
}
}
}
</script>

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,14 @@
<template>
<modal
:name="name"
classes="dialog"
height="auto"
:modalId="name"
class="dialog"
:clickToClose="false"
:contentTransition="{ name: 'loading-dialog' }"
:overlayTransition="{ name: 'loading-dialog' }"
>
<div class="dialog-header">
{{ title }}
<close-icon @click="$emit('cancel')" :disabled="loading"/>
<close-icon :disabled="loading" @click="$emit('cancel')" />
</div>
<div class="dialog-body">
<div v-if="loading" class="loading-dialog-body">
@@ -15,13 +16,17 @@
{{ 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')"
>
@@ -29,6 +34,7 @@
</button>
<button
class="primary"
type="button"
:disabled="loading"
@click="$emit('action')"
>
@@ -43,7 +49,8 @@ import LoadingIndicator from '@/components/LoadingIndicator'
import CloseIcon from '@/components/svg/close'
export default {
name: 'loadingDialog',
name: 'LoadingDialog',
components: { LoadingIndicator, CloseIcon },
props: {
loadingMsg: String,
successMsg: String,
@@ -52,6 +59,7 @@ export default {
title: String,
loading: Boolean
},
emits: ['cancel', 'action'],
watch: {
loading() {
if (this.loading) {
@@ -59,7 +67,6 @@ export default {
}
}
},
components: { LoadingIndicator, CloseIcon },
methods: {
cancel() {
this.$emit('cancel')
@@ -68,6 +75,31 @@ export default {
}
</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

@@ -0,0 +1,40 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 4C5 5.10457 4.10457 6 3 6C1.89543 6 1 5.10457 1 4C1 2.89543 1.89543 2 3 2C4.10457 2 5 2.89543 5 4Z"
fill="#A2B1C6"
/>
<path
d="M17 7.5C17 8.88071 15.8807 10 14.5 10C13.1193 10 12 8.88071 12 7.5C12 6.11929 13.1193 5 14.5 5C15.8807 5 17 6.11929 17 7.5Z"
fill="#A2B1C6"
/>
<path
d="M8 13.5C8 14.8807 6.88071 16 5.5 16C4.11929 16 3 14.8807 3 13.5C3 12.1193 4.11929 11 5.5 11C6.88071 11 8 12.1193 8 13.5Z"
fill="#A2B1C6"
/>
<path
d="M2.93128 5.31436L3.90527 5.08778L5.48693 11.8867L4.51294 12.1133L2.93128 5.31436Z"
fill="#A2B1C6"
/>
<path
d="M12.9447 7.79159L13.5548 8.58392L7.30516 13.3962L6.69507 12.6038L12.9447 7.79159Z"
fill="#A2B1C6"
/>
<path
d="M14.1316 6.51712L3.13166 3.51723L2.86844 4.48202L13.8684 7.48191L14.1316 6.51712Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'GraphIcon'
}
</script>

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

@@ -0,0 +1,20 @@
<template>
<svg
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 4C3 3.44772 3.44772 3 4 3H14C14.5523 3 15 3.44772 15 4V14C15 14.5523 14.5523 15 14 15H4C3.44772 15 3 14.5523 3 14V4Z"
fill="#A2B1C6"
/>
</svg>
</template>
<script>
export default {
name: 'StopIcon'
}
</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"
@@ -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,52 @@
import PropTypes from 'prop-types'
import React, { Component } from 'react'
import { localizeString } from 'react-chart-editor/lib'
class EditorControls extends Component {
constructor(props, context) {
super(props, context)
this.localize = key =>
localizeString(this.props.dictionaries || {}, this.props.locale, key)
}
getChildContext() {
return {
dictionaries: this.props.dictionaries || {},
localize: this.localize,
locale: this.props.locale
}
}
render() {
return (
<div
className={
'editor_controls plotly-editor--theme-provider' +
`${this.props.className ? ` ${this.props.className}` : ''}`
}
>
{this.props.children}
</div>
)
}
}
EditorControls.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
dictionaries: PropTypes.object,
locale: PropTypes.string
}
EditorControls.defaultProps = {
locale: 'en'
}
EditorControls.childContextTypes = {
dictionaries: PropTypes.object,
locale: PropTypes.string,
localize: PropTypes.func
}
export default EditorControls

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

307
src/lib/graphHelper.js Normal file
View File

@@ -0,0 +1,307 @@
import { COLOR_PICKER_CONSTANTS } from 'react-colorscales'
import tinycolor from 'tinycolor2'
const TYPE_NODE = 0
const TYPE_EDGE = 1
const DEFAULT_SCALE = COLOR_PICKER_CONSTANTS.DEFAULT_SCALE
export function buildNodes(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const { objectType, nodeId } = options.structure
if (objectType && nodeId) {
const nodes = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_NODE)
nodes.forEach(node => {
graph.addNode(node[nodeId], {
data: node,
labelColor: options.style.nodes.label.color
})
})
}
}
export function buildEdges(graph, dataSources, options) {
const docColumn = Object.keys(dataSources)[0] || 'doc'
const { objectType, edgeSource, edgeTarget } = options.structure
if (objectType && edgeSource && edgeTarget) {
const edges = dataSources[docColumn]
.map(json => JSON.parse(json))
.filter(item => item[objectType] === TYPE_EDGE)
edges.forEach(edge => {
const source = edge[edgeSource]
const target = edge[edgeTarget]
if (graph.hasNode(source) && graph.hasNode(target)) {
graph.addEdge(source, target, {
data: edge,
labelColor: options.style.edges.label.color
})
}
})
}
}
export function updateNodes(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateNodeColorMethod(graph, attributeUpdates.color))
}
graph.forEachNode(nodeId => {
graph.updateNode(nodeId, attributes => {
const newAttributes = { ...attributes }
changeMethods.forEach(method => method(newAttributes, nodeId))
return newAttributes
})
})
}
export function updateEdges(graph, attributeUpdates) {
const changeMethods = []
if (attributeUpdates.label) {
changeMethods.push(getUpdateLabelMethod(attributeUpdates.label))
}
if (attributeUpdates.size) {
changeMethods.push(getUpdateSizeMethod(graph, attributeUpdates.size))
}
if (attributeUpdates.color) {
changeMethods.push(getUpdateEdgeColorMethod(graph, attributeUpdates.color))
}
if ('showDirection' in attributeUpdates) {
changeMethods.push(
attributes =>
(attributes.type = attributeUpdates.showDirection ? 'arrow' : 'line')
)
}
graph.forEachEdge((edgeId, attributes, source, target) => {
graph.updateEdgeWithKey(edgeId, source, target, attr => {
const newAttributes = { ...attr }
changeMethods.forEach(method => method(newAttributes, edgeId))
return newAttributes
})
})
}
function getUpdateLabelMethod(labelSettings) {
const { source, color } = labelSettings
return attributes => {
const label = attributes.data[source] ?? ''
attributes.label = label.toString()
attributes.labelColor = color
}
}
function getUpdateSizeMethod(graph, sizeSettings) {
const { type, value, source, scale, mode, min, method } = sizeSettings
if (type === 'constant') {
return attributes => (attributes.size = value)
} else if (type === 'variable') {
return getVariabledSizeMethod(mode, source, scale, min)
} else {
return (attributes, nodeId) =>
(attributes.size = Math.max(graph[method](nodeId) * scale, min))
}
}
function getDirectVariableColorUpdateMethod(source) {
return attributes =>
(attributes.color = tinycolor(attributes.data[source]).toHexString())
}
function getUpdateNodeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode,
method
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else if (type === 'variable') {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(nodeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getNodeValueScale
)
: getDirectVariableColorUpdateMethod(source)
} else {
return getColorMethod(
graph,
mode,
nodeId => graph[method](nodeId),
colorscale,
colorscaleDirection,
getNodeValueScale
)
}
}
function getUpdateEdgeColorMethod(graph, colorSettings) {
const {
type,
value,
source,
sourceUsage,
colorscale,
colorscaleDirection,
mode
} = colorSettings
if (type === 'constant') {
return attributes => (attributes.color = value)
} else {
return sourceUsage === 'map_to'
? getColorMethod(
graph,
mode,
(edgeId, attributes) => attributes.data[source],
colorscale,
colorscaleDirection,
getEdgeValueScale
)
: getDirectVariableColorUpdateMethod(source)
}
}
function getVariabledSizeMethod(mode, source, scale, min) {
if (mode === 'diameter') {
return attributes =>
(attributes.size = Math.max(
(attributes.data[source] / 2) * scale,
min / 2
))
} else if (mode === 'area') {
return attributes =>
(attributes.size = Math.max(
Math.sqrt((attributes.data[source] / 2) * scale),
min / 2
))
} else {
return attributes =>
(attributes.size = Math.max(attributes.data[source] * scale, min))
}
}
function getColorMethod(
graph,
mode,
sourceGetter,
selectedColorscale,
colorscaleDirection,
valueScaleGetter
) {
const valueScale = valueScaleGetter(graph, sourceGetter)
let colorscale = selectedColorscale || DEFAULT_SCALE
if (colorscaleDirection === 'reversed') {
colorscale = [...colorscale].reverse()
}
if (mode === 'categorical') {
const colorMap = Object.fromEntries(
valueScale.map((value, index) => [
value,
colorscale[index % colorscale.length]
])
)
return (attributes, nodeId) => {
const category = sourceGetter(nodeId, attributes)
attributes.color = colorMap[category]
}
} else {
const min = valueScale[0]
const max = valueScale[valueScale.length - 1]
const normalizedColorscale = colorscale.map((color, index) => [
index / (colorscale.length - 1),
tinycolor(color).toRgb()
])
return (attributes, nodeId) => {
const value = sourceGetter(nodeId, attributes)
const normalizedValue = (value - min) / (max - min)
if (isNaN(normalizedValue)) {
return
}
const exactMatch = normalizedColorscale.find(
([value]) => value === normalizedValue
)
if (exactMatch) {
attributes.color = tinycolor(exactMatch[1]).toHexString()
return
}
const rightColorIndex = normalizedColorscale.findIndex(
([value]) => value >= normalizedValue
)
const leftColorIndex = (rightColorIndex || 1) - 1
const right = normalizedColorscale[rightColorIndex]
const left = normalizedColorscale[leftColorIndex]
const interpolationFactor =
(normalizedValue - left[0]) / (right[0] - left[0])
const r0 = left[1].r
const g0 = left[1].g
const b0 = left[1].b
const r1 = right[1].r
const g1 = right[1].g
const b1 = right[1].b
attributes.color = tinycolor({
r: r0 + interpolationFactor * (r1 - r0),
g: g0 + interpolationFactor * (g1 - g0),
b: b0 + interpolationFactor * (b1 - b0)
}).toHexString()
}
}
}
function getNodeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceNodes((res, nodeId, attributes) => {
res.add(sourceGetter(nodeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
function getEdgeValueScale(graph, sourceGetter) {
const scaleSet = graph.reduceEdges((res, edgeId, attributes) => {
res.add(sourceGetter(edgeId, attributes))
return res
}, new Set())
return Array.from(scaleSet).sort((a, b) => a - b)
}
export function getOptionsFromDataSources(dataSources) {
if (!dataSources) {
return []
}
return Object.keys(dataSources).map(name => ({
value: name,
label: name
}))
}
export default {
getOptionsFromDataSources
}

View File

@@ -36,46 +36,21 @@ export default {
return inquiryTab.isPredefined || !inquiryTab.name
},
save (inquiryTab, newName) {
const value = {
id: inquiryTab.isPredefined ? nanoid() : inquiryTab.id,
query: inquiryTab.query,
viewType: inquiryTab.dataView.mode,
viewOptions: inquiryTab.dataView.getOptionsForSave(),
name: newName || inquiryTab.name
}
// Get inquiries from local storage
const myInquiries = this.getStoredInquiries()
// Set createdAt
if (newName) {
value.createdAt = new Date()
} else {
var inquiryIndex = myInquiries.findIndex(oldInquiry => oldInquiry.id === inquiryTab.id)
value.createdAt = myInquiries[inquiryIndex].createdAt
}
// Insert in inquiries list
if (newName) {
myInquiries.push(value)
} else {
myInquiries[inquiryIndex] = value
}
// Save to local storage
this.updateStorage(myInquiries)
return value
},
updateStorage(inquiries) {
localStorage.setItem('myInquiries', JSON.stringify({ version: this.version, inquiries }))
localStorage.setItem(
'myInquiries',
JSON.stringify({ version: this.version, inquiries })
)
},
serialiseInquiries(inquiryList) {
const preparedData = JSON.parse(JSON.stringify(inquiryList))
preparedData.forEach(inquiry => delete inquiry.isPredefined)
return JSON.stringify({ version: this.version, inquiries: preparedData }, null, 4)
return JSON.stringify(
{ version: this.version, inquiries: preparedData },
null,
4
)
},
deserialiseInquiries(str) {
@@ -91,7 +66,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()
}
@@ -101,8 +78,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
@@ -39,7 +41,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

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

View File

@@ -1,12 +1,9 @@
import Vue from 'vue'
import 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

@@ -14,12 +14,24 @@ export default {
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
@@ -49,16 +61,24 @@ 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
},
setPredefinedInquiriesLoaded(state, value) {
state.predefinedInquiriesLoaded = value
},
setInquiries(state, value) {
state.inquiries = value
},
setIsWorkspaceVisible(state, value) {
state.isWorkspaceVisible = value
}
}

View File

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

View File

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

View File

@@ -2,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',
@@ -183,7 +211,6 @@ export default {
mixins: [tooltipMixin],
data() {
return {
inquiries: [],
filter: null,
newName: null,
processedInquiryId: null,
@@ -198,6 +225,9 @@ export default {
}
},
computed: {
inquiries() {
return this.$store.state.inquiries
},
predefinedInquiries() {
return this.$store.state.predefinedInquiries.map(inquiry => {
inquiry.isPredefined = true
@@ -211,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
@@ -221,46 +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() {
this.inquiries = storedInquiries.getStoredInquiries()
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)
@@ -282,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 ''
}
@@ -297,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`
@@ -314,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
@@ -330,31 +379,19 @@ export default {
this.errorMsg = "Inquiry name can't be empty"
return
}
const processedInquiry = this.inquiries[this.processedInquiryIndex]
processedInquiry.name = this.newName
this.$set(this.inquiries, this.processedInquiryIndex, processedInquiry)
// update inquiries in local storage
storedInquiries.updateStorage(this.inquiries)
// update tab, if renamed inquiry is opened
const tab = this.$store.state.tabs
.find(tab => tab.id === processedInquiry.id)
if (tab) {
this.$store.commit('updateTab', {
tab,
newValues: {
name: this.newName
}
this.$store.dispatch('renameInquiry', {
inquiryId: this.processedInquiryId,
newName: this.newName
})
}
// hide dialog
this.$modal.hide('rename')
},
duplicateInquiry(index) {
const newInquiry = storedInquiries.duplicateInquiry(this.showedInquiries[index])
this.inquiries.push(newInquiry)
storedInquiries.updateStorage(this.inquiries)
const newInquiry = storedInquiries.duplicateInquiry(
this.showedInquiries[index]
)
this.$store.dispatch('addInquiry', newInquiry)
},
showDeleteDialog(idsSet) {
this.deleteGroup = idsSet.size > 1
@@ -366,62 +403,48 @@ export default {
deleteInquiry() {
this.$modal.hide('delete')
if (!this.deleteGroup) {
this.inquiries.splice(this.processedInquiryIndex, 1)
// Close deleted inquiry tab if it was opened
const tab = this.$store.state.tabs
.find(tab => tab.id === this.processedInquiryId)
if (tab) {
this.$store.commit('deleteTab', tab)
}
this.$store.dispatch(
'deleteInquiries',
new Set().add(this.processedInquiryId)
)
// Clear checkbox
if (this.selectedInquiriesIds.has(this.processedInquiryId)) {
this.selectedInquiriesIds.delete(this.processedInquiryId)
}
} else {
this.inquiries = this.inquiries.filter(
inquiry => !this.selectedInquiriesIds.has(inquiry.id)
)
// Close deleted inquiries if it was opened
const tabs = this.$store.state.tabs
let i = tabs.length - 1
while (i > -1) {
if (this.selectedInquiriesIds.has(tabs[i].id)) {
this.$store.commit('deleteTab', tabs[i])
}
i--
}
this.$store.dispatch('deleteInquiries', this.selectedInquiriesIds)
// Clear checkboxes
this.selectedInquiriesIds.clear()
}
this.selectedInquiriesCount = this.selectedInquiriesIds.size
storedInquiries.updateStorage(this.inquiries)
},
exportToFile(inquiryList, fileName) {
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.inquiries = this.inquiries.concat(importedInquiries)
storedInquiries.updateStorage(this.inquiries)
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))
@@ -429,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()

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